verify-repair-loop
A bounded generate → run → revise → re-run cycle whose pass signal is the
executed verdict — the project's real test/quality output, scored against a
numeric threshold — escalated to a judge for qualitative confirmation only
after the numeric gate passes (staged escalation). The loop is the
conversation, not a process: this loop introduces no resident process of its
own and no persistent cross-session state. Use
it to drive a change to green when the verdict source is runtime execution,
not a judge reading a diff.
When to use
- A change must converge against the project's test runner / quality tools
and you want the agent to iterate until checks pass before asking for review.
- Test/quality coverage is the authoritative signal (high coverage, actionable
failures) — not subjective craft.
- You want bounded auto-repair with a hard stop, not an open-ended fix loop.
Do NOT use when:
- The verdict is subjective craft (naming, architecture, style) or coverage
is incomplete → use
do-and-judge in
subagent-orchestration (judge reads the
diff and is sovereign).
- You only need a one-shot multi-judge review of a finished diff → use
/review-changes.
- The verdict must come from a live app (Playwright against running services)
→ deferred (see Scope below); use test/quality verdicts until that trigger
fires.
The Iron Law
THE NUMERIC VERDICT IS NEVER SOVEREIGN.
TESTS DECIDE WHEN TO ESCALATE — A JUDGE DECIDES WHETHER TO APPLY.
A REVISION THAT BREAKS A PREVIOUSLY-GREEN CHECK STOPS THE LOOP,
EVEN IF THE OVERALL SCORE ROSE.
GENERATOR NEVER APPROVES ITS OWN CHANGE.
A loop that optimizes for pass_count ≥ N can game the metric — delete tests,
skip assertions, weaken checks — and still hit the threshold. The numeric gate
only decides when to escalate; a judge with veto confirms the change is real.
Procedure
1. Freeze the contract (spec fingerprint)
Before the first run, capture what "done" means and snapshot it:
- the task's acceptance criteria / requirement,
- the set of checks that define the verdict (test files + quality commands),
- a spec fingerprint = a hash of (requirement text + the check set).
Record the baseline green set — which checks pass before any edit — so
regressions are detectable. Pick the verdict commands per the project's
toolchain-resolver (PHP / JS-TS
/ Python / Go / Rust), not a hardcoded runner.
2. Set the gate
| Knob |
Default |
Meaning |
threshold |
all targeted checks green |
numeric pass bar, absolute (fraction of total), not "± N tests" |
allow_regressions |
false |
a revision breaking a baseline-green check stops the loop |
max_attempts |
3 |
hard cap (test feedback is more actionable than diff critique → one more than do-and-judge's 2; bounded by autonomous-execution N=3) |
plateau_window |
3 |
stop if the last window scores sit within tolerance |
tolerance |
small absolute fraction |
flake jitter that does not count as improvement |
3. The loop (multi-turn — the conversation IS the loop)
Each iteration is agent turns, never executing control-flow code:
Generate / revise — make the smallest change toward the contract.
Run — execute the verdict commands; parse the structured result
(counts green/total, which checks failed). This is one tool call, read in
context.
Score + regression check — compute the numeric score; compare failures
against the baseline green set.
- Regression (a baseline-green check now fails) → STOP, hand back with the
regression named, even if the score rose. No whack-a-mole.
- Plateau (last
plateau_window scores within tolerance) → STOP; surface
suspected flakiness or a stuck point. Do not thrash.
attempts == max_attempts → STOP, hand back the best envelope.
Tamper check (deterministic, loop-local) — before the numeric gate is
allowed to say anything, run the detector over the diff this attempt
produced and over the recorded verdict:
./scripts-run src/scripts/detect_verification_tampering \
--diff <this attempt's diff> --message <this attempt's message> \
--verdict <the runner's recorded verdict>
It is fail-closed inside the loop: outcome violations or
stale_verdict STOPS the loop and hands back with the finding's
file:line, whatever the score says. clean changes nothing — an
ordinary passing attempt is unaffected, because the detector reports only
on shapes the diff introduced.
The check answers exactly the gap the Iron Law names: assertion-removed,
assertion-weakened, verification-skipped (warn), expectation-rewritten,
early-exit-injected, test-file-deleted, subject-mocked-away, and
stale_verdict for a runner that finished before the last edit. A change
that legitimately deletes an obsolete test or relaxes an over-tight
assertion declares it in the attempt's message —
tamper-allow: <detector-id> — <reason> — scoped to one id, never to all.
It is not a CI gate and does not become one here. The tree's measured
lesson is that reach, not content, is the scarce thing; a new always-on
gate is the opposite of that lesson. Promotion owes a measured
false-positive rate first.
Numeric gate — score < threshold and attempts remain → back to step 1
(attempts++). Score ≥ threshold, no regression, tamper check clean →
escalate (step 4 of the stage).
4. Judge escalation (only after the numeric gate passes)
Dispatch the judge as a subagent with fresh context (the judge-* cluster via
subagent-orchestration) that sees only the
diff + the executed results — never the generator's reasoning. This is the real
generator ≠ judge separation; single-agent persona-switching is theater.
- judge
apply → DONE.
- judge
revise → back to step 3.1 (attempts++; still bounded).
- judge
reject → STOP, hand back; the approach must change.
5. Mid-loop invalidation (user-interrupt-priority)
Re-check the spec fingerprint each iteration. If the user changes the
requirement mid-loop (a new instruction in the conversation), the fingerprint
changes → abort and hand back per
user-interrupt-priority. Never keep
iterating against a stale contract.
Scope — what runs, what is deferred
| Verify context |
In scope |
Why |
| Unit / integration tests |
✅ |
fast, deterministic, sandboxed — runtime-free |
| Static quality (lint, type-check, format) |
✅ |
fast, deterministic, no side effects (auto-fixable failures may bypass the revise step) |
| Live-app Playwright / E2E |
⛔ deferred |
needs running services (DB, API) = a runtime; trigger = a UI-observable mission output + Playwright wired into a consumer CI |
State lives in the conversation, optionally mirrored to a re-read working file —
never a daemon or persistent cross-session store (honors
resident-process-governance).
Validation
Before finalizing, confirm:
- The numeric gate escalated to a judge — it did not apply on its own.
- No baseline-green check regressed (or the loop stopped and said so).
- The tamper check ran on the final attempt and returned
clean — or the loop
stopped on its finding. A green verdict recorded over a violations or
stale_verdict outcome is not a green verdict.
- The loop stopped at
max_attempts, a plateau, or a judge apply — never
ran unbounded.
- The judge saw only the diff + results, dispatched in fresh context.
- No unsupervised resident process introduced — and none of the four
governance conditions in
resident-process-governance.md left unmet if a
supervised one is. (Amended 2026-08-27, ADR-249: this read "No daemon /
persistent runtime introduced" when a daemon was prohibited outright. The
check still fires; what it tests moved from existence to governance.)
Output format
verify-repair-loop
Contract: <one-line requirement> · threshold <X> · allow_regressions false
Attempts: <k>/<max> scores: [s1, s2, …] stop: threshold|plateau|cap|regression|reject
Verdict: DONE | DONE_WITH_CONCERNS | BLOCKED
Judge: <judge skill> → apply|revise|reject
Evidence: <green/total> · regressions: none | <named checks>
Required fields (ordered):
- Contract — the frozen requirement + threshold +
allow_regressions.
- Attempts —
k/max, the score history, and the exact stop reason
(threshold / plateau / cap / regression / reject).
- Verdict —
DONE, DONE_WITH_CONCERNS, or BLOCKED.
- Judge — the dispatched judge skill and its
apply / revise /
reject verdict (omit only when the loop stopped before escalation).
- Evidence — final
green/total and any regressed checks by name.
Examples
Good — staged, bounded, judge confirms:
iter1 84/100 (<90) → revise
iter2 93/100 (≥90, no regression) → escalate → judge apply → DONE
Bad — numeric verdict treated as sovereign (rejected by the Iron Law):
iter1 91/100 ≥ threshold → "PASS, done" ✗ no judge escalation; metric may be gamed
iter2 fixes A, breaks B, 92% → "PASS" ✗ regression ignored
Gotcha
- Metric gaming — the agent "passes" by deleting a failing test or
weakening an assertion. The numeric gate still cannot catch this, and that
is why loop step 3.4 exists:
detect_verification_tampering decides the
mechanical shapes deterministically, and the fresh-context judge remains the
safeguard for everything shape cannot decide. If the diff removes checks,
that is a reject, not a pass.
- Phantom verification — the loop records a green verdict it never re-ran,
so the exit code describes a tree that no longer exists. The verdict carries
the runner's own timestamp; one that predates the last edit is
stale_verdict, its own outcome, never a pass.
- Flake mistaken for a plateau — a single non-deterministic test flips
the score and the window-comparison reads it as "no improvement". Re-run
the suspected check before declaring a plateau; surface flakiness instead
of thrashing.
- Score rose, but a regression hid inside it — fixing 6 checks while
breaking 2 still raises the total. Without the regression guard the loop
ships a regression. Always diff failures against the baseline green set,
not just the aggregate score.
- Persona-switch theater — asking the same agent to "now judge what you
wrote" is not separation. Dispatch the judge as a fresh-context subagent
that never saw the generator's reasoning.
- Stale contract — iterating against a requirement the user changed
mid-loop. Re-check the spec fingerprint each turn; a change aborts.
Do NOT
- NEVER let the numeric score apply a change without judge confirmation.
- NEVER record a green verdict over a tamper finding or a stale runner
timestamp — inside the loop the detector is fail-closed.
- NEVER widen a
tamper-allow declaration past the one id it names.
- NEVER continue past a regression because the overall score improved.
- NEVER run the live-app Playwright path here — it is deferred (needs a runtime).
- NEVER judge with the generator's own context — dispatch a fresh-context judge.
- NEVER loop past
max_attempts or a detected plateau.
See also
subagent-orchestration — do-and-judge
(judge-sovereign, diff-read) is the sibling; this skill is test-verdict-gated.
/review-changes — one-shot multi-judge
review; wires this loop as an opt-in step.
playwright-testing,
quality-tools — verdict sources.
src/scripts/detect_verification_tampering.ts — the loop-local tamper and
phantom-verification check bound at step 3.4, with its fixture corpus in
tests/fixtures/tamper-corpus/ and its polarity proven both ways by
--self-test.
autonomous-execution — the N=3 cap.
resident-process-governance — the
runtime-free constraint this loop honors.
- Optional impact pre-step (when a code-graph is present, ADR-124): run
agent-config code-graph affected --since <ref> to scope which symbols a
change touches before choosing verdict sources — cited, not duplicated; the
loop itself is unchanged when no graph exists.
1---2name: verify-repair-loop3description: Use to iterate a change until tests/quality checks pass — bounded run→revise→re-run gated by a numeric threshold, then a judge confirms. Triggers 'iterate to green', 'keep fixing until tests pass'.4---56# verify-repair-loop78> A bounded **generate → run → revise → re-run** cycle whose pass signal is the9> **executed verdict** — the project's real test/quality output, scored against a10> numeric threshold — escalated to a **judge** for qualitative confirmation only11> *after* the numeric gate passes (staged escalation). The loop is the12> conversation, not a process: this loop introduces no resident process of its13> own and no persistent cross-session state. Use14> it to drive a change to green when the verdict source is *runtime execution*,15> not a judge reading a diff.1617## When to use1819* A change must converge against the project's **test runner / quality tools**20 and you want the agent to iterate until checks pass before asking for review.21* Test/quality coverage is the authoritative signal (high coverage, actionable22 failures) — not subjective craft.23* You want bounded auto-repair with a hard stop, not an open-ended fix loop.2425Do NOT use when:2627* The verdict is **subjective craft** (naming, architecture, style) or coverage28 is incomplete → use `do-and-judge` in29 [`subagent-orchestration`](../subagent-orchestration/SKILL.md) (judge reads the30 diff and is sovereign).31* You only need a **one-shot** multi-judge review of a finished diff → use32 [`/review-changes`](../../commands/review/changes.md).33* The verdict must come from a **live app** (Playwright against running services)34 → **deferred** (see Scope below); use test/quality verdicts until that trigger35 fires.3637## The Iron Law3839```40THE NUMERIC VERDICT IS NEVER SOVEREIGN.41TESTS DECIDE WHEN TO ESCALATE — A JUDGE DECIDES WHETHER TO APPLY.42A REVISION THAT BREAKS A PREVIOUSLY-GREEN CHECK STOPS THE LOOP,43EVEN IF THE OVERALL SCORE ROSE.44GENERATOR NEVER APPROVES ITS OWN CHANGE.45```4647A loop that optimizes for `pass_count ≥ N` can game the metric — delete tests,48skip assertions, weaken checks — and still hit the threshold. The numeric gate49only decides **when to escalate**; a judge with veto confirms the change is real.5051## Procedure5253### 1. Freeze the contract (spec fingerprint)5455Before the first run, capture what "done" means and snapshot it:5657* the task's acceptance criteria / requirement,58* the set of checks that define the verdict (test files + quality commands),59* a **spec fingerprint** = a hash of (requirement text + the check set).6061Record the **baseline green set** — which checks pass *before* any edit — so62regressions are detectable. Pick the verdict commands per the project's63[`toolchain-resolver`](../../contexts/execution/toolchain-resolver.md) (PHP / JS-TS64/ Python / Go / Rust), not a hardcoded runner.6566### 2. Set the gate6768| Knob | Default | Meaning |69|---|---|---|70| `threshold` | all targeted checks green | numeric pass bar, **absolute** (fraction of total), not "± N tests" |71| `allow_regressions` | `false` | a revision breaking a baseline-green check stops the loop |72| `max_attempts` | **3** | hard cap (test feedback is more actionable than diff critique → one more than `do-and-judge`'s 2; bounded by [`autonomous-execution`](../../rules/autonomous-execution.md) N=3) |73| `plateau_window` | 3 | stop if the last *window* scores sit within `tolerance` |74| `tolerance` | small absolute fraction | flake jitter that does **not** count as improvement |7576### 3. The loop (multi-turn — the conversation IS the loop)7778Each iteration is agent turns, never executing control-flow code:79801. **Generate / revise** — make the smallest change toward the contract.812. **Run** — execute the verdict commands; parse the **structured** result82 (counts green/total, which checks failed). This is one tool call, read in83 context.843. **Score + regression check** — compute the numeric score; compare failures85 against the baseline green set.86 * **Regression** (a baseline-green check now fails) → STOP, hand back with the87 regression named, even if the score rose. No whack-a-mole.88 * **Plateau** (last `plateau_window` scores within `tolerance`) → STOP; surface89 suspected flakiness or a stuck point. Do not thrash.90 * **`attempts == max_attempts`** → STOP, hand back the best envelope.914. **Tamper check (deterministic, loop-local)** — before the numeric gate is92 allowed to say anything, run the detector over the diff this attempt93 produced and over the recorded verdict:9495 ```bash96 ./scripts-run src/scripts/detect_verification_tampering \97 --diff <this attempt's diff> --message <this attempt's message> \98 --verdict <the runner's recorded verdict>99 ```100101 It is **fail-closed inside the loop**: outcome `violations` or102 `stale_verdict` STOPS the loop and hands back with the finding's103 `file:line`, whatever the score says. `clean` changes nothing — an104 ordinary passing attempt is unaffected, because the detector reports only105 on shapes the diff introduced.106107 The check answers exactly the gap the Iron Law names: `assertion-removed`,108 `assertion-weakened`, `verification-skipped` (warn), `expectation-rewritten`,109 `early-exit-injected`, `test-file-deleted`, `subject-mocked-away`, and110 `stale_verdict` for a runner that finished before the last edit. A change111 that legitimately deletes an obsolete test or relaxes an over-tight112 assertion declares it in the attempt's message —113 `tamper-allow: <detector-id> — <reason>` — scoped to one id, never to all.114115 It is **not** a CI gate and does not become one here. The tree's measured116 lesson is that reach, not content, is the scarce thing; a new always-on117 gate is the opposite of that lesson. Promotion owes a measured118 false-positive rate first.1191205. **Numeric gate** — score `< threshold` and attempts remain → back to step 1121 (attempts++). Score `≥ threshold`, no regression, tamper check clean →122 **escalate** (step 4 of the stage).123124### 4. Judge escalation (only after the numeric gate passes)125126Dispatch the judge as a **subagent with fresh context** (the `judge-*` cluster via127[`subagent-orchestration`](../subagent-orchestration/SKILL.md)) that sees **only the128diff + the executed results** — never the generator's reasoning. This is the real129generator ≠ judge separation; single-agent persona-switching is theater.130131* judge `apply` → DONE.132* judge `revise` → back to step 3.1 (attempts++; still bounded).133* judge `reject` → STOP, hand back; the approach must change.134135### 5. Mid-loop invalidation (user-interrupt-priority)136137Re-check the spec fingerprint each iteration. If the user changes the138requirement mid-loop (a new instruction in the conversation), the fingerprint139changes → **abort and hand back** per140[`user-interrupt-priority`](../../rules/user-interrupt-priority.md). Never keep141iterating against a stale contract.142143## Scope — what runs, what is deferred144145| Verify context | In scope | Why |146|---|---|---|147| Unit / integration tests | ✅ | fast, deterministic, sandboxed — runtime-free |148| Static quality (lint, type-check, format) | ✅ | fast, deterministic, no side effects (auto-fixable failures may bypass the revise step) |149| Live-app Playwright / E2E | ⛔ **deferred** | needs running services (DB, API) = a runtime; trigger = a UI-observable mission output + Playwright wired into a consumer CI |150151State lives in the conversation, optionally mirrored to a re-read working file —152**never** a daemon or persistent cross-session store (honors153[`resident-process-governance`](../../../docs/contracts/resident-process-governance.md)).154155## Validation156157Before finalizing, confirm:1581591. The numeric gate **escalated to a judge** — it did not apply on its own.1602. No baseline-green check regressed (or the loop stopped and said so).1613. The tamper check ran on the final attempt and returned `clean` — or the loop162 stopped on its finding. A green verdict recorded over a `violations` or163 `stale_verdict` outcome is not a green verdict.1644. The loop stopped at `max_attempts`, a plateau, or a judge `apply` — never165 ran unbounded.1665. The judge saw only the diff + results, dispatched in fresh context.1676. No **unsupervised** resident process introduced — and none of the four168 governance conditions in `resident-process-governance.md` left unmet if a169 supervised one is. (Amended 2026-08-27, ADR-249: this read "No daemon /170 persistent runtime introduced" when a daemon was prohibited outright. The171 check still fires; what it tests moved from existence to governance.)172173## Output format174175```176verify-repair-loop177Contract: <one-line requirement> · threshold <X> · allow_regressions false178Attempts: <k>/<max> scores: [s1, s2, …] stop: threshold|plateau|cap|regression|reject179Verdict: DONE | DONE_WITH_CONCERNS | BLOCKED180Judge: <judge skill> → apply|revise|reject181Evidence: <green/total> · regressions: none | <named checks>182```183184Required fields (ordered):1851861. **Contract** — the frozen requirement + threshold + `allow_regressions`.1872. **Attempts** — `k/max`, the score history, and the exact stop reason188 (`threshold` / `plateau` / `cap` / `regression` / `reject`).1893. **Verdict** — `DONE`, `DONE_WITH_CONCERNS`, or `BLOCKED`.1904. **Judge** — the dispatched judge skill and its `apply` / `revise` /191 `reject` verdict (omit only when the loop stopped before escalation).1925. **Evidence** — final `green/total` and any regressed checks by name.193194## Examples195196Good — staged, bounded, judge confirms:197```198iter1 84/100 (<90) → revise199iter2 93/100 (≥90, no regression) → escalate → judge apply → DONE200```201202Bad — numeric verdict treated as sovereign (rejected by the Iron Law):203```204iter1 91/100 ≥ threshold → "PASS, done" ✗ no judge escalation; metric may be gamed205iter2 fixes A, breaks B, 92% → "PASS" ✗ regression ignored206```207208## Gotcha209210* **Metric gaming** — the agent "passes" by deleting a failing test or211 weakening an assertion. The *numeric* gate still cannot catch this, and that212 is why loop step 3.4 exists: `detect_verification_tampering` decides the213 mechanical shapes deterministically, and the fresh-context judge remains the214 safeguard for everything shape cannot decide. If the diff *removes* checks,215 that is a `reject`, not a `pass`.216* **Phantom verification** — the loop records a green verdict it never re-ran,217 so the exit code describes a tree that no longer exists. The verdict carries218 the runner's own timestamp; one that predates the last edit is219 `stale_verdict`, its own outcome, never a pass.220* **Flake mistaken for a plateau** — a single non-deterministic test flips221 the score and the window-comparison reads it as "no improvement". Re-run222 the suspected check before declaring a plateau; surface flakiness instead223 of thrashing.224* **Score rose, but a regression hid inside it** — fixing 6 checks while225 breaking 2 still raises the total. Without the regression guard the loop226 ships a regression. Always diff failures against the baseline green set,227 not just the aggregate score.228* **Persona-switch theater** — asking the same agent to "now judge what you229 wrote" is not separation. Dispatch the judge as a fresh-context subagent230 that never saw the generator's reasoning.231* **Stale contract** — iterating against a requirement the user changed232 mid-loop. Re-check the spec fingerprint each turn; a change aborts.233234## Do NOT235236* NEVER let the numeric score apply a change without judge confirmation.237* NEVER record a green verdict over a tamper finding or a stale runner238 timestamp — inside the loop the detector is fail-closed.239* NEVER widen a `tamper-allow` declaration past the one id it names.240* NEVER continue past a regression because the overall score improved.241* NEVER run the live-app Playwright path here — it is deferred (needs a runtime).242* NEVER judge with the generator's own context — dispatch a fresh-context judge.243* NEVER loop past `max_attempts` or a detected plateau.244245## See also246247* [`subagent-orchestration`](../subagent-orchestration/SKILL.md) — `do-and-judge`248 (judge-sovereign, diff-read) is the sibling; this skill is test-verdict-gated.249* [`/review-changes`](../../commands/review/changes.md) — one-shot multi-judge250 review; wires this loop as an opt-in step.251* [`playwright-testing`](../playwright-testing/SKILL.md),252 [`quality-tools`](../quality-tools/SKILL.md) — verdict sources.253* `src/scripts/detect_verification_tampering.ts` — the loop-local tamper and254 phantom-verification check bound at step 3.4, with its fixture corpus in255 `tests/fixtures/tamper-corpus/` and its polarity proven both ways by256 `--self-test`.257* [`autonomous-execution`](../../rules/autonomous-execution.md) — the N=3 cap.258* [`resident-process-governance`](../../../docs/contracts/resident-process-governance.md) — the259 runtime-free constraint this loop honors.260* Optional impact pre-step (when a code-graph is present, ADR-124): run261 `agent-config code-graph affected --since <ref>` to scope which symbols a262 change touches before choosing verdict sources — cited, not duplicated; the263 loop itself is unchanged when no graph exists.