{"slug": "what-broke-after-i-moved-my-agent-s-rules-into-a-cli-and-how-synthetic-scars-ag", "title": "What Broke After I Moved My Agent's Rules into a CLI (And How Synthetic Scars Reshaped ag_ops v2.1)", "summary": "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.", "body_md": "*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.*\n\nIn [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.\n\nShipping 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.\n\nIn 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.\n\nMeanwhile, 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.\n\n`.ag_state/merkle_guard.json`\nVersion 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.\n\n`distill` that panicked over polite success logs\nTo 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:\n\n```\n Step 1 DONE    everything fine\n```\n\nLook 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.\n\n`state`\nOur 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.\n\n`ast-lint`\nIn 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.\n\nMy 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.*\n\nBefore 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.\n\nI 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:\n\n`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.\n\nTranslating 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.\n\n| Dimension | `ag_ops` v1.0 | `ag_ops` v2.1.0 | Delta / Engineering Notes | \n|---|---|---|---|\n| **Core Script Size** | `1,031` lines (`39,144 B` ) | `1,783` lines (`74,674 B` ) | `+72.9%` lines; single-file architecture preserved | \n| **External Dependencies** | `0` (`14` stdlib modules) | `0` (`15` stdlib modules) | Added `fnmatch` for receipt-bound`--ignore` globs | \n| **CLI Subcommands** | `4` (`guard` ,`distill` ,`state` ,`ast-lint` ) | `5` (+`regret-mine` ) | Added Stage-1 AST/Git Rapid-Regret Miner | \n| **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` | \n| **Guard Receipts & Renegotiation** | Single `merkle_guard.json` , prose`--reason` | `merkle_guard_<sha256[:12]>.json` ,`SCAR-PROC-150` | Target isolation, persistent `collisions` array,`--require-witness` | \n| **Automated Test Suite** | Ad-hoc script checks | `tests/test_ag_ops.py` (`333` lines) | `6/6` integration suites using isolated temp dirs | \n\nHere is how each piece works in practice, using real invocations and verbatim JSON payloads from `ag_ops.py` v2.1.0.\n\n`guard` and `SCAR-PROC-150`: Isolated Receipts, Collision Telemetry, and Constructive Witnesses\nFirst, `_receipt_path_for_target` killed the single-file collision bug by hashing the normalized resolved target path (`os.path.normcase(str(test_path.resolve()))`) into `.ag_state/merkle_guard_<sha256[:12]>.json`. Two subagents sealing `tests/test_a.py` and `tests/test_b.py` in parallel now write independent v2 receipts.\n\nExclusions in `_compute_merkle_tree` were stripped down to surgical minimums: only the exact `state_dir`, the receipt file itself, `__pycache__`, `.pyc/.pyo/.pyd`, root-level `.pytest_cache`, and explicit globs passed via `--ignore` (which get persisted inside `ignored_patterns` in the receipt so `verify-green` enforces the exact same boundary). Empty directories fail immediately with `EMPTY_TEST_TARGET` (`exit 1`), and `witness-red` blocks non-assertion runner failures (exit codes `2, 3, 4, 5, 126, 127, 9009`), returning `INVALID_TEST_INVOCATION` unless overridden via `--expected-exit`.\n\nEvery time `verify-green` catches a violation (`COMMAND_MISMATCH`, `EACCES_TEST_TAMPERING`, or `EACCES_TEST_TAMPERING_DURING_RUN` at `exit 2`; `COMMAND_TIMED_OUT` or `TEST_STILL_FAILING` at `exit 1`), `_record_guard_collision` appends a UTC-stamped event into the receipt's `collisions` list before exiting. Even if an agent backs out its unauthorized test edit two minutes later, the collision stays on the permanent record.\n\nThen came [@max_quimby](https://dev.to/max_quimby)'s question: how do you stop lazy prose renegotiations when subagents run unattended without an interactive TTY? You cannot call blocking `input(\"Allow test edit? [y/N]\")` inside a background pipe without hanging or raising `EOFError`. Instead, v2.1.0 implements Schwartz's **SCAR-PROC-150** constructive witness gate via `--require-witness`:\n\n```\n# Agent tries to unlock a modified test with a hand-wavy prose excuse:\npython ag_ops.py guard renegotiate \\\n  --test-dir tests/test_billing.py \\\n  --reason \"fixed tests\" \\\n  --require-witness \\\n  --json\n{\n  \"ok\": false,\n  \"error\": \"LAZY_RETRACTION_REJECTED: --require-witness is active; specify --witness-type (spec-collision | api-lock | arch-fork).\"\n}\n```\n\nWith `--require-witness` active, prose alone gets `exit 1`. To obtain a `RENEGOTIATED_RED` receipt, the agent has to pass a proof that `_verify_constructive_witness` can verify mechanically against disk or compiler output:\n\n`spec-collision`: requires `--conflict-a file:line` and `--conflict-b file:line`, verifying via `_validate_file_line_ref` that both files exist on disk, both line numbers are in bounds, and the two locations are distinct.`api-lock`: inspects the failing test run's `stdout`/` stderr` (case-insensitively) for concrete boundary/type lock diagnostics (`TypeError`, `AttributeError`, `ImportError`, `Final`, `Protocol`, `sealed`, `cannot assign`, `visibility`, `private`, `readonly`).` arch-fork`: requires `--fork-options` with at least two pipe-separated `Label@file:line` blast-radius anchors verified on disk.\n\n```\npython ag_ops.py guard renegotiate \\\n  --test-dir tests/test_billing.py \\\n  --reason \"RFC-104 mandates UUIDv4 strings while legacy parser returns int\" \\\n  --require-witness \\\n  --witness-type spec-collision \\\n  --conflict-a docs/rfc_104.md:3 \\\n  --conflict-b src/billing.py:2 \\\n  --json\n{\n  \"ok\": true,\n  \"status\": \"RENEGOTIATED_RED\",\n  \"reason\": \"RFC-104 mandates UUIDv4 strings while legacy parser returns int\",\n  \"new_tree_hash\": \"c3704260ab99c04b3f7316f3fb075eb305853816466d3155762eff89714174dd\",\n  \"previous_tree_hash\": \"de4dbd3b859429a6c711918cd0af3bc8c41095e0e0409f9173b122f662e8e69d\",\n  \"cmd\": \"python tests/test_billing.py\",\n  \"red_exit_code\": 1,\n  \"history_count\": 1,\n  \"constructive_witness\": {\n    \"type\": \"spec-collision\",\n    \"conflict_a\": \"D:/Users/francisco.grandon/AppData/Local/Temp/ag_ops_lab/docs/rfc_104.md:3\",\n    \"conflict_b\": \"D:/Users/francisco.grandon/AppData/Local/Temp/ag_ops_lab/src/billing.py:2\"\n  },\n  \"receipt_file\": \"D:\\\\Users\\\\francisco.grandon\\\\AppData\\\\Local\\\\Temp\\\\ag_ops_lab\\\\tests\\\\.ag_state\\\\merkle_guard_c4d80759f832.json\"\n}\n```\n\nAsking a Transformer in Markdown to respect test integrity invites creative lawyering; handing it `exit 2` and a SHA-256 receipt leaves nothing to negotiate.\n\n`reflex`), `--context-map`, and Pre-Flight Graphviz `.dot`\nIn `ast-lint`, `_AntigravityVisitor` replaced prefix guessing with full `_dotted_name` AST resolution across two `ast.walk` passes. Calling `tkinter.ttk.Button(root, text='Sync', bg='red')` is now caught reliably even without an `as ttk` alias, while `def ttk_helper(bg='blue'):` and `tk.Button(root, bg='green')` pass untouched. `AUTH001` only runs when `--author` is explicitly passed.\n\nTo guard Python path invariants (`I_path(π)`), v2.1.0 added two rules targeting classic *Cathedral on Quicksand* maneuvers:\n\n`PATH001`: flags broad `except:` or `except Exception:` / `BaseException:` handlers whose body is solely `pass` or `...`.` PATH002`: flags `unittest.mock` or `mock` imports anywhere outside test paths (`test_*.py`, `*_test.py`, `conftest.py`, or `test/`, `tests/`, `testing/` directories).\nEvery violation also carries **Vector 4** from Schwartz's 4-vector scar anatomy: a `\"reflex\"` field containing the concrete orthogonal fix so the model doesn't stall in an apology loop:\n\n```\npython ag_ops.py ast-lint src/invoice_ui.py --json\n{\n  \"ok\": false,\n  \"elapsed_ms\": 0.4,\n  \"files_checked\": 1,\n  \"violations_count\": 3,\n  \"violations\": [\n    {\n      \"file\": \"D:\\\\Users\\\\francisco.grandon\\\\AppData\\\\Local\\\\Temp\\\\ag_ops_lab\\\\src\\\\invoice_ui.py\",\n      \"line\": 2,\n      \"rule\": \"PATH002\",\n      \"message\": \"Mocking primitives from 'unittest.mock' imported in production module.\",\n      \"reflex\": \"Remove unittest.mock imports from production modules; pass dependencies via constructor or callable parameters.\"\n    },\n    {\n      \"file\": \"D:\\\\Users\\\\francisco.grandon\\\\AppData\\\\Local\\\\Temp\\\\ag_ops_lab\\\\src\\\\invoice_ui.py\",\n      \"line\": 9,\n      \"rule\": \"TK002\",\n      \"message\": \"Direct 'bg=' argument forbidden on ttk.Button. Use ttk.Style() or tk equivalent.\",\n      \"reflex\": \"Configure 'bg' via ttk.Style().configure('<Style>.TButton', ...) or switch to tk.Button.\"\n    },\n    {\n      \"file\": \"D:\\\\Users\\\\francisco.grandon\\\\AppData\\\\Local\\\\Temp\\\\ag_ops_lab\\\\src\\\\invoice_ui.py\",\n      \"line\": 12,\n      \"rule\": \"PATH001\",\n      \"message\": \"Silent broad exception swallowing ('except: pass') hides broken path invariants.\",\n      \"reflex\": \"Catch a specific exception type and log/handle the failure context, or re-raise the error.\"\n    }\n  ]\n}\n```\n\nStatic scars also age poorly when dependencies change, a failure mode Schwartz calls *Scar Ossification* (autoimmune drift). Passing `--context-map context_map.json` pins any rule ID to an anchor file's SHA-256 signature (for example, `pyproject.toml`). If a library upgrade alters `pyproject.toml`, `_apply_context_signatures` automatically downgrades that rule from `ENFORCED` (`exit 1`) to `PROBATIONARY_REVERIFY` (`\"autoimmune_drift\": true`, `exit 0`), warning the orchestrator to re-verify the rule rather than blocking valid builds:\n\n```\n{\n  \"ok\": true,\n  \"elapsed_ms\": 1.041,\n  \"files_checked\": 1,\n  \"violations_count\": 0,\n  \"violations\": [],\n  \"probationary_count\": 1,\n  \"probationary_warnings\": [\n    {\n      \"file\": \"D:\\\\Users\\\\francisco.grandon\\\\AppData\\\\Local\\\\Temp\\\\ag_ops_lab\\\\src\\\\sync_button.py\",\n      \"line\": 3,\n      \"rule\": \"TK002\",\n      \"message\": \"Direct 'bg=' argument forbidden on ttk.Button. Use ttk.Style() or tk equivalent.\",\n      \"reflex\": \"Configure 'bg' via ttk.Style().configure('<Style>.TButton', ...) or switch to tk.Button.\",\n      \"severity\": \"PROBATIONARY_REVERIFY\",\n      \"autoimmune_drift\": true\n    }\n  ]\n}\n```\n\nFinally, `ast-lint` now validates pre-flight **Graphviz `.dot` state machines** (`_lint_dot_state_machine`, or `--include-dot` on directories) before an agent writes async code. Four topological rules run in sub-millisecond time: `DOT001` (deadlock freedom: non-terminal states need `out-degree >= 1`), `DOT002` (reachability: non-initial states need `in-degree >= 1`), `SCAR-PROC-152` (async states marked via `parallelogram`, `async=true`, or `Awaiting*`/` Fetching*`/` Stream*`/` Loading*` prefixes must have `out-degree >= 2` including an explicit `error|cancel|dispose|fail|abort|timeout|close` exit edge), and `SCAR-ASYNC-01` (every non-teardown resolution edge from an async state must declare a `[guard: ...]` predicate against late-arrival zombie callbacks):\n\n``` php\ndigraph SyncStream {\n  Idle -> Fetching\n  Fetching -> Active [label=\"onPayload\"]\n  Active -> Closed [label=\"onClose\"]\n}\npython ag_ops.py ast-lint sync_stream.dot --json\n{\n  \"ok\": false,\n  \"elapsed_ms\": 0.436,\n  \"files_checked\": 1,\n  \"violations_count\": 2,\n  \"violations\": [\n    {\n      \"file\": \"D:\\\\Users\\\\francisco.grandon\\\\AppData\\\\Local\\\\Temp\\\\ag_ops_lab\\\\sync_stream.dot\",\n      \"line\": 1,\n      \"rule\": \"SCAR-PROC-152\",\n      \"message\": \"Async state 'Fetching' lacks explicit teardown/cancel/error exit edge (out-degree=1).\",\n      \"reflex\": \"Add an explicit transition from 'Fetching' to a Disposed, Cancelled, or Error state.\"\n    },\n    {\n      \"file\": \"D:\\\\Users\\\\francisco.grandon\\\\AppData\\\\Local\\\\Temp\\\\ag_ops_lab\\\\sync_stream.dot\",\n      \"line\": 1,\n      \"rule\": \"SCAR-ASYNC-01\",\n      \"message\": \"Late-arrival zombie race on 'Fetching -> Active': missing '[guard: ...]' predicate on edge label.\",\n      \"reflex\": \"Add a cancellation/epoch guard on the resolution edge, e.g. Fetching -> Active [label=\\\"onResolve [guard: !isClosed && reqId == activeReqId]\\\"].\"\n    }\n  ]\n}\n```\n\nFixing `distill` (`--cmd`, `--max-lines`, `--log-dir`, `--timeout` exiting with `124` after killing the isolated process group) meant anchoring `_ERROR_LINE_RE` strictly to line boundaries: `^\\s*E\\s{3}` for pytest assertion lines, paired with `^\\s*error(?:\\[[A-Z0-9]+\\])?:` for compiler diagnostics like Rust's `error[E0382]:`. `\" Step 1 DONE    everything fine\"` now scrolls past in peace, while the real `E   AssertionError` anchors the critical delta.\n\nIn `state` (`write` and `status`, requiring `--dir` alongside `--step`, `--name`, `--status`, `--summary`, `--artifacts`, `--replace`, and `--strict`), reusing an existing step number with a different slug now aborts with `STEP_CONFLICT` (`exit 1`) unless `--replace` is explicit. Artifact tracking follows a clean three-stage lifecycle:\n\n`\"NOT_FOUND\"`. Calling `state status --dir .ag_state/runlevels --strict` flags `ARTIFACT_NEVER_EXISTED` (`exit 2`), accurately distinguishing \"not built yet\" from \"deleted.\"` state status` sees that file materialize on disk, it computes `_sha256_file`, persists the digest atomically into `NN_<slug>.json`, and returns `has_drift: false` (`exit 0`).` DRIFT_MODIFIED` or `FILE_DELETED` (`--strict`).` regret-mine`: Stage-1 Deterministic Rapid-Regret Miner\nInspired by Target 90 in *Synthetic Scars*, `regret-mine` (`--repo`, `--window-min 60`, `--limit 100`, `--state-dir`) audits recent Git commits to find where an agent (or human) wrote a function and had to rush back into the exact same AST node minutes later to fix what broke.\n\nFor consecutive commits touching the same `.py` production file inside `--window-min`, `_extract_ast_function_hashes` parses each revision and hashes `ast.dump(node, include_attributes=False)` with SHA-256. Formatting tweaks, blank lines, and edits outside the function body produce identical hashes and are ignored. When the same function's normalized AST mutates twice inside the window, `regret-mine` checks four deterministic asymmetry signals to separate genuine rapid regret from healthy `Green -> Refactor` TDD:\n\n`FIX_VERB_INTENT`: corrective keywords in the follow-up commit subject matching `_FIX_VERB_RE` (`fix`, `fixes`, `fixed`, `bug`, `hotfix`, `revert`, `oops`, `regret`, `patch`, `broken`, `typo`).` TEST_ASSERTION_DELTA`: `len(newer[\"test_files\"]) > 0`, catching production rewrites accompanied by new or updated test assertions.`GUARD_COLLISION_RECEIPT`: Unix timestamp correlation (` older[\"ts\"] <= ev_ts <= newer[\"ts\"]`) against mid-flight collisions or renegotiations recorded inside `.ag_state/merkle_guard_*.json`.` LINE_REVERSAL_OVERLAP`: lines added in the earlier commit and deleted in the follow-up (`added_k & deleted_k1`), gated by refactor and control-guard checks.\n\n```\npython ag_ops.py regret-mine --repo . --window-min 60 --json\n{\n  \"ok\": true,\n  \"window_minutes\": 60,\n  \"commits_scanned\": 3,\n  \"tdd_refactors_filtered\": 1,\n  \"regret_pairs_count\": 1,\n  \"regret_pairs\": [\n    {\n      \"file\": \"pricing.py\",\n      \"overlapping_functions\": [\n        \"compute_fee\"\n      ],\n      \"delta_minutes\": 12.0,\n      \"initial_commit\": \"24864454\",\n      \"regret_commit\": \"456955a2\",\n      \"regret_subject\": \"fix: handle None amount in compute_fee\",\n      \"asymmetry_signals\": [\n        \"FIX_VERB_INTENT\",\n        \"TEST_ASSERTION_DELTA\",\n        \"GUARD_COLLISION_RECEIPT\"\n      ]\n    }\n  ]\n}\n```\n\nIt would be dishonest to write about deterministic guardrails as if `v2.1.0` compiled cleanly on the first try. Building the scar engine produced three fresh scars of its own before the release could ship.\n\nWhen I wired up `test_06_regret_mine` in `tests/test_ag_ops.py`, I fed it a textbook three-commit TDD sequence on `calc.py`:\n\n`C1` (`feat: add calc`): `res = x + 1; return res`\n`C2` (`refactor: simplify calc`): inlined to `return x + 1`\n`C3` (`fix: edge case None in calc`): added `if x is None: return 0`\nI expected `tdd_refactors_filtered: 1` and `regret_pairs_count: 1` (`C2 -> C3`). Instead, my `v2.0` candidate failed its own release gate (`5/6` suites passing) with `tdd_refactors_filtered: 0` and `regret_pairs_count: 3`. There is a special kind of poetic justice in writing a Rapid-Regret Miner whose maiden run blocks its own release because it cannot tell disciplined TDD apart from panic.\n\nTwo bugs had compounded:\n\n`C1 -> C2` with `paired_files` set, the nested commit loop paired `C1 -> C3`), counting one bug fix twice.\nIn `v2.1.0`, `regret-mine` tracks `paired_files` per `newer` commit so each file only compares adjacent `C_k -> C_{k+1}` transitions, skips line-reversal checks when `_REFACTOR_VERB_RE` matches without fix verbs (`is_pure_refactor`), and requires `inverted_lines` to either touch a control-flow guard (`_CONTROL_GUARD_RE`: `if|elif|else|raise|assert|except`) or accompany another corroborating signal. That brought the suite to `6/6` green.\n\n`\\s*` that ate semicolon-free Graphviz edges\nGraphviz DOT allows edges without trailing semicolons:\n\n``` php\ndigraph G {\n  Idle -> Fetching\n  Fetching -> Done [label=\"onSuccess\"]\n}\n```\n\nIn my first draft of `_lint_dot_state_machine`, `edge_re` ended with `\\s*(?:\\[(.*?)\\])?` to capture optional edge attributes. Because `Idle -> Fetching` had no `[...]` block, `\\s*` happily ate the newline at the end of line 2 plus the leading indentation on line 3, treating line breaks between state transitions as optional whitespace and leaving `SCAR-PROC-152` and `SCAR-ASYNC-01` blind whenever semicolons were omitted. Restricting every horizontal whitespace token in `edge_re` and `node_attr_re` from `\\s*` to `[ \\t]*` under `re.M` fixed it immediately.\n\n`NodeVisitor` vs. `ast.walk`, OneDrive, and Windows cold start)\nInitially, `_AntigravityVisitor` subclassed `ast.NodeVisitor` in a single top-down pass. That failed whenever an import lived near the bottom of a file (below a helper function calling it) and paid dynamic `getattr` dispatch on every AST node. Replacing `NodeVisitor` with two flat `ast.walk(tree)` passes (imports first, calls and exception handlers second) fixed the late-import bug cleanly.\n\nThen I ran real timing benchmarks (median of 5 runs on Python 3.12 on Windows) to see how honest our `<15 ms` static-check label actually was:\n\n| Target File | Lines | In-Process ( `elapsed_ms` ) | Wall-Clock Subprocess | Notes | \n|---|---|---|---|---|\n| `tests/test_ag_ops.py` | `333` | **`6.54 ms`** | `151.1 ms` | Typical module size; well inside `<15 ms` | \n| `ag_ops.py` (local SSD) | `1,783` | **`42.6 ms`** | `~182 ms` | Dominated by `ast.parse()` , not tree walking | \n| `ag_ops.py` (OneDrive path) | `1,783` | **`52.9 ms`** | `~193 ms` | Cloud-synced filesystem stat/read overhead | \n\nTwo lessons jumped out of that table. First, once a single Python file approaches 1,800 lines, CPython's `ast.parse()` dominates the in-process budget; micro-optimizing the visitor barely moves the needle. Second, spawning `python.exe` on Windows via `CreateProcessW` adds roughly `140 ms` of fixed interpreter startup outside `time.perf_counter()`. In-process, a 333-line file lints in `6.54 ms`; end-to-end from an agent's shell tool, every invocation pays `~150–185 ms`. That is still fast enough to run on every turn without friction, and worth reporting without hiding behind `perf_counter()` alone.\n\n`dbader/schedule`\nSynthetic unit tests are friendly terrain, so I pointed `regret-mine` at an external open-source repository, [`dbader/schedule`](https://github.com/dbader/schedule) (` 398` commits), across three time windows:\n\n`--window-min 60` (1 hour):`19` TDD refactors filtered, `11` regret pairs flagged.`--window-min 1440` (24 hours):`25` TDD refactors filtered, `24` regret pairs flagged.`--window-min 10080` (7 days):`35` TDD refactors filtered, `37` regret pairs flagged.\nInspection of the `11` one-hour pairs showed that the miner catches real rapid-regret defects cleanly, including commits like *\"Fix a bug where the at(':SS') syntax wasn't working\"* and *\"Missed == to !=\"*.\n\nIt also exposed two remaining sources of false-positive noise in `v2.1.0`:\n\n`FunctionDef.body[0]` as an `ast.Expr(ast.Constant(...))`. Fixing a typo inside a docstring changes `ast.dump(node, include_attributes=False)` `\"fix typo\"` in the commit subject, firing `_extract_ast_function_hashes` currently keys functions by `defs[node.name]`. If `Job.__init__` changes in Commit A and `Scheduler.__init__` in the same file gets a bugfix 20 minutes later in Commit B, `defs[\"__init__\"]` collides. Stripping leading docstring constants before hashing and qualifying `ClassName.method_name` are queued for `v2.2`.` ag_ops.py`\nIn Randal's full architecture, Stage 2 hands candidate regret pairs and `transcript.jsonl` slices to an isolated clean-room critic model to compile new 4-vector scars automatically. I deliberately left Stage 2 out of `ag_ops.py`. Mixing API keys, network retries, and LLM prompts into a deterministic guardrail CLI would violate the exact property that makes `ag_ops.py` trustworthy: zero external dependencies, zero network calls, zero token spend. Shrinking `398` commits down to `11` structured JSON candidates in milliseconds is the CLI's job; deciding whether an orchestrator subagent or a human reviews those `11` candidates belongs one layer up.\n\n`.ag_state/` Perimeter\nJust like v1, `ag_ops` v2.1.0 runs in-band under the same OS user account as the Antigravity agent. An adversarial process with unrestricted shell access could still inspect or overwrite `.ag_state/merkle_guard_<hash>.json` directly. In daily engineering work, `ag_ops` targets **autoregressive laziness**: stopping an over-eager model from taking statistical shortcuts like `except: pass` or quietly widening `assert status == 200`. Defending against hostile multi-tenant breakouts is a different threat model; true adversarial custody still requires separating the performer and the verifier across OS UIDs, read-only bind mounts, or CI runners.\n\n`ag_ops` v2.1.0 remains a working lab instrument in standard-library Python, shaped by watching real multi-agent runs in Google Antigravity fail in ways I didn't anticipate and tightening the bolts one bug at a time.\n\nI owe a clear debt of gratitude to **[Randal L. Schwartz](https://dev.to/randalschwartz)** ([@randalschwartz](https://dev.to/randalschwartz)) for the [*Synthetic Scars*](https://dev.to/randalschwartz/series/44156) series, and particularly [*Giving AI Pain*](https://dev.to/gde/giving-ai-pain-the-architecture-of-synthetic-scars-the-rapid-regret-miner-1740), for bringing decades of Unix engineering discipline, formal state-machine rigor, and empirical clarity to a space that badly needed an alternative to prompt superstition. Thank you as well to **[@max_quimby](https://dev.to/max_quimby)** and **howcani**, whose sharp questions on [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) turned a loose prose `--reason` flag into constructive witnesses and pushed this engine onto much firmer ground.\n\nThe full source code (`ag_ops.py` v2.1.0), the `6/6` automated verification suite (`tests/test_ag_ops.py`), and the MIT License are available at **[` github.com/FranciscoGrandon/ag-ops`](https://github.com/FranciscoGrandon/ag-ops)**. If you run `regret-mine` against your own repositories and find an edge case that breaks it, I would genuinely love to hear what broke.", "url": "https://wpnews.pro/news/what-broke-after-i-moved-my-agent-s-rules-into-a-cli-and-how-synthetic-scars-ag", "canonical_source": "https://dev.to/fgrandon/what-broke-after-i-moved-my-agents-rules-into-a-cli-and-how-synthetic-scars-reshaped-agops-v21-g4k", "published_at": "2026-10-31 07:36:57+00:00", "updated_at": "2026-10-31 07:49:33.541075+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops"], "entities": ["ag_ops", "FranciscoGrandon/ag-ops", "Google Antigravity", "Randal L. Schwartz", "max_quimby", "howcani"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/what-broke-after-i-moved-my-agent-s-rules-into-a-cli-and-how-synthetic-scars-ag", "markdown": "https://wpnews.pro/news/what-broke-after-i-moved-my-agent-s-rules-into-a-cli-and-how-synthetic-scars-ag.md", "text": "https://wpnews.pro/news/what-broke-after-i-moved-my-agent-s-rules-into-a-cli-and-how-synthetic-scars-ag.txt", "jsonld": "https://wpnews.pro/news/what-broke-after-i-moved-my-agent-s-rules-into-a-cli-and-how-synthetic-scars-ag.jsonld"}}