What Broke After I Moved My Agent's Rules into a CLI (And How Synthetic Scars Reshaped ag_ops v2.1) A developer released ag_ops v2.1, an MIT-licensed governance engine for AI agents that grew from a 1,031-line test guardrail into a 1,783-line tool after four bugs surfaced in daily multi-agent use. The v1 flaws included a single hardcoded Merkle receipt file that concurrent subagents overwrote, a global *.tmp exclusion that let stray fixtures escape sealing, an unanchored error-extraction pattern that mistook a "DONE" banner for a pytest failure, and state tracking that reported never-created artifacts as deleted. The rewrite was shaped by comment-thread criticism that a prose rationale for unlocking sealed tests is "just an honor system with extra steps" and by Randal L. Schwartz's Synthetic Scars architecture. How four embarrassing bugs in v1, two sharp comment threads, and Randal L. Schwartz's Synthetic Scars architecture pushed a 1,031-line test guardrail into a 1,783-line governance engine—and nearly blocked its own release along the way. In Antigravity Field Notes 1 https://dev.to/fgrandon/i-cut-my-agents-fixed-context-by-42-and-moved-its-test-rules-into-a-hash-checked-cli-5dmp , I wrote about auditing my daily Google Antigravity setup, trimming the fixed per-turn prompt prefix by 42.4%, and moving test-tampering checks out of Markdown and into a 1,031-line 39,144 -byte standard-library Python script. Shipping that first version felt clean for about forty-eight hours. Then two things happened at once: readers in the comments started poking at the exact joints I had taped over, and I started throwing multi-agent workloads at v1 every day. In the comment thread on Field Notes 1, @max quimby https://dev.to/max quimby put a finger straight on the softest spot in the design: if an agent with shell access can unlock a Merkle-sealed test tree by passing renegotiate --reason "fixed tests" , a prose rationale is just an honor system with extra steps. Any language model can invent a plausible excuse in 200 milliseconds. Around the same time, howcani highlighted a subtler problem on the red side of the cycle: witnessing a non-zero exit code proves that a command failed, not that the failure was informative or tied to the right invariant. Meanwhile, v1 outgrew its original single-file Gist. As the tool gained a full automated test harness, changelog, and stricter contracts, I moved it into a standalone MIT repository at FranciscoGrandon/ag-ops https://github.com/FranciscoGrandon/ag-ops . That repository exists because v1 developed four concrete cracks once real multi-agent sessions a main orchestrator coordinating parallel subagents hit it every day. .ag state/merkle guard.json Version 1 wrote a single hardcoded file .ag state/merkle guard.json per state directory. That worked fine when one agent sealed all of tests/ . It fell apart the afternoon two subagents worked concurrently on separate modules in the same folder tests/test a.py and tests/test b.py , because the second witness-red run quietly overwrote the first agent's receipt. Worse, v1 excluded .tmp globally during Merkle traversal, so a stray fixture dropped at tests/sub/datos.tmp went completely unnoticed. Running witness-red against an empty directory also happily sealed zero files and issued a valid receipt over nothing. distill that panicked over polite success logs To extract pytest failures without dumping thousands of lines into context, v1 scanned output with an unanchored E\s{3} pattern meant to catch pytest's E AssertionError: lines. Then a batch runner printed this status banner near the top of a job: Step 1 DONE everything fine Look closely at the end of DONE followed by four spaces: E . My error extractor saw "DONE " , panicked over the trailing E plus three spaces, and spent its entire root-cause line budget preserving a cheerful progress banner 2,000 lines above the real crash. I had built a failure filter that was allergic to well-tabulated optimism. state Our lexical runlevel tracker 00 plan.json , 10 build.json lets long-running sessions survive context compaction. In v1, if Step 10 registered an expected output path --artifacts dist/schema.json while marking the phase EN PROGRESO before the build finished, state write recorded "NOT FOUND" . On the next check, state status reported FILE DELETED for an artifact that had never been born; once dist/schema.json did appear on disk, state status never sealed its SHA-256 hash, leaving later mutations invisible. Reusing step 10 with a new slug also unlinked the previous step file without warning. ast-lint In v1, TK002 caught forbidden bg= / fg= arguments on themed ttk widgets using naive prefix heuristics. If an agent wrote import tkinter.ttk without as ttk and called tkinter.ttk.Button root, bg="red" , the linter missed it completely. Conversely, if a developer wrote a plain function named def ttk helper bg="blue" : , startswith "ttk " accused it of being a broken Tkinter widget. Beyond that, AUTH001 authorship checks were hardcoded instead of opt-in, and the linter had nothing to say when an agent took its two favorite shortcuts under pressure: silencing failures with except Exception: pass or importing from unittest.mock import MagicMock straight into a production module. My first instinct after finding those cracks was the bad habit most of us learned over the last two years: open GEMINI.md and type four new paragraphs of sternly worded rules. Never use bare except: pass . Never import unittest.mock outside tests/ . Always clean up async streams. Before doing that, I spent several evenings studying Randal L. Schwartz https://dev.to/randalschwartz 's ongoing Synthetic Scars series especially Part 12, Giving AI Pain: The Architecture of Synthetic Scars & The Rapid-Regret Miner https://dev.to/gde/giving-ai-pain-the-architecture-of-synthetic-scars-the-rapid-regret-miner-1740 and testing how his systems model mapped onto what I was seeing inside Antigravity. I am approaching this as a working systems engineer, not a theorist, so what grabbed me in Randal's writing was how directly three of his observations diagnosed my own bugs: except blocks engineered to turn pytest green while hollowing out the system underneath. I goal vs. I path π across 588 production scars: π , such as wiring an error/cancel exit out of an async fetch state or guarding a callback against late arrivals after teardown. Humans learn If I wanted my Antigravity sessions to respect I path π without bloating GEMINI.md back up to 20 KB, the pain had to live in exit codes, file hashes, AST nodes, and Git history. Translating those ideas into standard-library Python pushed ag ops.py from a 1,031-line test wrapper to a 1,783-line governance engine v2.1.0 , still running on zero third-party packages. | Dimension | ag ops v1.0 | ag ops v2.1.0 | Delta / Engineering Notes | |---|---|---|---| | Core Script Size | 1,031 lines 39,144 B | 1,783 lines 74,674 B | +72.9% lines; single-file architecture preserved | | External Dependencies | 0 14 stdlib modules | 0 15 stdlib modules | Added fnmatch for receipt-bound --ignore globs | | CLI Subcommands | 4 guard , distill , state , ast-lint | 5 + regret-mine | Added Stage-1 AST/Git Rapid-Regret Miner | | Static Rules ast-lint | 3 .py only | 9 5 .py + 4 .dot + --context-map | TK001 , TK002 , PATH001 , PATH002 , opt-in AUTH001 + Graphviz DOT001 , DOT002 , SCAR-PROC-152 , SCAR-ASYNC-01 | | Guard Receipts & Renegotiation | Single merkle guard.json , prose --reason | merkle guard