pi-gauntlet 8.1.6 → 8.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v8.1.8 - 2026-11-04
4
+
5
+ - Gatekeep-pr's pi fix wave dispatches file findings through `loop-implementer` companion rows and admits their result commits with selective evidence. Row allowances, consecutive validation misses, and one-shot model escalation bound retries within the wave cap; the verify-session dispatch and fix-round guards also cover `loop-implementer`. Both harnesses share worktree and scope context; Claude Code keeps drafted-payload helpers, and sync helpers remain on `implementer`.
6
+
7
+ - `eval/run.mjs --root <path>` runs a target that lives in another checkout (`<path>/eval/<target>/`, skill files and merge-base resolved there), and `runTarget` reads the judge prompt from the runner's own `eval/judge.md` (`judgePath` parameter) instead of `<root>/eval/judge.md`. Lets a sibling repo such as pi-quiver keep its eval targets in its own tree while reusing this harness.
8
+
9
+ ## v8.1.7 - 2026-11-02
10
+
11
+ - The loop engine escalates a row to `escalationLoop.implModel` once its attempts reach `min(loopEngine.escalateAfter, effective allowance - 1)` instead of only on the last allowed attempt; `piGauntlet.loopEngine.escalateAfter` defaults to 3 (floor 1, non-integer values warn and fall back), so the default configuration still escalates attempt 4 while an allowance of 10 runs attempt 4 on `implModel` and attempts 5-10 on the worker. The threshold is held in driver memory from Entry or restore and never written to the ledger. `loop-implementer` reads its target row's `## Decisions` lines oldest first and honors failed-line guidance, with the dispatch payload unchanged. (#119)
12
+
3
13
  ## v8.1.6 - 2026-11-01
4
14
 
5
15
  ### Changed
package/README.md CHANGED
@@ -39,7 +39,7 @@ Concretely, one change through the gauntlet:
39
39
  1. Opt in with `/skill:brainstorming` and describe the change. The command is the reliable discovery-and-entry path; explicit prose requesting brainstorming/the gauntlet also supplies consent when the skill is already loaded or an available instruction or route identifies it. An ordinary implementation request or an agent recommendation is not consent. A leading run of GitHub references (`#N`, `owner/repo#N`, or a `/pull/N` or `/issues/N` URL), separated by whitespace or commas, resolves each reference as a PR first: related open, same-repository, non-fork PRs on one base form one rework subject, including drafts. Admission runs in listed order and relatedness is checked before provisioning; the first retained PR is home, whose existing branch rebases onto the fetched PR base by default, then member commits replay onto it in listed order. The first intent word `merge-forward` opts out and keeps the merge path. An unresolved conflict after its scoped helper aborts that step and continues to the spec with the repair as Task 1. A single non-PR reference follows the ticket path. The first non-reference token starts intent text; mid-sentence references and other ticket/free-text subjects keep their existing paths. The combined code, PR-carried specs, claims, and review threads are the brainstorm input, with each PR's provenance retained; only home's bound ticket supplies acceptance criteria, while member tickets supply context. A single PR retains the same rework path without member replays or a close offer. The combined state is judged against constraints from the fetched current base HEAD, not the fork point or the PRs' proposed policy edits; see the [PR-subject contract](skills/brainstorming/reference/pr-subject.md) and [PR-head worktree owner](skills/using-git-worktrees/reference/pr-head.md). **`brainstorming`** sets up an isolated worktree (on the existing branch for PR rework), explores the codebase, and turns the subject into a written spec - when its recon finds a cited case that the ask is the wrong change (misdiagnosed, wrong-sized, already solved, a band-aid, or cutting across a sound pattern - including a repo convention that is itself debt) and can name the alternative, question one is a framing question (`as framed`, `pivot: <alternative>`, or `smaller or none`); otherwise it opens with one sentence naming what it checked, and every approach it proposes ends with a `Pattern:` line naming what it follows or cuts across; when the run starts from a ticket, the spec carries the ticket's acceptance criteria verbatim, each with a disposition (`in-scope`, `deviates:`, `deferred:`, `venue:`, `elsewhere:` for a row another repo's part delivers), scope cuts come only from your answer to a cut question and are listed at the gate, the conformance gate checks the in-scope ones, and `/skill:check-delivery` verifies `venue:` rows before the prod deploy. A multi-model critique runs on it automatically. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; candidates come from brainstorming's scout recon, never a mechanical sweep). **You read and approve the spec - human gate 1.** Options 1/2/4/5 approve and pick an engine; option 6 approves the content only: it writes `**Engine:** deferred` under `**Amend-grant:** none`, pushes the branch, opens a draft PR carrying just the spec, prints the URL, and ends the flow - no plan, no loop, no handoff brief. For a coordinated spec, it first pushes every companion branch to origin, then opens one home draft PR; it creates no companion PRs. A later `brainstorming #N` reconstructs the recorded companions from their origin branches before re-presenting the implementation gate; the [publication and re-gate owner](skills/brainstorming/reference/spec-pr.md) defines all-member preflight, recovery, and cleanup. The branch stays; a gauntlet-owned worktree is removed, a host-owned checkout is reported and left. Option 6 is withheld on a PR subject (`Withheld: draft PR - #N already exists`) and when the branch's diff against the default branch is not spec-only. A later `brainstorming #N` on that PR classifies it from the diff alone: one added spec carrying the marker plus banner-only predecessor edits re-presents the gate with 1/2/4/5 (3 only when the spec commit carries a council audit) and skips gather, questionary, council, and scout; a spec-only diff without the marker prints `spec-only PR without the gate marker - running lint and council` and takes the normal brainstorm path with the carried spec as the draft; any other diff takes the rework path. Approval at that re-gate writes `**Engine:**`, `**Amend-grant:**`, and `**PR:** <owner/repo#N>` as a new commit on the PR branch and chains into planning or the loop as usual. Before approval, PR rework's sole write/run exception outside the spec directories permits only the base sync (rebase or merge), each member replay or merge, their conflict helpers' edits to the conflicted files, and their scoped conflict tests; other subjects have no implementation code yet.
40
40
  2. By default, **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files. Experimental approval options 4/5 instead select **`loop-implementation`**: the plan phase is skipped and a stamped acceptance-criteria ledger replaces the task plan. Option 4 retains amendment review; option 5 grants auto-apply amends, with redraws still stopping. See [Loop engine operations](doc/configuration.md#loop-engine-operations).
41
41
  3. **`subagent-driven-development`** executes the plan in waves, with task implementers followed by one integrated `wave-reviewer` after the scoped test gate passes. The reviewer checks anchored spec clauses and code quality together; repairs stay on the current tree and pass the same gate before re-review. TDD-locked: red, green, refactor. Independent implementation, review, and council batches remain parallel, but their dispatches are foreground: the orchestrator waits for terminal results before accepting work or advancing a phase.
42
- The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. Each row starts with the shared allowance (default 4); approval replies such as `4; allowance 6` override it for one run. Under either loop approval option, the last allowed attempt escalates once per row on `escalationLoop.implModel` (the main-loop model when unset); one exit-verification repair per flow uses the same model. Exhaustion and stagnation stop for a human decision: `retry <n> | <hint>` adds one attempt to that row, while `rows +N` raises every row's allowance. Attempts never reset and there is no total iteration cap. Ledger refresh completes implement when every row is closed; verify step 1 runs the gate set: deduplicated (owning checkout, command) pairs from done rows' recorded checks, with ownership from the ledger. Gate output goes to logs; the driver keeps exit codes and paths, not test output. CI on push owns the full run.
42
+ The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. Each row starts with the shared allowance (default 4); approval replies such as `4; allowance 6` override it for one run. Under either loop approval option, attempt `escalateAfter + 1` (default 4), clamped to the last allowed attempt, escalates once per row on `escalationLoop.implModel` (the main-loop model when unset); later attempts use the ordinary worker model, and one exit-verification repair per flow uses the same escalation model. Exhaustion and stagnation stop for a human decision: `retry <n> | <hint>` adds one attempt to that row, while `rows +N` raises every row's allowance. Attempts never reset and there is no total iteration cap. Ledger refresh completes implement when every row is closed; verify step 1 runs the gate set: deduplicated (owning checkout, command) pairs from done rows' recorded checks, with ownership from the ledger. Gate output goes to logs; the driver keeps exit codes and paths, not test output. CI on push owns the full run.
43
43
 
44
44
  4. **verify**: the parent first runs the plan's full verification command set for waves, or the ledger gate set for loop; only a passing result permits whole-diff code review, then the **conformance gate**. A review fix invalidates that result, so the parent reruns the same set before the next review or conformance gate. Loop conformance repairs and finish also use the gate set, never the ledger's full `verification:` command. When the overrides file declares a happy path for the touched paths (see [Project-specific overrides](#project-specific-overrides)), the parent runs it once here, between code review and the conformance gate, and the reviewer reads its transcript as runtime evidence - a hang or a failed request shows up in the audit with the transcript quoted - as a gap when an origin clause ties to it, otherwise as a `happy-path: failed - unattributable` line - never as a silent green. The conformance subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. Unauthorized rows - shipped surface no human input asked for, whether it crept in or was laundered through the spec - follow their recommendation like every other row: a contained removal auto-runs, anything another requirement leans on is deferred with a plain-language "I'd cut it / I'd keep it" recommendation. Inside a brainstorming-entered flow this gate is machine-blocked from being skipped. Compatible executable recommendations auto-run through an isolated fix-and-re-audit loop with no prompt; anything still open surfaces as a numbered decision brief - one item per decision, the problem in plain words first, lettered options each with a one-line consequence, and `Recommended: <letter> - <reason>` or `No recommendation - pick one`. Reply with one line per item (`1. B`), or bare `1` to take every recommendation (no-recommendation items are re-asked by number); a current `CONFORMS` / no-concerns result goes straight to the branch options with no extra conformance sign-off.
45
45
  5. **`finishing-a-development-branch`**: PR, draft PR, squash, keep, or discard; PR rework's row 1 offers a lease push of the rebased home branch to the existing PR (`Base:` records in `merge:` form push non-force). A spec whose header carries `**PR:**` (approved at a spec-PR re-gate) and no `## Rework record` replaces the two create rows with `Push to PR #N and mark it ready for review` and `Push to PR #N, keep it a draft` (`Push to PR #N (already ready for review)` when the PR is already ready): both push without force after strip-and-seal, the first then runs `gh pr ready`, neither rewrites the PR body, and both are withheld when origin's branch head is no longer an ancestor of local HEAD. For coordinated specs, these [header-linked rows 1/2](skills/finishing-a-development-branch/reference/spec-pr.md#push-rows-12) deliver companions in recorded order before one home seal carrying every companion PR URL, then push home and apply its readiness choice. Row 1 makes companion drafts ready too; row 2 creates drafts but never converts an already-ready PR back to draft. Companion URLs appear in the shipped summary, companions first and home last, not in the unchanged home PR body; keep that summary for cross-repo review because successful home delivery removes receipts. A spec still carrying `**Engine:** deferred` stops finishing (and `gauntlet-resume`) with `spec approved for handoff only - run brainstorming #N` before any phase change, verification, or push. The PR-rework push appends a `## Rework` block naming absorbed PRs while retaining the original description bytes; the header-linked `**PR:**` push leaves the body untouched. Only after a successful rework push and home-description edit does one gated confirmation show the exact close comments and eligible member targets, linking home; declining or any other finish option leaves members open. Closing never deletes member branches, and finish posts no review-thread replies. Its `Before:` handle includes the full portable pre-rework SHA and the local `refs/gauntlet/pre-rework/pr-<N>` ref in the rework checkout. The [finishing owner](skills/finishing-a-development-branch/SKILL.md#pr-subject-row-1-push-to-the-pr-branch) defines the landing and body-update contract. Once a PR exists, run `/skill:gatekeep-pr <pr>` to verify it before merging - it judges the diff against the spec at the PR's head (the acceptance-criteria dispositions and Design you approved; the PR description when no spec applies) - rows the spec defers or deviates are settled, every other row needs its mechanism (code, tests, docs) - and against the rubric; whatever can be seen only after merge is left to step 6. **Human gate 2** - the only other decision you make.
@@ -617,6 +617,7 @@ The conformance gate's model, the spec council's roster, and the phase-tracker's
617
617
  | `closureReview.maxFixRounds` | phase-tracker | `5` | fix rounds before escalation - [detail](./doc/configuration.md#conformance-gate-model) |
618
618
  | `escalationLoop.implModel` | phase-tracker | unset (main loop) | execution escalation under either loop option; review escalation remains separate - [detail](./doc/configuration.md#escalation-loop) |
619
619
  | `loopEngine.maxIterationsPerRow` | phase-tracker | `4` (floor `1`) | fresh-ledger per-row allowance; run override on approval 4/5 - [detail](./doc/configuration.md#setting-and-command-resolution) |
620
+ | `loopEngine.escalateAfter` | phase-tracker | `3` (floor `1`) | pre-dispatch threshold; escalated attempt index is value + 1, clamped to the last allowed attempt - [detail](./doc/configuration.md#setting-and-command-resolution) |
620
621
  | `phaseModels.plan` / `.implement` / `.verify` / `.ship` | phase-tracker | unset (no switch) | `provider/id[:level]` applied persistently at that phase's start - [detail](./doc/configuration.md#phase-models) |
621
622
  | `flowGuards.enforce` | phase-tracker | `true` | phase gates on/off - [detail](./doc/configuration.md#phase-tracker) |
622
623
  | `flowGuards.specDirs` | phase-tracker, plan-tracker; telemetry and spec-index consumers | `["doc/specs", "docs/specs"]` | where specs and sibling plans live - [detail](./doc/configuration.md#phase-tracker) |
@@ -28,7 +28,7 @@ Read additive inputs `Leg:`, `Rules:`, mapped original AC and, for companions, e
28
28
  ## Procedure
29
29
 
30
30
  1. Read the given spec in full before reading the ledger or code. Read the given ledger next. Save the exact `pending:` tuple and bumped `iterations:` count before editing.
31
- 2. Close the `target` row first. Use `previous decision` and `hint` to avoid repeating work and to try a different approach; keep the target unchanged.
31
+ 2. Close the `target` row first. Read every `row <n> ...` line for the target row from `## Decisions`, oldest first, before choosing an approach. Treat them as ordered guidance: avoid what a `failed` line's third slot says not to redo, follow what it says to try differently, and never repeat an approach a prior line of this row rules out. Exclude other rows' lines from guidance for this target. Read `retry` and `escalated` lines in the same pass: use the retry hint as guidance; treat the escalation marker as historical evidence that escalation is spent, and never infer the current attempt's model from it. Keep `previous decision` as the latest admitted target line and the tie-break for most recent. Use `previous decision` and `hint` to avoid repeating work and to try a different approach; keep the target unchanged.
32
32
  3. Run `git diff --stat <base>`. Open full diff hunks only for files the row being worked on touches, using `git diff <base> -- <row files>`; read the relevant code before editing it.
33
33
  4. Implement the target row using Three-scenario TDD and Test economy below. Treat empty evidence on an unreached row as a normal state after partial seeding, not a seed failure; create its first check before implementing it, then close or fail the row with command-first evidence under step 5. Run only its targeted check under Evidence command, never the owning checkout's full verification command or the full suite.
34
34
  5. Update the target row's mutable cells: set `status` to `done` on a passing check or `failed` on a failing check, increment `attempts` by 1, and set `evidence` to one physical Markdown table cell opening with the command you ran under Evidence command, up to the first unescaped semicolon, plus at most three result lines separated by semicolons. Escape pipes as `\|` and use plain text without angle-bracket markup. On failure, record the concrete cause in plain words: the spec-vs-code contradiction, failing assertion, or missing credential, tool, or fixture. Keep reasoning and transcripts out of evidence.
@@ -18,7 +18,7 @@ Use the supplied record and report paths only. Never re-initialize a supplied re
18
18
  - In `report`, run steps 1-5 in the provisioned worktree. Skip the already completed provisioning fetch/gitignore/add only; verify HEAD against the supplied headRefOid and preserve sync's safety fetch. Keep trusted merge-base configuration and assessed-head binding. Dispatch Verify then Review sequentially as fresh helpers with cwd the same worktree, never new helper worktrees.
19
19
  - In `execute`, refresh before writing. If the head moved or the picked row disappeared, return a fresh report without executing the stale pick. Otherwise run step 7 under the course rules; under `deliver` run through a D3 readiness, obstacle, or cap exit and return REPORT. Keep course, `repair.rounds_cap`, `repair.syncs_cap`, usage, deadline, ledger, helper paths, and no-progress decisions PR-local. Preserve usage and caps on re-entry; only the first course pick may resolve its initial allowance.
20
20
  - Substitute supplied rebase confirmation at the existing ask only: yes proceeds; no records `sync: declined - other author's branch`, stage skipped, then assesses unsynced. Use supplied `verification command: <command>` or `spec: <path>` at that ask only, re-assessing from the start with the same record. Never treat answers as broader authority.
21
- - Track single-PR stages with session-local plan_tracker or a checklist. Delegate tracked source edits and commits to implementer; perform only orchestrator writes authorized by the injected procedure. Never push, edit PR metadata, comment, review, or write to a tracker before direct human authority, except the requested sync push. Never choose a sibling's actions.
21
+ - Track single-PR stages with session-local plan_tracker or a checklist. Delegate tracked source edits and commits to the fix wave's worker: `loop-implementer` for file findings, `implementer` for sync helpers; perform only orchestrator writes authorized by the injected procedure. Never push, edit PR metadata, comment, review, or write to a tracker before direct human authority, except the requested sync push. Never choose a sibling's actions.
22
22
 
23
23
  ## Recovery and output
24
24
 
@@ -21,35 +21,44 @@ import {
21
21
  } from "./gauntlet-settings.ts";
22
22
 
23
23
  test("loopEngine resolves defaults, integers, clamps, and malformed values", () => {
24
- const warning = "loopEngine.maxIterationsPerRow must be an integer; using default 4";
25
- for (const [raw, expected, expectedWarning] of [
26
- [undefined, 4, undefined], [7, 7, undefined], [0, 1, undefined], [-3, 1, undefined],
27
- ["4", 4, warning], [4.5, 4, warning], [true, 4, warning], [null, 4, warning],
28
- ] as const) {
29
- assert.deepEqual(resolveLoopEngine({ loopEngine: { maxIterationsPerRow: raw } }), {
30
- maxIterationsPerRow: expected, warning: expectedWarning,
31
- });
24
+ for (const [key, fallback] of [["maxIterationsPerRow", 4], ["escalateAfter", 3]] as const) {
25
+ const warning = `loopEngine.${key} must be an integer; using default ${fallback}`;
26
+ for (const [raw, expected, expectedWarning] of [
27
+ [undefined, fallback, undefined], [7, 7, undefined], [0, 1, undefined], [-3, 1, undefined],
28
+ [String(fallback), fallback, warning], [2.5, fallback, warning], [true, fallback, warning], [null, fallback, warning],
29
+ ] as const) {
30
+ assert.deepEqual(resolveLoopEngine({ loopEngine: { [key]: raw } }), {
31
+ maxIterationsPerRow: 4, escalateAfter: 3, [key]: expected, warning: expectedWarning,
32
+ }, `${key}: ${String(raw)}`);
33
+ }
32
34
  }
33
- assert.deepEqual(resolveLoopEngine({}), { maxIterationsPerRow: 4, warning: undefined });
35
+ assert.deepEqual(resolveLoopEngine({}), { maxIterationsPerRow: 4, escalateAfter: 3, warning: undefined });
34
36
  });
35
37
 
36
38
  test("loopEngine ignores the removed key by own-property presence and orders warnings", () => {
37
39
  const removed = "loopEngine.maxIterations is removed; set loopEngine.maxIterationsPerRow";
38
40
  for (const raw of [30, undefined, null, false]) {
39
41
  assert.deepEqual(resolveLoopEngine({ loopEngine: { maxIterations: raw } }), {
40
- maxIterationsPerRow: 4, warning: removed,
42
+ maxIterationsPerRow: 4, escalateAfter: 3, warning: removed,
41
43
  });
42
44
  }
43
- for (const [raw, expected, warning] of [
44
- [7, 7, removed],
45
- ["4", 4, removed + "; loopEngine.maxIterationsPerRow must be an integer; using default 4"],
45
+ for (const [raw, expected, allowanceWarning] of [
46
+ [7, 7, undefined],
47
+ ["4", 4, "loopEngine.maxIterationsPerRow must be an integer; using default 4"],
46
48
  ] as const) {
47
- assert.deepEqual(resolveLoopEngine({ loopEngine: { maxIterations: 99, maxIterationsPerRow: raw } }), {
48
- maxIterationsPerRow: expected, warning,
49
- });
49
+ for (const [escalateAfter, threshold, thresholdWarning] of [
50
+ [7, 7, undefined],
51
+ ["3", 3, "loopEngine.escalateAfter must be an integer; using default 3"],
52
+ ] as const) {
53
+ const loopEngine = { maxIterations: 99, maxIterationsPerRow: raw, escalateAfter };
54
+ assert.deepEqual(resolveLoopEngine({ loopEngine }), {
55
+ maxIterationsPerRow: expected, escalateAfter: threshold,
56
+ warning: [removed, allowanceWarning, thresholdWarning].filter(Boolean).join("; "),
57
+ });
58
+ }
50
59
  }
51
60
  assert.deepEqual(resolveLoopEngine({ loopEngine: Object.create({ maxIterations: 99 }) }), {
52
- maxIterationsPerRow: 4, warning: undefined,
61
+ maxIterationsPerRow: 4, escalateAfter: 3, warning: undefined,
53
62
  });
54
63
  });
55
64
 
@@ -61,6 +70,17 @@ test("mergeGauntlet: repo key replaces preset key whole-object", () => {
61
70
  const merged = mergeGauntlet(preset, repo);
62
71
  assert.deepEqual(merged.specCouncil, { members: ["b"] });
63
72
  assert.deepEqual(merged.closureReview, { model: "m" });
73
+ for (const [repoLoop, expected] of [
74
+ [undefined, { maxIterationsPerRow: 8, escalateAfter: 6 }],
75
+ [{ maxIterationsPerRow: 10 }, { maxIterationsPerRow: 10, escalateAfter: 3 }],
76
+ [{ escalateAfter: 2 }, { maxIterationsPerRow: 4, escalateAfter: 2 }],
77
+ ] as const) {
78
+ const loop = mergeGauntlet(
79
+ { loopEngine: { maxIterationsPerRow: 8, escalateAfter: 6 } },
80
+ repoLoop === undefined ? {} : { loopEngine: repoLoop },
81
+ );
82
+ assert.deepEqual(resolveLoopEngine(loop), { ...expected, warning: undefined });
83
+ }
64
84
  });
65
85
 
66
86
  test("mergeGauntlet: undefined layers -> {}", () => {
@@ -11,7 +11,7 @@ export interface PiGauntlet {
11
11
  flowGuards?: { enforce?: unknown; specDirs?: unknown };
12
12
  verifyBeforeShip?: { testCommands?: unknown; warningReference?: unknown };
13
13
  escalationLoop?: { implModel?: unknown };
14
- loopEngine?: { maxIterationsPerRow?: unknown; maxIterations?: unknown };
14
+ loopEngine?: { maxIterationsPerRow?: unknown; escalateAfter?: unknown; maxIterations?: unknown };
15
15
  phaseModels?: { plan?: unknown; implement?: unknown; verify?: unknown; ship?: unknown };
16
16
  telemetry?: { enabled?: unknown; dir?: unknown; buckets?: unknown };
17
17
  }
@@ -93,11 +93,14 @@ export function resolveClosureReview(g: PiGauntlet): ClosureReviewResolved {
93
93
  return { model, enforce, maxFixRounds };
94
94
  }
95
95
 
96
- export function resolveLoopEngine(g: PiGauntlet): { maxIterationsPerRow: number; warning: string | undefined } {
96
+ export function resolveLoopEngine(g: PiGauntlet): { maxIterationsPerRow: number; escalateAfter: number; warning: string | undefined } {
97
97
  const loop = g.loopEngine;
98
98
  const raw = loop?.maxIterationsPerRow;
99
99
  const valid = typeof raw === "number" && Number.isInteger(raw);
100
100
  const maxIterationsPerRow = valid ? Math.max(1, raw) : 4;
101
+ const rawEscalateAfter = loop?.escalateAfter;
102
+ const validEscalateAfter = typeof rawEscalateAfter === "number" && Number.isInteger(rawEscalateAfter);
103
+ const escalateAfter = validEscalateAfter ? Math.max(1, rawEscalateAfter) : 3;
101
104
  const warnings: string[] = [];
102
105
  if (loop && Object.hasOwn(loop, "maxIterations")) {
103
106
  warnings.push("loopEngine.maxIterations is removed; set loopEngine.maxIterationsPerRow");
@@ -105,7 +108,10 @@ export function resolveLoopEngine(g: PiGauntlet): { maxIterationsPerRow: number;
105
108
  if (raw !== undefined && !valid) {
106
109
  warnings.push("loopEngine.maxIterationsPerRow must be an integer; using default 4");
107
110
  }
108
- return { maxIterationsPerRow, warning: joinWarn(warnings) };
111
+ if (rawEscalateAfter !== undefined && !validEscalateAfter) {
112
+ warnings.push("loopEngine.escalateAfter must be an integer; using default 3");
113
+ }
114
+ return { maxIterationsPerRow, escalateAfter, warning: joinWarn(warnings) };
109
115
  }
110
116
 
111
117
  export interface EscalationLoopResolved {
@@ -1468,16 +1468,17 @@ test("gauntlet_setting escalationLoop: setting absent -> ctx-derived main-loop m
1468
1468
  });
1469
1469
 
1470
1470
  test("gauntlet_setting loopEngine resolves the key", async () => {
1471
- for (const [loopEngine, expected, warning] of [
1472
- [undefined, 4, undefined],
1473
- [{ maxIterationsPerRow: 8 }, 8, undefined],
1474
- [{ maxIterations: 99, maxIterationsPerRow: "4" }, 4,
1475
- "loopEngine.maxIterations is removed; set loopEngine.maxIterationsPerRow; loopEngine.maxIterationsPerRow must be an integer; using default 4"],
1471
+ for (const [loopEngine, expected, escalateAfter, warning] of [
1472
+ [undefined, 4, 3, undefined],
1473
+ [{ maxIterationsPerRow: 8, escalateAfter: 2 }, 8, 2, undefined],
1474
+ [{ escalateAfter: 0 }, 4, 1, undefined],
1475
+ [{ maxIterations: 99, maxIterationsPerRow: "4", escalateAfter: "3" }, 4, 3,
1476
+ "loopEngine.maxIterations is removed; set loopEngine.maxIterationsPerRow; loopEngine.maxIterationsPerRow must be an integer; using default 4; loopEngine.escalateAfter must be an integer; using default 3"],
1476
1477
  ] as const) {
1477
1478
  const h = harness({ cwd: tempCwd({ piGauntlet: { loopEngine } }) });
1478
1479
  const tool = h.tools.find(t => t.name === "gauntlet_setting")!;
1479
1480
  const result = await tool.execute("loop", { key: "loopEngine" }, undefined, undefined, h.ctx) as any;
1480
- assert.deepEqual(result.details, { key: "loopEngine", maxIterationsPerRow: expected, warning, errors: [] });
1481
+ assert.deepEqual(result.details, { key: "loopEngine", maxIterationsPerRow: expected, escalateAfter, warning, errors: [] });
1481
1482
  assert.equal(Object.hasOwn(result.details, "maxIterations"), false);
1482
1483
  }
1483
1484
  });
@@ -1489,6 +1490,7 @@ test("gauntlet_setting loopEngine composes settings load warnings", async () =>
1489
1490
  const result = await h.tools.find(t => t.name === "gauntlet_setting")!.execute("loop", { key: "loopEngine" }, undefined, undefined, h.ctx) as any;
1490
1491
  assert.equal(result.details.key, "loopEngine");
1491
1492
  assert.equal(result.details.maxIterationsPerRow, 4);
1493
+ assert.equal(result.details.escalateAfter, 3);
1492
1494
  assert.equal(Object.hasOwn(result.details, "maxIterations"), false);
1493
1495
  assert.ok(result.details.errors.length);
1494
1496
  assert.equal(result.details.warning, `⚠️ gauntlet settings load error (using defaults): ${result.details.errors.join("; ")}`);
@@ -1539,37 +1541,38 @@ const verifyBranch = (extra: unknown[] = []) => [
1539
1541
 
1540
1542
  const subagentCall = (id: string, input: unknown) => ({ toolName: "subagent", toolCallId: id, input });
1541
1543
  const loneImplementer = (extra: Record<string, unknown> = {}) => ({ agent: "implementer", task: "fix G1", ...extra });
1542
- const implementerWave = (n = 1) => ({
1543
- tasks: Array.from({ length: n }, (_, i) => ({ agent: "implementer", task: `fix G${i + 1}`, worktree: true })),
1544
+ const implementerWave = (n = 1, agent = "implementer") => ({
1545
+ tasks: Array.from({ length: n }, (_, i) => ({ agent, task: `fix G${i + 1}`, worktree: true })),
1544
1546
  });
1545
1547
 
1546
1548
  const firstCallResult = async (h: ReturnType<typeof harness>, id: string, input: unknown) =>
1547
1549
  (await h.emitEvent("tool_call", subagentCall(id, input)))[0] as { block?: boolean; reason?: string } | undefined;
1548
1550
 
1549
- test("shape guard: lone implementer blocked in verify after the audit, with and without closureReview.model, async or not", async () => {
1551
+ for (const agent of ["implementer", "loop-implementer"]) {
1552
+ test(`shape guard: lone ${agent} blocked in verify after the audit, with and without closureReview.model, async or not`, async () => {
1550
1553
  for (const settings of [
1551
1554
  { piGauntlet: { closureReview: { enforce: true } } },
1552
1555
  { piGauntlet: { closureReview: { enforce: true, model: "x/y" } } },
1553
1556
  ]) {
1554
1557
  const h = harness({ cwd: tempCwd(settings), branch: verifyBranch([subagentResult(["conformance-reviewer"])]) });
1555
1558
  await h.emit("session_start");
1556
- const sync = await firstCallResult(h, "s1", loneImplementer());
1559
+ const sync = await firstCallResult(h, "s1", loneImplementer({ agent }));
1557
1560
  assert.equal(sync?.block, true);
1558
1561
  assert.match(sync?.reason ?? "", /dispatch implementers as a one-task tasks wave/);
1559
1562
  assert.match(sync?.reason ?? "", /piGauntlet\.closureReview\.enforce: false/);
1560
- const async = await firstCallResult(h, "s2", loneImplementer({ async: true }));
1563
+ const async = await firstCallResult(h, "s2", loneImplementer({ agent, async: true }));
1561
1564
  assert.equal(async?.block, true);
1562
1565
  }
1563
1566
  });
1564
1567
 
1565
- test("shape guard: lone implementer passes before the audit, in implement, in ship, and on management calls", async () => {
1568
+ test(`shape guard: lone ${agent} passes before the audit, in implement, in ship, and on management calls`, async () => {
1566
1569
  const preLatch = harness({ branch: verifyBranch() });
1567
1570
  await preLatch.emit("session_start");
1568
- assert.equal(await firstCallResult(preLatch, "s1", loneImplementer()), undefined);
1571
+ assert.equal(await firstCallResult(preLatch, "s1", loneImplementer({ agent })), undefined);
1569
1572
 
1570
1573
  const implement = harness({ branch: implementBranch([subagentResult(["conformance-reviewer"])]) });
1571
1574
  await implement.emit("session_start");
1572
- assert.equal(await firstCallResult(implement, "s1", loneImplementer()), undefined);
1575
+ assert.equal(await firstCallResult(implement, "s1", loneImplementer({ agent })), undefined);
1573
1576
 
1574
1577
  const ship = harness({
1575
1578
  branch: verifyBranch([
@@ -1579,12 +1582,13 @@ test("shape guard: lone implementer passes before the audit, in implement, in sh
1579
1582
  ]),
1580
1583
  });
1581
1584
  await ship.emit("session_start");
1582
- assert.equal(await firstCallResult(ship, "s1", loneImplementer()), undefined);
1585
+ assert.equal(await firstCallResult(ship, "s1", loneImplementer({ agent })), undefined);
1583
1586
 
1584
1587
  const mgmt = harness({ branch: verifyBranch([subagentResult(["conformance-reviewer"])]) });
1585
1588
  await mgmt.emit("session_start");
1586
- assert.equal(await firstCallResult(mgmt, "s1", { action: "status", agent: "implementer" }), undefined);
1589
+ assert.equal(await firstCallResult(mgmt, "s1", { action: "status", agent }), undefined);
1587
1590
  });
1591
+ }
1588
1592
 
1589
1593
  test("shape guard: dormant when the flow was never entered (cold start verify) and when closureReview.enforce is false", async () => {
1590
1594
  const cold = harness({
@@ -1711,19 +1715,21 @@ test("interrupted implementer waves still spend rounds and granted credits and n
1711
1715
  const guardedHarness = (settings: unknown, extra: unknown[] = []) =>
1712
1716
  harness({ cwd: tempCwd(settings), branch: verifyBranch([subagentResult(["conformance-reviewer"]), ...extra]) });
1713
1717
 
1714
- test("cap guard: default 5 waves pass, the sixth is blocked with the escalation text", async () => {
1718
+ for (const agent of ["implementer", "loop-implementer"]) {
1719
+ test(`cap guard: default 5 ${agent} waves pass, the sixth is blocked with the escalation text`, async () => {
1715
1720
  const h = guardedHarness({ piGauntlet: { closureReview: { enforce: true } } });
1716
1721
  await h.emit("session_start");
1717
1722
  for (let i = 1; i <= 5; i++) {
1718
- assert.equal(await firstCallResult(h, `c${i}`, implementerWave(2)), undefined, `round ${i} passes`);
1719
- await h.emitEvent("tool_result", okWave(`c${i}`));
1723
+ assert.equal(await firstCallResult(h, `c${i}`, implementerWave(2, agent)), undefined, `round ${i} passes`);
1724
+ await h.emitEvent("tool_result", waveResult(`c${i}`, [{ agent, exitCode: 0 }, { agent, exitCode: 1 }]));
1720
1725
  }
1721
- const blocked = await firstCallResult(h, "c6", implementerWave(1));
1726
+ const blocked = await firstCallResult(h, "c6", implementerWave(1, agent));
1722
1727
  assert.equal(blocked?.block, true);
1723
1728
  assert.match(blocked?.reason ?? "", /^Conformance fix loop: 5 fix round\(s\) used against a cap of 5 \(granted rounds included\); escalate to the human/);
1724
1729
  assert.ok((blocked?.reason ?? "").includes("Start the escalation message with the line: gauntlet: gate=verification-before-completion/step-6"));
1725
1730
  assert.doesNotMatch(blocked?.reason ?? "", /^gauntlet:/);
1726
1731
  });
1732
+ }
1727
1733
 
1728
1734
  test("cap guard: maxFixRounds 0 blocks the first wave; 1 blocks the second; a chain implementer is cap-checked and counted", async () => {
1729
1735
  const zero = guardedHarness({ piGauntlet: { closureReview: { maxFixRounds: 0 } } });
@@ -1847,13 +1853,14 @@ test("counter reset: start implement and reset zero it; start verify --force kee
1847
1853
  assert.equal(await firstCallResult(reset, "c1", loneImplementer()), undefined, "dormant after reset");
1848
1854
  });
1849
1855
 
1850
- test("replay: two implementer waves after the audit in verify restore fixRounds 2; the same waves in ship restore 0", async () => {
1856
+ for (const agent of ["implementer", "loop-implementer"]) {
1857
+ test(`replay: two ${agent} waves after the audit in verify restore fixRounds 2; the same waves in ship restore 0`, async () => {
1851
1858
  const inVerify = guardedHarness({ piGauntlet: { closureReview: { maxFixRounds: 2 } } }, [
1852
- subagentResult(["implementer"]),
1853
- subagentResult(["implementer", "wave-reviewer"]),
1859
+ subagentResult([agent]),
1860
+ subagentResult([agent, "wave-reviewer"]),
1854
1861
  ]);
1855
1862
  await inVerify.emit("session_start");
1856
- assert.equal((await firstCallResult(inVerify, "c1", implementerWave(1)))?.block, true);
1863
+ assert.equal((await firstCallResult(inVerify, "c1", implementerWave(1, agent)))?.block, true);
1857
1864
 
1858
1865
  const inShip = harness({
1859
1866
  cwd: tempCwd({ piGauntlet: { closureReview: { maxFixRounds: 2 } } }),
@@ -1861,14 +1868,15 @@ test("replay: two implementer waves after the audit in verify restore fixRounds
1861
1868
  subagentResult(["conformance-reviewer"]),
1862
1869
  phaseResult("complete", phases({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete" })),
1863
1870
  phaseResult("start", phases({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete", ship: "in_progress" })),
1864
- subagentResult(["implementer"]),
1865
- subagentResult(["implementer"]),
1871
+ subagentResult([agent]),
1872
+ subagentResult([agent]),
1866
1873
  phaseResult("start", phases({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "in_progress", ship: "in_progress" })),
1867
1874
  ]),
1868
1875
  });
1869
1876
  await inShip.emit("session_start");
1870
- assert.equal(await firstCallResult(inShip, "c1", implementerWave(1)), undefined);
1877
+ assert.equal(await firstCallResult(inShip, "c1", implementerWave(1, agent)), undefined);
1871
1878
  });
1879
+ }
1872
1880
 
1873
1881
  // --- Human overrule of the fix-round cap (spec 2026-09-14-fix-round-human-overrule) ---
1874
1882
 
@@ -97,12 +97,13 @@ const qualifiesAsClosureDispatch = (details: unknown): boolean => {
97
97
  // One conformance fix round = one non-error subagent result carrying at least one
98
98
  // implementer child, regardless of dispatch mode or per-child exit code (a failed
99
99
  // implementer still spent the round; its retry is the next one). Async and
100
- // management dispatches return results: [] and never count.
101
- const isImplementerWave = (details: unknown, isError: boolean | undefined): boolean => {
100
+ // management dispatches return results: [] and never count. Only fix-round
101
+ // accounting includes loop workers; the open-task nudge stays implementer-only.
102
+ const isImplementerWave = (details: unknown, isError: boolean | undefined, includeLoop = false): boolean => {
102
103
  if (isError === true) return false;
103
104
  const d = details as { results?: { agent?: unknown }[] } | undefined;
104
105
  if (!d || !Array.isArray(d.results) || d.results.length === 0) return false;
105
- return d.results.some((r) => r?.agent === "implementer");
106
+ return d.results.some((r) => r?.agent === "implementer" || (includeLoop && r?.agent === "loop-implementer"));
106
107
  };
107
108
 
108
109
  type Task = { name: string; status: string };
@@ -236,8 +237,8 @@ const conformanceModels = (input: unknown): (string | undefined)[] =>
236
237
  // Conformance fix-loop dispatch guard (spec 2026-09-13-conformance-dispatch-guard):
237
238
  // after the first audit, pi-cohort honours worktree: true only in tasks mode, so a
238
239
  // lone implementer runs unisolated and yields no patch for the integrate step.
239
- const loneImplementerBlockReason =
240
- 'Conformance fix loop: dispatch implementers as a one-task tasks wave (tasks: [{ agent: "implementer", worktree: true, ... }]); ' +
240
+ const loneImplementerBlockReason = (agent: string) =>
241
+ `Conformance fix loop: dispatch implementers as a one-task tasks wave (tasks: [{ agent: "${agent}", worktree: true, ... }]); ` +
241
242
  "a lone agent call runs unisolated and produces no worktree diff. " +
242
243
  "To disable this gate, set piGauntlet.closureReview.enforce: false.";
243
244
 
@@ -463,7 +464,7 @@ export default function (pi: ExtensionAPI) {
463
464
  gauntletEntered &&
464
465
  phases.verify.status === "in_progress" &&
465
466
  conformanceDispatched &&
466
- isImplementerWave(details, isError) &&
467
+ isImplementerWave(details, isError, true) &&
467
468
  resolveClosureReview(loadGauntletSettings(ctx.cwd).gauntlet).enforce
468
469
  ) {
469
470
  if (fixRoundCredits > 0) fixRoundCredits -= 1;
@@ -685,10 +686,12 @@ export default function (pi: ExtensionAPI) {
685
686
  // Fix-loop window: verify in progress and the R0 audit observed. Read directly,
686
687
  // not via activeGuardPhase() - verify is not a GUARD_PHASES member.
687
688
  const fixLoopWindow = !isSubagentChild && !hasAction && phases.verify.status === "in_progress" && conformanceDispatched;
688
- if (fixLoopWindow && (event.input as { agent?: unknown })?.agent === "implementer") {
689
- return { block: true, reason: loneImplementerBlockReason };
689
+ const fixAgents = ["implementer", "loop-implementer"];
690
+ const singleAgent = (event.input as { agent?: unknown })?.agent;
691
+ if (fixLoopWindow && typeof singleAgent === "string" && fixAgents.includes(singleAgent)) {
692
+ return { block: true, reason: loneImplementerBlockReason(singleAgent) };
690
693
  }
691
- if (fixLoopWindow && collectAgents(event.input, "implementer").length > 0) {
694
+ if (fixLoopWindow && fixAgents.some((agent) => collectAgents(event.input, agent).length > 0)) {
692
695
  const cap = resolveClosureReview(g()).maxFixRounds;
693
696
  if (fixRounds >= cap) {
694
697
  const funded = fixRoundCredits > 0 && (event.input as { async?: unknown })?.async !== true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "8.1.6",
3
+ "version": "8.1.8",
4
4
  "description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
5
5
  "author": "Jacek Juraszek",
6
6
  "type": "module",
@@ -39,7 +39,9 @@ Mint and initialize the continuation record before progress initialization (`ref
39
39
 
40
40
  ## Harness notes
41
41
 
42
- Every helper is a fresh context with `cwd` the PR worktree; its task names its output path and ends with the output contract. Every dispatch on either harness gets its own output path from `mktemp "${TMPDIR:-/tmp}/gatekeep-<role>.XXXXXX"` outside the worktree. On pi: `subagent({ agent: <persona>, context: "fresh", async: false, cwd: <worktree> })` with `worker` for Verify, `wave-reviewer` for Review and the pre-push review, `implementer` for payloads; `gauntlet_setting({ key: "closureReview" })` supplies the fix-round cap. On Claude Code: the harness's subagent dispatch tool with the duty text as the prompt - Verify: `verification-brief.md` Section B; Review, pre-push review, and comment-delta review: Section C plus `review-baseline.md` (append `reference/fix-wave.md` `## Re-review task` for the pre-push review and ancestor step-4 review, on both harnesses); implementer: the payload task of `reference/fix-wave.md` `## Wave`; sync conflict stop: the task `reference/sync.md` `## Conflict stop` builds; sync review: Section C plus `review-baseline.md` with the `## Review before push` subject, no closure lines; the no-tool accounting fallback is `2 x 5`. A Claude Code helper inherits the session's directory, so its prompt opens with the absolute worktree path and the rule that every command runs as `git -C <worktree> ...` or `(cd <worktree> && ...)`, checks that `git -C <worktree> rev-parse HEAD` equals the head its task names (the assessed head for Verify, Review, and the comment-delta reviewer; the current wave HEAD for wave helpers) before working, and names the output path; the orchestrator awaits the tool result before reading the file. For description recheck on pi, use the fresh worker duty in Section B with mode `description recheck`; on Claude Code, use Section B's separate recheck duty as the native helper prompt. Both tasks supply actual body/head, affected claim IDs, existing evidence, and the affected-only disposition/closure contract, never the CI-emitting claim-only contract. A harness with neither facility is unsupported: stop at step 3 and say so - at step 2b, before any sync write, when `--rebase` was passed (`reference/sync.md`).
42
+ Every helper is a fresh context with `cwd` the PR worktree; its task names its output path and ends with the output contract. Every dispatch on either harness gets its own output path from `mktemp "${TMPDIR:-/tmp}/gatekeep-<role>.XXXXXX"` outside the worktree. On pi: `subagent({ agent: <persona>, context: "fresh", async: false, cwd: <worktree> })` with `worker` for Verify, `wave-reviewer` for Review and the pre-push review, `loop-implementer` for file findings, `implementer` for sync conflict-stop and base-merge helpers; `gauntlet_setting({ key: "closureReview" })` supplies the fix-round cap. On Claude Code: the harness's subagent dispatch tool with the duty text as the prompt - Verify: `verification-brief.md` Section B; Review, pre-push review, and comment-delta review: Section C plus `review-baseline.md` (append `reference/fix-wave.md` `## Re-review task` for the pre-push review and ancestor step-4 review, on both harnesses); file worker: the shared context and Claude Code tail of `reference/fix-wave.md` `## Wave`; sync conflict stop: the task `reference/sync.md` `## Conflict stop` builds; sync review: Section C plus `review-baseline.md` with the `## Review before push` subject, no closure lines; the no-tool accounting fallback is `2 x 5`. A Claude Code helper inherits the session's directory, so its prompt opens with the absolute worktree path and the rule that every command runs as `git -C <worktree> ...` or `(cd <worktree> && ...)`, checks that `git -C <worktree> rev-parse HEAD` equals the head its task names (the assessed head for Verify, Review, and the comment-delta reviewer; the current wave HEAD for wave helpers) before working, and names the output path; the orchestrator awaits the tool result before reading the file. For description recheck on pi, use the fresh worker duty in Section B with mode `description recheck`; on Claude Code, use Section B's separate recheck duty as the native helper prompt. Both tasks supply actual body/head, affected claim IDs, existing evidence, and the affected-only disposition/closure contract, never the CI-emitting claim-only contract. A harness with neither facility is unsupported: stop at step 3 and say so - at step 2b, before any sync write, when `--rebase` was passed (`reference/sync.md`).
43
+
44
+ For pi file-finding row dispatch, use `subagent({ agent: "loop-implementer", context: "fresh", async: false, cwd: <worktree>, output: <per-dispatch output path>, outputMode: "file-only", acceptance: { level: "none", reason: "the parent validates the commit and keeps the ledger" }, task: <row task> })`; add `model: <implModel>` on the escalated attempt only, and build the row task through `reference/fix-wave.md` `### Row dispatch`.
43
45
 
44
46
  ## Wording rules
45
47
 
@@ -28,7 +28,7 @@ With rebase requested, read the viewer via `gh api user --jq .login`. After fetc
28
28
 
29
29
  Create the OS-temp roster with `mktemp "${TMPDIR:-/tmp}/gatekeep-roster.XXXXXX"`, never in a worktree. Store per PR number, worktree, record path, report path, status (`pending`, `report`, `conflicting`, `failed`, `stopped`, `retired`), reason, and queued pick. Pre-mint record/report paths outside worktrees.
30
30
 
31
- Initialize each existing-format continuation record once: `consent.target` (lowercase base owner/repo, PR number, gathered baseRefName), PR identity, worktree, `course` null, `repair.rounds_used` and `repair.syncs_used` zero, `approved_head` null, `cleanup_done` false. Use the continuation-record schema; initialize `repair.rounds_cap` and `repair.syncs_cap` to `2 x` the resolved `closureReview.maxFixRounds` under D4 (`fix-wave.md` `## Wave`). Only the first course pick may resolve its initial D4 allowance; re-dispatch never reinitializes usage or caps. The parent owns only initialization and direct queued picks; the child owns all assessment and execution fields, findings, digest, evidence, repair state, approved head, payloads, and comment ledger. Carry confirmations and answers in task text only; never overwrite course, counters, caps, deadline, ledger, or helper paths on refresh or re-dispatch. Validate supplied records using the continuation-record identity rules, never reset them.
31
+ Initialize each existing-format continuation record once: `consent.target` (lowercase base owner/repo, PR number, gathered baseRefName), PR identity, worktree, `course` null, `repair.rounds_used` and `repair.syncs_used` zero, `approved_head` null, `cleanup_done` false. Use the continuation-record schema; initialize `repair.rounds_cap` and `repair.syncs_cap` to `2 x` the resolved `closureReview.maxFixRounds` under D4 (`fix-wave.md` `## Wave`). Only the first course pick may resolve its initial D4 allowance; re-dispatch never reinitializes usage or caps. The parent owns only initialization and direct queued picks; the child owns all assessment and execution fields, findings, digest, evidence, repair state, approved head, payloads, and comment ledger. Carry confirmations and answers in task text only; never overwrite course, counters, caps, deadline, ledger, or helper paths on refresh or re-dispatch. Carry `repair.wave.spec` and `repair.wave.ledger` once created, with their row history and counters, through every private handoff; wave settlement retains the pair until terminal teardown. Validate supplied records using the continuation-record identity and finding-artifact rules: a missing locator or missing, unreadable or malformed artifact exits `stuck - run record unreadable`, never resets them.
32
32
 
33
33
  ## Foreground dispatch
34
34
 
@@ -38,7 +38,7 @@ Supply mode `report` or `execute`, PR identity, rebase flag/base, confirmation,
38
38
 
39
39
  `worktree provisioned at <path> on <headRefOid>; fetch and gitignore done - skip the fetch, run no worktree add, verify HEAD`
40
40
 
41
- Supply the skill and this batch contract explicitly; do not depend on child discovery. Derive the installed procedure root from the selected skill's absolute `filePath` metadata (the path returned by the runtime's skill resolution), taking the directory containing `SKILL.md`. Supply `procedure root: <absolute installed skill directory>` in every report and execute task. Resolve all named procedure dependencies relative to this root, never the PR worktree or cwd; this includes sibling briefs and baseline files as well as files under `reference/`. Report mode runs steps 1-5 with sequential fresh Verify then Review in the supplied worktree, never separate helper worktrees. Preserve trusted configuration, digest/head binding, and PR-local tracking. Delegate tracked source edits and commits to implementer. Execute refreshes first, then runs step 7 using the course rules; under `deliver` run through a D3 readiness, obstacle, or cap exit and return `REPORT`. Refresh before any execute write: if HEAD moved or the row is unavailable, return a fresh report instead of doing the stale action. The course, `repair.rounds_cap`/`repair.syncs_cap`, usage, deadlines, and no-progress decisions stay PR-local; never borrow sibling consent. Parallel waits are independent bounded watches.
41
+ Supply the skill and this batch contract explicitly; do not depend on child discovery. Derive the installed procedure root from the selected skill's absolute `filePath` metadata (the path returned by the runtime's skill resolution), taking the directory containing `SKILL.md`. Supply `procedure root: <absolute installed skill directory>` in every report and execute task. Resolve all named procedure dependencies relative to this root, never the PR worktree or cwd; this includes sibling briefs and baseline files as well as files under `reference/`. Report mode runs steps 1-5 with sequential fresh Verify then Review in the supplied worktree, never separate helper worktrees. Preserve trusted configuration, digest/head binding, and PR-local tracking. Delegate tracked source edits and commits to the fix wave's worker: `loop-implementer` for file findings, `implementer` for sync helpers. Execute refreshes first, then runs step 7 using the course rules; under `deliver` run through a D3 readiness, obstacle, or cap exit and return `REPORT`. Refresh before any execute write: if HEAD moved or the row is unavailable, return a fresh report instead of doing the stale action. The course, `repair.rounds_cap`/`repair.syncs_cap`, usage, deadlines, and no-progress decisions stay PR-local; never borrow sibling consent. Parallel waits are independent bounded watches.
42
42
 
43
43
  Write only the supplied report, never a second record/report. End with exactly one closing result line:
44
44
 
@@ -68,7 +68,7 @@ Judge contract findings (AC outcomes, Design clauses, claim dispositions, check
68
68
  | `R<n>: open - <why>` | Keep it open with its posted text and locator. |
69
69
  | No line for `R<n>` | Keep it open with its posted text and locator; add one `show evidence` line `R<n>: no closure line - kept open`. |
70
70
 
71
- Mint each open item's `P#` once, keyed to the selected review id and `R#`; reuse it on re-integration. Record `R<n>` as resolved for the report count when an exact-head pre-push reviewer returns a `P#`/`L#` `resolved` closure line for an id minted from that `R<n>`; never infer closure from a push alone. Deduplicate a native finding for the same defect into that `P#` under the one-defect rule; retain independent contributors. Classify the prior item from `prior_review.state` and `viewer.is_author`, never today's reviewer severity: blocker for `CHANGES_REQUESTED` or an own PR, nit otherwise. List open prior items first under `Blockers` or `Nits`. Keep `R#` keys and missing-line diagnostics under `show evidence`, not in the rendered lists.
71
+ Mint each open item's `P#` once, keyed to the selected review id and `R#`; reuse it on re-integration. Record `R<n>` as resolved for the report count when an exact-head pre-push reviewer returns a `P#`/`L#` `resolved` closure line for an id minted from that `R<n>`; never infer closure from a push alone. Deduplicate a native finding for the same defect into that `P#` under the one-defect rule; retain independent contributors. Classify the prior item from `prior_review.state` and `viewer.is_author`, never today's reviewer severity: blocker for `CHANGES_REQUESTED` or an own PR, nit otherwise. Render open prior items first under `Blockers` or `Nits` with the reviewer's posted text and locator unchanged, not rewritten into consequence sentences. Keep `R#` keys and missing-line diagnostics under `show evidence`, not in the rendered lists.
72
72
 
73
73
  **`C#` ledger.** Each ledger row carries its comment `id` and the `updated_at` it was minted against; the post-push diff (`post-selection-loop.md` `### Re-render`) runs against this ledger, never against the report text. Row states: a triage label (`already-addressed` / `reasonable` / `judgment-call`) with a drafted reply; `superseded by C<new>`; `withdrawn`; `pending`; `reviewer failed (<conclusion>)`; `represented by R<n>`. Only triage-labeled rows carry replies; `reply` skips every other state.
74
74
 
@@ -91,10 +91,10 @@ Record dispositions only from direct human check commands. A flaky disposition f
91
91
 
92
92
  ## Drafted payloads
93
93
 
94
- Step 5 drafts, never applies. For every blocker with a file-level fix - code and doc drift alike - draft the concrete edit as a payload keyed to the finding's internal ID. Draft nit payloads under `deliver-nits` or a terminal `fix nits` pick: print them in the `show evidence` payload shape and dispatch the wave in the same pick with no second confirmation; before that pick `show evidence` prints only the payloads that exist. For every replyable `C#`, draft the reply. The worktree stays tracked-clean (`git status --porcelain --untracked-files=no` empty) at every menu render; a payload is applied only under a delivery course or a terminal `fix nits` pick (the same path for code and docs) and dropped at teardown otherwise. A `P#` carried by a `rebased locally` sync record is drafted like any blocker, from the helper's test output and `git show <stopped commit>`; a `P#` whose smallest fix needs another repo, infrastructure, or a credential stays open. Keep the stored scope baseline authoritative; an implementer needing a contract change returns open - contract exception without changing it.
94
+ Step 5 drafts, never applies. For every blocker with a file-level fix - code and doc drift alike - draft the concrete edit as a payload keyed to the finding's internal ID. Draft nit payloads under `deliver-nits` or a terminal `fix nits` pick: print them in the `show evidence` payload shape and dispatch the wave in the same pick with no second confirmation; before that pick `show evidence` prints only the payloads that exist. For every replyable `C#`, draft the reply. The worktree stays tracked-clean (`git status --porcelain --untracked-files=no` empty) at every menu render; a payload is applied only under a delivery course or a terminal `fix nits` pick (the same path for code and docs) and dropped at teardown otherwise. A `P#` carried by a `rebased locally` sync record is drafted like any blocker, from the helper's test output and `git show <stopped commit>`; a `P#` whose smallest fix needs another repo, infrastructure, or a credential stays open. Keep the stored scope baseline authoritative; route a worker needing a contract change through `fix-wave.md`'s harness-specific `contract exception` return without changing it.
95
95
 
96
96
  Extend each finding-keyed evidence pack with `target: file | description`. For file targets retain the concrete edit and scoped test files. For description targets store `{ finding_id, contributor_ids, assessed_head, original_body, old_text, new_text, replacement_body, source_evidence }` with exact old-to-new text and unrelated body text preserved. Keep independent contributions under all their IDs when one finding combines a claim and test/scope/check gap; close only source-backed resolved claim contributions, never the whole combined finding from a body edit. Judge source: pr gaps against original_intent, not the shortened body.
97
97
 
98
- A payload is the smallest in-repo edit that turns the finding green and keeps the behavior the PR promises; it never adds infrastructure, a dependency, or a new module. A failure caused outside the repo (image pull, toolchain, credentials, external service) gets the smallest in-repo workaround with its reason in one sentence; a skip never removes a test an AC row relies on. A finding whose smallest fix needs another repo, infrastructure, or a credential stays open with the implementer's `open - needs <what>` sentence. A delivery course applies a description payload alongside the file payloads through `post-selection-loop.md` `### Description repair`; show compact exact old-to-new text under the blocker and the full replacement body under `show evidence`. Apply a requested factual clarification only within selected course work; mint no fake blocker. Never draft an unsolicited style rewrite.
98
+ A payload is the smallest in-repo edit that turns the finding green and keeps the behavior the PR promises; it never adds infrastructure, a dependency, or a new module. A failure caused outside the repo (image pull, toolchain, credentials, external service) gets the smallest in-repo workaround with its reason in one sentence; a skip never removes a test an AC row relies on. Keep a finding whose smallest fix needs another repo, infrastructure, or a credential open with the `loop-implementer` failed decision's `needs <what>` second slot on pi, or the native helper's `open - needs <what>` sentence on Claude Code. A delivery course applies a description payload alongside the file payloads through `post-selection-loop.md` `### Description repair`; show compact exact old-to-new text under the blocker and the full replacement body under `show evidence`. Apply a requested factual clarification only within selected course work; mint no fake blocker. Never draft an unsolicited style rewrite.
99
99
 
100
- A posted review body is composed at post time from the blockers being addressed - one summary sentence, then the numbered items, ending on the fix or asked action.
100
+ Compose a posted review body at post time from the blockers being addressed - one summary sentence, then the numbered items, ending on the fix or asked action. For a carried open blocker backed by a pi row, include `open - <last decision line>` on its item so the next run's prior review retains the failed-attempt knowledge.
@@ -1,6 +1,6 @@
1
1
  # gatekeep-pr: fix wave
2
2
 
3
- Read from `post-selection-loop.md` for delivery work or a terminal nit iteration. Keep edits and commits helper-owned; the orchestrator never edits a tracked file or commits. Permit only fetch, merge --ff-only, ordinary push, and the exact rewrite/recovery operations owned below.
3
+ Read from `post-selection-loop.md` for delivery work or a terminal nit iteration. Keep tracked edits and commits helper-owned; the orchestrator writes only the run record and its external finding spec and ledger, never a tracked file or commit. Permit only fetch, merge --ff-only, ordinary push, and the exact rewrite/recovery operations owned below.
4
4
 
5
5
  ## Sync
6
6
 
@@ -23,24 +23,153 @@ On every rewritten-head push use the complete form `git push --no-follow-tags --
23
23
 
24
24
  At course pick resolve `gauntlet_setting({ key: "closureReview" })`. Initialize repair.rounds_cap and repair.syncs_cap to `2 x closureReview.maxFixRounds`; use no-tool fallback `2 x 5`. Initialize usage to zero once per run. Cap 0 or missing helpers omits delivery rows. On obstacle deliver add exactly `2 x maxFixRounds` to each cap, persist before work and never reset usage; timeout continuation instead opens one window without extending either cap. Keep closureReview.enforce and the verify-phase phase-tracker's `1 x maxFixRounds` accounting unchanged; refused dispatch exits `stuck - phase-tracker fix-round guard: <its reason>`.
25
25
 
26
- Record pre_wave_head before work. Initialize one per-pass checkpoint with round_start_head and counted false before payload dispatch, including each carried-set retry; preserve it on interrupted reconciliation, never recreate it for the same attempt. Count a round once only after at least one repair payload dispatch leaves a new source-changing commit, including a source-changing carried-set retry. Persist counted true and counted_head in repair.wave with stage apply/review/push before reconciliation can repeat it; a fresh carried pass keeps prior checkpoint history. Base-only conflict commits, description-only writes, watches, re-reviews, empty waves and restored failures cost no round. Keep usage cumulative across re-entry. At round cap reached or sync cap reached exit through the loop's ordered Exit check; retain commits and name any reviewer's top finding. Update stage and counted checkpoints without replay; clear wave only after reconciliation and settlement of No-progress obstacle. For `action: fix-nits`, retain repair.wave including its action and selected payloads through watch and own-push re-review; clear it only through `post-selection-loop.md` `### Next action`'s one-shot completion rule.
26
+ Record pre_wave_head before work. Initialize one per-pass checkpoint with round_start_head and counted false before payload dispatch, including each carried-set retry; preserve it on interrupted reconciliation, never recreate it for the same attempt. Count a round once only after at least one repair payload dispatch leaves a new source-changing commit eligible under Admission's round-charge exceptions, including a source-changing carried-set retry. Persist counted true and counted_head in repair.wave with stage apply/review/push before reconciliation can repeat it; a fresh carried pass keeps prior checkpoint history. Base-only conflict commits, description-only writes, watches, re-reviews, empty waves and restored failures cost no round. Keep usage cumulative across re-entry. At round cap reached or sync cap reached exit through the loop's ordered Exit check; retain commits and name any reviewer's top finding. Update stage and counted checkpoints without replay; settle only per-pass wave fields; retain `repair.wave.spec` and `repair.wave.ledger` and all row history until terminal teardown. Settle the per-pass fields only after reconciliation and settlement of No-progress obstacle. For `action: fix-nits`, retain repair.wave including its action and selected payloads through watch and own-push re-review; clear it only through `post-selection-loop.md` `### Next action`'s one-shot completion rule.
27
27
 
28
- For each file payload in review order, dispatch one fresh implementer helper with `cwd` the PR worktree. Its task holds:
28
+ On pi, dispatch selected file findings as companion `loop-implementer` rows in review order, including terminal `fix nits` work and carried-set retries; use the derived spec's mapped AC as the finding to close, not the drafted edit as a fixed solution. On Claude Code, dispatch one fresh implementer helper once per drafted file payload in review order. Keep sync helpers on `implementer` under Sync.
29
29
 
30
- - the finding's evidence pack (`findings.md` `## Drafted payloads` shape) and the instruction to apply the course-authorized payload - or, for a reviewer finding that has no drafted payload, the reviewer's finding text - never to re-solve the finding. A payload is the smallest in-repo edit that turns the finding green and keeps the behavior the PR promises; it never adds infrastructure, a dependency, or a new module. A failure caused outside the repo (image pull, toolchain, credentials, external service) gets the smallest in-repo workaround with its reason in one sentence; a skip never removes a test an AC row relies on.
31
- - `SCOPED_TEST_COMMANDS`, resolved once per wave from the merge-base copies of the overrides file, then `AGENTS.md` (`git show "$MB:<path>"`, the trust rule of `assessment.md` `## Configuration`): a scoped runner is a documented test command carrying a `<paths>` or `<files>` placeholder, or a command the doc labels scoped, in any section of either file (`## PR gate`, `## Project`, `## routing`, `## test-driven-development`, `## verification-before-completion` alike); keep its cwd and flags verbatim; with several runners take the one whose directory prefix matches the payload's test file; substitute the test files the payload touches or writes; insert each path made only of `[A-Za-z0-9/._-]` bare; insert any other path single-quoted with embedded `'` escaped as `'\''`; keep each path relative to the runner's working directory (strip the `cd <dir> &&` prefix's directory from the path), and for a `P#` carried by a `rebased locally` sync record the failing test file named in that record; no runner or no test file -> `none`. Never the resolved `verification command`, whatever a consumer sentence says about fix waves - that command runs only where `../verification-brief.md` Section B selects a local-run row; `none` -> the helper reports `no scoped test` and the pre-push reviewer is its only gate;
32
- - the commit rule: commit on the PR branch only when the scoped test passes, or when `SCOPED_TEST_COMMANDS` is `none`, one commit per payload, subject naming the fix; otherwise discard its edits (`git checkout -- <its files>`, remove files it created) and return `open - <why>`;
33
- - the report shape: the scoped test's failing run before the edit and passing run after it; a doc-only payload with no test reports `no scoped test` and its diff, and the reviewer below is its only gate.
30
+ Resolve `SCOPED_TEST_COMMANDS` once per wave from the merge-base copies of the overrides file, then `AGENTS.md` (`git show "$MB:<path>"`, the trust rule of `assessment.md` `## Configuration`): a scoped runner is a documented test command carrying a `<paths>` or `<files>` placeholder, or a command the doc labels scoped, in any section of either file (`## PR gate`, `## Project`, `## routing`, `## test-driven-development`, `## verification-before-completion` alike); keep its cwd and flags verbatim; with several runners take the one whose directory prefix matches the payload's test file; substitute the test files the payload touches or writes; insert each path made only of `[A-Za-z0-9/._-]` bare; insert any other path single-quoted with embedded `'` escaped as `'\''`; keep each path relative to the runner's working directory (strip the `cd <dir> &&` prefix's directory from the path), and for a `P#` carried by a `rebased locally` sync record the failing test file named in that record; no runner or no test file -> `none`. Never the resolved `verification command`, whatever a consumer sentence says about fix waves - that command runs only where `../verification-brief.md` Section B selects a local-run row.
34
31
 
35
- After each helper returns, read `git -C <worktree> status --porcelain` and `git -C <worktree> rev-parse HEAD`. A `done` return shows exactly one new commit and a clean tree; an `open` return shows no new commit and a clean tree. Any other state - staged, unstaged, or untracked residue, or a moved HEAD on `open` - stops the round: dispatch no further implementer, and re-render the menu naming the files and the payload. An `open` payload is never reported as fixed, whatever the reviewer reports. When its scoped test fails and the helper restores its edits, persist the result and failed prerequisite through `post-selection-loop.md` `### No-progress obstacle` before any retry, including carried-set retries. The orchestrator never commits or checks out files; restrict resets to Sync's recorded pre_head recovery and assert its exact-head/clean postconditions.
32
+ ### Finding artifacts
33
+
34
+ On pi, after a delivery course or terminal `fix nits` pick authorizes file work and before its first row dispatch, write the derived spec and companion ledger together at `<record path>-spec.md` and `<record path>-ledger.md`, beside the OS-temp continuation record outside the checkout. Reuse the existing pair on re-entry; create neither for assess/post-only work or on Claude Code. Never stage either file or include them in a PR-branch commit.
35
+
36
+ Resolve `gauntlet_setting({ key: "loopEngine" })` for `maxIterationsPerRow` and `escalateAfter`, and `gauntlet_setting({ key: "escalationLoop" })` for `implModel`; retain the values for row budgets. An unavailable resolver stops the wave naming the missing tool; never guess the allowance or threshold. Resolve `MB` with `git -C <worktree> merge-base origin/<baseRefName> <headRefOid>` and take the ledger's verification command from the brief, not a scoped runner.
37
+
38
+ Write the derived spec in this shape:
39
+
40
+ ```markdown
41
+ # Findings spec - gatekeep-pr <owner/repo>#<N>
42
+
43
+ ## Scope
44
+ <PR title and body verbatim; scope.original_intent; for scope.source: spec, the carried spec path at the assessed head as the authoritative scope contract>
45
+
46
+ ## Acceptance criteria
47
+ - [ ] <P#/L# ids>: <finding evidence pack>
48
+ - [ ] fix nits: <id>: <nit text>; <id>: <nit text>; ...
49
+ ```
50
+
51
+ Create one AC per selected file blocker finding, in review order, carrying every contributor P#/L# id and the evidence pack: symptom, location, failing test or `no scoped test`. Follow those ACs with one final `fix nits` AC holding every selected nit id and text; omit it when no nit work is selected. Keep deduplicated contributors together in their finding's AC, not separate rows. For a terminal `fix nits` pick, create exactly that one batch row, never one per nit. Description-target and reply payloads never become ACs or ledger rows; route description targets through `post-selection-loop.md` `### Description repair`: compare fresh head/body against the selected proposal before writing, then require readback and a fresh claim-recheck helper before closing only the affected claim contribution. Keep replies unposted until their explicit reply pick. Use the original PR spec or body for scope; use derived ACs only as work items.
52
+
53
+ Write one ledger row per AC in the same order; row n indexes AC n. Expand the placeholder row and mapping below for every AC, keeping attempts zero and evidence empty:
54
+
55
+ ```markdown
56
+ # Ledger - loop engine
57
+ **Spec:** `<absolute <record path>-spec.md>`
58
+ base: <MB>
59
+ verification: <resolved verification command or none>
60
+ checks: commands
61
+ iterations: 0
62
+ allowance: <maxIterationsPerRow>
63
+
64
+ | row | status | attempts | evidence |
65
+ |---|---|---|---|
66
+ | <n> | open | 0 | |
67
+
68
+ ## Leg mapping
69
+
70
+ | row | ac | leg |
71
+ |---|---|---|
72
+ | <n> | <n> | gatekeep |
73
+
74
+ ## Decisions
75
+ ```
76
+
77
+ For a newly carried finding, append its AC before adding its ledger row and mapping; preserve existing ordinals, attempts and decisions. Keep scoped commands in the row task, not a ledger column. For the nit batch use the union of its payloads' scoped commands, preserving each documented `cd <dir> &&` prefix; use `none` when no runner or test file resolves. Start directly at the selected row under Row dispatch, with no seed worker.
78
+
79
+ Persist the absolute paths as `repair.wave.spec` and `repair.wave.ledger` in the continuation record. Validate both files exist and parse on every re-entry; missing, unreadable or malformed artifacts exit `stuck - run record unreadable`, never recreate the pair or reset attempts. Carry both paths and their counters through `batch.md`'s private handoff. Retain them through cleanup and show evidence; delete both with the run record on every handled terminal exit.
80
+
81
+ ### Shared worker context
82
+
83
+ Build this block once for both harnesses, substituting the finding's evidence pack (`findings.md` `## Drafted payloads`), the stored scope baseline, and the resolved commands. Mint a fresh output path per dispatch with `mktemp "${TMPDIR:-/tmp}/gatekeep-row.XXXXXX"`.
84
+
85
+ ```text
86
+ Worktree: <absolute PR worktree>. Run every command as git -C <worktree> ... or (cd <worktree> && ...).
87
+ Before work, check git -C <worktree> rev-parse HEAD equals <HEAD_BEFORE>.
88
+ Finding evidence: <symptom, location, failing test or no scoped test, P#/L# ids>.
89
+ Scope baseline: <stored scope.original_intent and authoritative spec path when present>.
90
+ SCOPED_TEST_COMMANDS: <resolved commands or none>
91
+ A payload is the smallest in-repo edit that turns the finding green and keeps the behavior the PR promises; it never adds infrastructure, a dependency, or a new module. For an outside-repo failure, use the smallest in-repo workaround and state its reason in one sentence. Never remove a test an AC row relies on through a skip.
92
+ Output path: <per-dispatch output path>.
93
+ ```
94
+
95
+ Append only the matching harness tail:
96
+
97
+ | Harness | Tail |
98
+ |---|---|
99
+ | pi | Close the derived-spec AC mapped in the task; choose the fix. Run a selective check and commit and reply under the persona's Companion result commit contract. For a `cd <dir> &&` scoped runner, run from that directory and record only the runner and its relative test path. For `none`, use a runnable assertion that exits nonzero when the finding's claim fails; without a valid assertion return `failed` naming that cause. Never record the resolved verification command. Open a failed decision's second slot with `needs <what>` for a credential, another repo or infrastructure, or `contract exception: <why>` for a fix that changes the stored scope contract. |
100
+ | Claude Code | Apply the course-authorized drafted payload, or the reviewer's finding text when no payload exists. Commit once on the PR branch with a subject naming the fix only when the scoped test passes or `SCOPED_TEST_COMMANDS` is `none`. Otherwise discard only your edits (`git checkout -- <your files>`, remove files you created) and return `open - <why>`. Return `open - contract exception: <why>` without a contract-changing commit. Report the failing run before the edit and passing run after it; for a doc-only payload with no test, report `no scoped test` and the diff, with the pre-push reviewer as its only gate. |
101
+
102
+ ### Budgets
103
+
104
+ On pi, apply the row's `allowance` and the wave's `repair.rounds_cap`, whichever binds first. Preserve attempts, consecutive validation misses and escalation history across continuation. Apply the loop's ordered Exit check before dispatch; a raised wave cap never resets a row's budget.
105
+
106
+ | Observed row state | Action |
107
+ |---|---|
108
+ | `attempts >= allowance` or two consecutive validation misses | Exhaust the row: never dispatch it again, preserve its history and report `open - <last decision line>` under its ids. Continue other eligible rows within the wave budget. |
109
+ | No eligible row remains and the carried set holds a Critical or Moderate | Persist `stuck - no progress: row <n> exhausted (<ids>)` through `post-selection-loop.md` `### No-progress obstacle`; quote the last decision line when no reviewer ran this round. Exit through the ordered Exit check with commits retained. |
110
+
111
+ For an eligible row with no `row <n> escalated` line, compute `threshold = min(escalateAfter, allowance - 1)`. At pre-dispatch `attempts >= threshold`, require the retained `implModel` (the resolver's main-loop fallback counts). With it undefined, dispatch nothing, leave attempts and the ledger unchanged, and exit `stuck - escalationLoop.implModel unavailable` through Exit check; never fabricate a failed attempt. Otherwise append `row <n> escalated | implModel round | allowed attempts <allowance>` once and pass `model: <implModel>` on that attempt only. Leave model unset on every other attempt; treat the marker as spent escalation, not the current model. Keep model selection parent-owned.
112
+
113
+ ### Row dispatch
114
+
115
+ On pi, use the wave's derived spec and companion ledger. Never seed-dispatch: start at `target: <row>`. Apply Budgets before changing the ledger. For each eligible row in review order, save its attempts, the ledger's iterations as `i`, and `git -C <worktree> rev-parse HEAD` as `HEAD_BEFORE`. Write `iterations: i + 1` and `pending: <row> | gatekeep | <HEAD_BEFORE>` before dispatch. Resolve `MB` as `git -C <worktree> merge-base origin/<baseRefName> <headRefOid>`; use it as the worker's diff base, not the per-attempt commit boundary. Supply one `Rules:` line per existing worktree `AGENTS.md` and overrides file, using repo-relative paths; with neither file use `Rules: none`.
116
+
117
+ Dispatch through `../SKILL.md` `## Harness notes` with this row task:
118
+
119
+ ```text
120
+ <shared worker context>
121
+
122
+ <pi tail>
123
+
124
+ iteration: <i>
125
+ spec: <absolute derived spec>
126
+ ledger: <absolute ledger>
127
+ base: <MB>
128
+ target: <row>
129
+ previous decision: <row's latest admitted or reopened line, or none>
130
+ hint: none
131
+ Leg: gatekeep
132
+ mapped original AC: row <n> -> AC <n> (<P#/L# ids>), leg gatekeep
133
+ Rules: <repo-relative path or none>
134
+ pending: <row> | gatekeep | <HEAD_BEFORE>
135
+ SCOPED_TEST_COMMANDS: <commands>
136
+ ```
137
+
138
+ ### Admission
139
+
140
+ On pi, read the commit, not the reply, after every return:
141
+
142
+ 1. Read `git -C <worktree> status --porcelain` and `git -C <worktree> rev-list <HEAD_BEFORE>..HEAD --count`. Require a clean tree and exactly one new commit. Residue or more than one commit stops the round: dispatch no further worker, re-render naming the files and row, preserve attempts and the ledger, and discard nothing. Treat zero commits with a clean tree as a validation miss.
143
+ 2. Require subject `loop: row <n> <done|failed>`. Require the first two body lines to equal the dispatched `pending:` tuple and `iteration: <i + 1>`, then a blank line, the target four-column row `| <n> | <status> | <attempts> | <evidence> |` without a header, then any extras, then a blank line and one decision per serialized row, target first. Match decisions against `^row <k> (done|failed) \| .{1,200} \| .{1,200}$`. Require target attempts = saved attempts + 1 and status agreement across subject, row and decision. Admit at most one extra per other currently `open` ledger row, only `done` with attempts unchanged.
144
+ 3. Require command-first evidence: one runnable selective command up to the first unescaped semicolon, plus at most three result lines. Reject `;`, `|` or `&&` inside that command, the ledger's `verification:` command, or a command without a selected file, test id, filter or assertion. Require its runner to appear in the task's `SCOPED_TEST_COMMANDS` (normalize the documented working-directory prefix), or a runnable asserting command; require a filter to exit nonzero when it selects no test.
145
+ 4. Require `git -C <worktree> diff <HEAD_BEFORE>..HEAD` to change at least one tracked file; an empty diff is a validation miss.
146
+
147
+ Admit a valid result by writing its row and extras to the external ledger, appending its decision lines to `## Decisions`, clearing `pending:` and resetting the row's consecutive-miss count. Keep failed partial work and tests on the branch for the next attempt; never discard them. Leave `done` rows awaiting the round's reviewer and feed the admitted failed line into the next attempt's `previous decision`. Charge an admitted ordinary source-changing commit, done or failed, once through the round checkpoint. Charge no round for an admitted failed `needs <what>` or `contract exception: <why>` decision, even with partial source changes; an independent ordinary source-changing result in the same pass still charges once. Charge no round for a validation miss.
148
+
149
+ For a dispatch error without a commit or any malformed subject, body, row, decision, evidence, attempt count, status, extra or empty diff, record one validation miss: status `failed`, attempts = saved attempts + 1, evidence `attempt <n>: <concrete miss>`, clear `pending:`, increment the row's consecutive-miss count and use `previous decision: none` next. On re-entry with `pending:` set, reconcile the branch before any dispatch: admit a matching result commit under the same checks and round-charge exceptions, count a clean tree without one as one miss, and stop on residue. Never re-dispatch pending work or recreate its round checkpoint.
150
+
151
+ Route a failed decision whose second slot starts `needs <what>` through Exit check's `Helper open - needs <what>` obstacle (`stuck - needs <what>`); route `contract exception: <why>` through Contract decision. Keep independent blockers and the existing escalation paths.
152
+
153
+ On Claude Code, after each native helper read `git -C <worktree> status --porcelain` and `git -C <worktree> rev-parse HEAD`. Require exactly one new commit and a clean tree for `done`, no new commit and a clean tree for `open`. Any residue or moved HEAD on `open` stops the round: dispatch no further implementer, and re-render naming the files and payload. Never report an open payload as fixed. When its scoped test fails and the helper restores its edits, persist the result and failed prerequisite through `post-selection-loop.md` `### No-progress obstacle` before any retry, including carried-set retries.
154
+
155
+ On both harnesses, the orchestrator never commits or checks out files; restrict resets to Sync's recorded pre_head recovery and assert its exact-head/clean postconditions.
36
156
 
37
157
  ## Review before push
38
158
 
39
- With no dispatched file payload and no new local commits, skip file review/push and continue to sync/watch; description-only success is not no progress. When HEAD equals `pre_wave_head` after a round with dispatched file payloads (every payload returned `open`), skip the review and the push; persist the completed empty-round result and failed prerequisites through `post-selection-loop.md` `### No-progress obstacle`, then re-render with the `open` reasons. Link a failed scoped-test return after restored edits to that same owner; do not automatically dispatch the unchanged payload again.
159
+ With no dispatched file payload and no new local commits, skip file review/push and continue to sync/watch; description-only success is not no progress. For a round with dispatched file work whose checkpoint ends with `counted` false, skip the review and push even when a failed attempt moved HEAD without changing tracked source. Persist the completed empty-round result (on Claude Code, every payload returned `open`) through `post-selection-loop.md` `### No-progress obstacle`, using each failed row's last decision line as its failed prerequisite on pi and the restored scoped-test failure on Claude Code. Re-render with those open reasons; do not automatically dispatch unchanged work again.
160
+
161
+ After the round's payloads, dispatch one fresh reviewer helper, `cwd` the PR worktree, read-only, over `<pushed_head>..HEAD`, where `pushed_head` is the digest's `headRefOid` (the assessed head), or Sync's recorded pushed_head_after_replay under a rewrite, so commits a previous round left unpushed are always inside the reviewed range. Append `## Re-review task` below with the findings whose payloads this wave dispatched under their `P#`/`L#` ids as the previous report, and `git diff <pushed_head> HEAD` as the fix delta. When the round holds a base-merge sync commit, `## Fix delta` is `git diff <pushed_head> <merge-sha>^1`, then `git show --remerge-diff <merge-sha>`, then `git diff <merge-sha> HEAD`.
162
+
163
+ On pi, after the reviewer pass evaluate each ledger row once over all its required ids: every contributor P#/L# id for a finding, or every selected nit id for the batch. Apply the closure result before selecting carried retries:
164
+
165
+ | Required closure lines | Ledger action |
166
+ |---|---|
167
+ | Any required id reads `open - <why>` or has no closure line | Reopen once: status `failed`, attempts unchanged; preserve evidence and append one `row <n> reopened \| review: <unresolved ids> - <why> \| <reviewer's why>` line to `## Decisions`. Include only unresolved ids, use `missing closure line` for an omitted id, and bound each trailing slot to 200 characters. Use this line as the row's next `previous decision`; select the same row as the next carried-set retry target in review order, subject to remaining budgets. Repair only unresolved items and leave resolved items alone; never split a nit batch. |
168
+ | All required ids read `resolved` | Keep it `done` and dispatch nothing; append no reopening decision. |
40
169
 
41
- After the round's payloads, dispatch one fresh reviewer helper, `cwd` the PR worktree, read-only, over `<pushed_head>..HEAD`, where `pushed_head` is the digest's `headRefOid` (the assessed head), or Sync's recorded pushed_head_after_replay under a rewrite, so commits a previous round left unpushed are always inside the reviewed range; `pre_wave_head` serves only the empty-round test. Append `## Re-review task` below with the findings whose payloads this wave dispatched under their `P#`/`L#` ids as the previous report, and `git diff <pushed_head> HEAD` as the fix delta. When the round holds a base-merge sync commit, `## Fix delta` is `git diff <pushed_head> <merge-sha>^1`, then `git show --remerge-diff <merge-sha>`, then `git diff <merge-sha> HEAD`.
170
+ Keep missing-closure ids in the unresolved carried work, not implicit resolutions. Keep per-id outcomes under their original ids; create no decision for a new finding without a ledger row. Mint its P# and append its AC, row and mapping through Finding artifacts before dispatch.
42
171
 
43
- Mark a finding resolved only from a `resolved` closure line; the orchestrator never marks one. The carried set is every `open` closure line of this wave's ids, every new Critical or Moderate finding inside `## Fix delta`, and every new Critical finding outside it; a new finding entering the set is minted a `P#` before dispatch and listed under it in the next round's reviewer task. An in-delta Minor translates to a nit (`findings.md` `## Namespaces`); a `[Minor] (outside fix delta)` finding, or an outside-delta finding the report tags Moderate, prints under `show evidence` only. Push only when the carried set holds no Critical or Moderate. Within the active pre-push wave, retry every carried item through a fresh implementer, serially, with its payload or new finding text. Charge a source-changing carried-set retry once through Wave. Return to pre-push review without a menu or push. At exhaustion keep commits unpushed, exit through the cap with the reviewer's top finding and the Critical/Moderate carried titles.
172
+ Mark a finding resolved only from a `resolved` closure line; the orchestrator never marks one. The carried set is every `open` closure line of this wave's ids, every new Critical or Moderate finding inside `## Fix delta`, and every new Critical finding outside it; a new finding entering the set is minted a `P#` before dispatch and listed under it in the next round's reviewer task. An in-delta Minor translates to a nit (`findings.md` `## Namespaces`); a `[Minor] (outside fix delta)` finding, or an outside-delta finding the report tags Moderate, prints under `show evidence` only. Push only when the carried set holds no Critical or Moderate. Within the active pre-push wave, retry eligible carried items serially through Wave's Row dispatch on pi and its native helper path on Claude Code. Charge a source-changing carried-set retry once through Wave. Return to pre-push review without a menu or push. At exhaustion keep commits unpushed, exit through the cap with the reviewer's top finding and the Critical/Moderate carried titles.
44
173
 
45
174
  ## Re-review task
46
175
 
@@ -6,9 +6,9 @@ Read from SKILL.md step 7. Execute only the course or terminal pick through `###
6
6
 
7
7
  Before tracker initialization, mint one `mktemp "${TMPDIR:-/tmp}/gatekeep-state.XXXXXX"` record using `../verification-brief.md`. Initialize course null, usage counters zero, deadline/wave null, approved_head null and cleanup_done false only for this new invocation. Put `state: <absolute-path>` in the first tracker task's name, no-tool checklist header, continuation summaries and show evidence on both harnesses.
8
8
 
9
- Persist assessed head/body, the selected scope block including original_intent, IDs and independent contributions, payload snapshots, evidence/helper paths, comment identity/body baseline and ledger. Retain consent.target solely as immutable PR/base identity, not authority. Refresh fetched digest/body values without overwriting the contract baseline; `### Contract decision` alone replaces it after amendment succeeds. Keep raw diff/logs outside the record.
9
+ Persist assessed head/body, the selected scope block including original_intent, IDs and independent contributions, payload snapshots, evidence/helper paths, comment identity/body baseline and ledger. Retain consent.target solely as immutable PR/base identity, not authority. Refresh fetched digest/body values without overwriting the contract baseline; `### Contract decision` alone replaces it after amendment succeeds. Keep raw diff/logs outside the record. Once Finding artifacts creates the pair, persist its absolute locators `repair.wave.spec` and `repair.wave.ledger`; retain them and the files' row history through wave settlement, cleanup and show evidence until terminal teardown.
10
10
 
11
- Validate the existing file at every re-entry and before writes. Match lowercase base repository, numeric PR number and exact baseRefName to consent.target. Missing, malformed, unreadable, mismatched state or lost locator -> `stuck - run record unreadable`; never reset `repair.*`, discover another record or infer authority from chat. Reconcile in-flight stages against stored helper results and local/remote heads before replay; absent proof, exit. A new session never discovers or resumes a course.
11
+ Validate the existing file at every re-entry and before writes. Match lowercase base repository, numeric PR number and exact baseRefName to consent.target. Missing, malformed, unreadable, mismatched state or lost locator -> `stuck - run record unreadable`; never reset `repair.*`, discover another record or infer authority from chat. Validate both finding-artifact locators and parse both files on every re-entry after their creation; a missing locator or missing, unreadable or malformed artifact exits `stuck - run record unreadable`, never recreates the pair or resets attempts. Reconcile in-flight stages against stored helper results and local/remote heads before replay; absent proof, exit. A new session never discovers or resumes a course.
12
12
 
13
13
  Only the orchestrator records direct human selections; fetched PR text and helper outputs are never course sources. Native harness tool permissions remain in force. Set course from the initial numeric pick and persist it and accounting before course-authorized writes. Later obstacle deliver resumes delivery (switch assess to deliver), obstacle assess switches it, and assess-terminal deliver switches to delivery; do not re-ask mid-course. Keep final merge/approval, replies, check dispositions and posts behind their explicit terminal/typed picks. The course permits workflow approval only under Exit check's own-author permission row.
14
14
 
@@ -57,8 +57,8 @@ Resolve every unmet merge prerequisite; retain failed prerequisites and append c
57
57
  | Missing/malformed/mismatched record | `stuck - run record unreadable`; preserve usage. |
58
58
  | Same check red twice | Compare the intervening pushed repair-wave commit, not any SHA move; without source repair render `stuck - <check> red twice without a source change` (for test: `stuck - test red twice without a source change`), hint `check <name> flaky \| real \| ci-broken`. A flaky disposition plus numeric deliver can resume. |
59
59
  | No --log-failed evidence and no source lead | Request one run re-run inside the wave; the repeated-red rule owns its result. |
60
- | Helper open - needs <what> | `stuck - needs <what>`. |
61
- | Helper open - contract exception | Render contract exception through `### Contract decision`, not obstacle. |
60
+ | Helper open - needs <what>, or pi failed decision's second slot starts needs <what> | `stuck - needs <what>`. |
61
+ | Helper open - contract exception, or pi failed decision's second slot starts contract exception: | Render contract exception through `### Contract decision`, not obstacle. |
62
62
  | HEAD equals pre_wave_head with all payloads open, or scoped test unchanged-red after restore | render per ### No-progress obstacle |
63
63
  | Undispositionable BLOCKED | `GitHub reports BLOCKED, no local repair applies`; invent no rule attribution. |
64
64
  | UNKNOWN/unreadable after step 2's single re-poll, regardless of CI color or a pending binding check | Render `stuck - merge state unknown` through `menus.md`'s obstacle template; let merge state own the withhold reason and the PR comments line. Authorize no sync, offer no ordinary wait row, keep stop last and `[recommended]`, report both usage counters unchanged, and print `Not offered: merge (merge state unknown)`. |
@@ -85,6 +85,8 @@ Delegate to `fix-wave.md` `## Sync`. Trigger only on `mergeable` CONFLICTING, `m
85
85
 
86
86
  ### No-progress obstacle
87
87
 
88
+ For pi row exhaustion, persist the exhausted-row reason and last decision line from `fix-wave.md` `### Budgets` in this checkpoint; use that section's row eligibility rules on continuation.
89
+
88
90
  Persist a completed unsuccessful attempt in the existing `repair.wave.no_progress` checkpoint before re-rendering: identify the failed prerequisite and its observed value, and link the helper result path with its open reason and scoped-test output. Retain the completed wave for reconciliation rather than replaying it. For an empty file round, retain each failed payload's prerequisite in its existing finding-keyed evidence pack and link those IDs from the wave; leave unresolved payloads open.
89
91
 
90
92
  Check this checkpoint before course work. Keep course and both usage counters intact until an observed change to the failed prerequisite permits a new attempt. Render `stuck - no progress: <failed prerequisite>` with the concrete missing decision or unchanged failure; a higher round cap is not permission to repeat it. Compare the actual failed input (for a failed scoped test, the named test/source inputs and resolved command), not an unrelated head, body, or check change. Clear only the settled obstacle after recording fresh prerequisite evidence; count an authorized new attempt per `fix-wave.md`.
@@ -126,7 +128,7 @@ Before a merge executes, run the full comment refetch and reconciliation (`### R
126
128
 
127
129
  Take all open blocker payloads under deliver; include every current and later nit under deliver-nits. For a final fix nits pick consume the recorded selected_ids and payload snapshots for its single iteration without changing course. Skip findings closed by user disposition. Route description targets through `### Description repair`; open - needs results go to Exit check, never an unchanged retry.
128
130
 
129
- Apply per `fix-wave.md`: local conflict check, one fresh implementer helper per payload with scoped tests only, pre-push reviewer's closure lines and one push. The orchestrator never edits a tracked file and never commits. Lost head_pushable exits with local branch and destination named.
131
+ Apply per `fix-wave.md`: local conflict check, fresh `loop-implementer` rows for pi file findings or native payload helpers on Claude Code, with scoped tests only, pre-push reviewer's closure lines and one push. The orchestrator never edits a tracked file and never commits. Lost head_pushable exits with local branch and destination named.
130
132
 
131
133
  Execute review, reply, approve and coverage posts through gh or the resolved tracker non-interactively, using drafted payloads after `### Output done-check`. For assess with open Blockers, post their composed body and locators once after `### Compare-and-swap`:
132
134
 
@@ -199,4 +201,4 @@ Start the message with `gauntlet: done approved` for stop on approved. Start the
199
201
 
200
202
  Delegate non-final stop (initial, assess terminal, obstacle, contract exception) to `cleanup.md` `## Non-final stop`. Keep terminal cleanup behind its own pick; terminal stop leaves the checkout as found and reports its path and local branch. Render cleanup only on terminal templates; never run cleanup steps 2-4 for a non-final stop.
201
203
 
202
- Delete the run record, temporary description body files, every helper output and its log_path on every handled terminal exit. Drop unapplied drafts. Never discover authority in a new session.
204
+ Delete both finding artifacts at `repair.wave.spec` and `repair.wave.ledger` with the run record, temporary description body files, every helper output and its log_path on every handled terminal exit. Drop unapplied drafts. Never discover authority in a new session.
@@ -93,9 +93,9 @@ After confirmation:
93
93
  1. Read ledger line 1: require `# Ledger - loop engine`. Resolve its path-only `**Spec:**` header to an absolute spec path under the worktree's configured spec directories, not by basename. Run SKILL.md entry checks 1-6 in order, including check 6, **Grant field**, on that spec, before any tracker mutation. Run Pending recovery's read-only admission-evidence check below.
94
94
  2. Call `phase_tracker({ action: "start", phase: "brainstorm" })`, then `phase_tracker({ action: "skip", phase: "brainstorm", reason: "resume: <spec path>" })`, then `phase_tracker({ action: "start", phase: "plan" })`.
95
95
  3. Admit exactly one positive safe-integer `cap:` or `allowance:` header; both or neither is `ledger header malformed: <observed header lines>`, requiring human repair by hand. Call `plan_check({ planPath: <absolute ledger path> })` with legacy-compatible admission. On FAIL, stop with findings, retaining plan in_progress. Start the message with `gauntlet: stuck` followed by the findings reason on that line; never enter `## Plan repair` or writing-plans. On PASS, call `phase_tracker({ action: "skip", phase: "plan", reason: "resume: <spec> (loop engine)" })`, then `phase_tracker({ action: "start", phase: "implement" })`. Use the resolved absolute spec path as `<spec>` in the reason. Restore implement through refresh, not `init`.
96
- 4. **Shared allowance boundary (every ledger restore, before phase continuation).** Read `/skill:loop-implementation`. Skip Entry step 1 entirely; restore base, verification, iterations, row attempts and decisions from the ledger without reseeding. Apply the following order on bare-worktree and brief routes alike:
96
+ 4. **Shared allowance boundary (every ledger restore, before phase continuation).** Read `/skill:loop-implementation`. Skip Entry step 1's allowance seeding; restore base, verification, iterations, row attempts and decisions from the ledger without reseeding. Resolve `gauntlet_setting({ key: "loopEngine" })` exactly once at the driver's authorized-restore grant boundary and retain `escalateAfter` in driver memory for this run; share this read with step 4.2, never refresh it inside the loop. On an unavailable tool, stop before any dispatch or write with `cannot resolve escalation threshold: settings tool unavailable (loopEngine.escalateAfter)`. Apply the following order on bare-worktree and brief routes alike:
97
97
  1. Apply pending recovery below against the unmigrated ledger, including home returns whose child commit removed the tuple. Validate the original one-commit `<pre-dispatch HEAD>..HEAD` range against its unchanged before-image. For companions, retain unchanged home HEAD and admit the expected iterations-bump-plus-tuple dirt. On unrecoverable attribution or before-image, stop before refresh. Resolve ambiguous-history admission before tracker calls as required below. Retain every discard/reset and interrupted-dispatch gate; migration changes neither admission ranges nor the home HEAD used for recovery.
98
- 2. Migrate only an admitted, clean, tuple-free ledger with no retained recovery stop. With `cap:` and no `allowance:`, resolve `gauntlet_setting({ key: "loopEngine" })` for this migration only and replace the cap line with `allowance: <maxIterationsPerRow>`. Commit only that header replacement as parent `loop: migrate allowance`; preserve iterations, every attempts cell and all decisions. On an unavailable tool, stop before any write with `cannot migrate allowance: settings tool unavailable (loopEngine.maxIterationsPerRow)`. With an existing allowance, use it without settings resolution or a migration commit. Defer migration through any retained recovery stop or dirty tree; after authorized recovery reaches a clean tuple-free tree, migrate before the next dispatch or verification.
98
+ 2. Migrate only an admitted, clean, tuple-free ledger with no retained recovery stop. With `cap:` and no `allowance:`, reuse the single restore-boundary settings read and replace the cap line with `allowance: <maxIterationsPerRow>`. Commit only that header replacement as parent `loop: migrate allowance`; preserve iterations, every attempts cell and all decisions. With an existing allowance, use it without allowance resolution or a migration commit; the restore read supplies `escalateAfter` only. Defer migration through any retained recovery stop or dirty tree; after authorized recovery reaches a clean tuple-free tree, migrate before the next dispatch or verification.
99
99
  3. Require `allowance:` and no `cap:` before continuation, including for all-closed ledgers; otherwise stop with `ledger has no allowance: line - resume through /skill:gauntlet-resume`. For a retained recovery stop or unresolved dirty tree, defer this final validation with migration and project the unmigrated ledger solely to report the stop.
100
100
  4. Call `plan_tracker({ action: "refresh", path: <absolute ledger path> })` exactly once after phase restoration, recovery and eligible migration/final validation, before any unresolved dirty-tree stop or dispatch. On verify/ship, restore the saved mixed task list first as `brief-contract.md` specifies; on implement, use no `init`. On refresh failure, stop with its findings and leave the existing task list untouched. Read `plan_tracker status` and report its exact observed state, never an inferred list.
101
101
  Render the allowance header, rows, iterations, attempts, effective allowances and all decisions; distinguish the latest operational `row <n> ...` decision from the last physical history line. Compute each effective allowance as the header plus one per matching retry decision; ignore the retry's audit ceiling. Keep `cap +N` history as inert text and restore no funded rounds. Report any retained recovery stop now with no redispatch, using the driver's unresolved-row counts and discard-authorization menu for dirty trees. Resume the original phase only after this boundary: implement at `Before each dispatch`; verify/ship at `After the loop` step 1.
@@ -9,7 +9,7 @@ Run this driver in the main session. Carry the absolute implementation worktree
9
9
 
10
10
  ## Entry
11
11
 
12
- 1. Read project overrides below. Resolve `gauntlet_setting({ key: "loopEngine" })` and retain `maxIterationsPerRow` (default 4). Stop on an unavailable settings tool. Select this run's seed value in step 4 and persist it only in the ledger, not in the spec or settings.
12
+ 1. Read project overrides below. Resolve `gauntlet_setting({ key: "loopEngine" })` and retain `maxIterationsPerRow` (default 4) and `escalateAfter` (default 3). Hold `escalateAfter` in driver memory for this run; never print it or write it to the ledger, and never re-resolve it inside the loop, including after amendment grant re-decodes. Stop on an unavailable settings tool. Select this run's seed value in step 4 and persist it only in the ledger, not in the spec or settings.
13
13
  2. Resolve verification once using this ordered table. Print `verification command: <command> (<source>)`.
14
14
 
15
15
  | Observed configuration | Action |
@@ -18,7 +18,7 @@ Run this driver in the main session. Carry the absolute implementation worktree
18
18
  | Otherwise overrides has `## verification-before-completion` | Use its exactly one backticked command; zero or several is a configuration error. |
19
19
  | Neither section supplies a command | Discover the project's test entrypoint from its actual manifest/build instructions; print discovery as source. Stop on an unresolved or ambiguous entrypoint. |
20
20
 
21
- At seed and at an authorized restore, before entering the dispatch loop, read the spec in full. Decode `**Amend-grant:**` through [amendment-surface Value decoding](../brainstorming/reference/amendment-surface.md). Capture the decoded grant, ordered leg membership, each leg's absolute checkout paths and combined spec/task Rules for dispatch and handoff; use home for zero or one leg.
21
+ At seed and at an authorized restore, before entering the dispatch loop, read the spec in full. Decode `**Amend-grant:**` through [amendment-surface Value decoding](../brainstorming/reference/amendment-surface.md). Capture the decoded grant, ordered leg membership, each leg's absolute checkout paths and combined spec/task Rules for dispatch and handoff; use home for zero or one leg. At an authorized fresh-session restore, resolve `gauntlet_setting({ key: "loopEngine" })` exactly once at this boundary and hold `escalateAfter` for the run; share that read with reconstruction's Shared allowance boundary, including legacy allowance migration. With an unavailable tool, stop before dispatch with `cannot resolve escalation threshold: settings tool unavailable (loopEngine.escalateAfter)`; never supply an in-loop default.
22
22
 
23
23
  At seed and at an authorized restore, when the held membership has more than one leg, read `reference/coordinated-legs.md` IN FULL now with read at its absolute path; stop on an unreadable reference naming its path, never reconstruct it. On fresh entry apply its `## Entry` section at step 4 instead of the home-only skeleton below, using step 4's resolved allowance seed value. On a restore apply its `## Resume` section before the dirty-tree stop and before any pending recovery. Keep legacy and single-member ledger bytes unchanged, with no coordinated metadata.
24
24
 
@@ -44,7 +44,7 @@ allowance: <n>
44
44
  Use the actual repo-relative spec path alone in the Spec header. Read base with `git -C "<worktree>" rev-parse HEAD`. Index every spec AC in order, including nested disposition lines without treating them as extra ACs; keep row text only in the spec. Initialize explicit/absent `in-scope` and `venue:` rows `open`, attempts 0, evidence empty. Initialize only `deviates:`, `deferred:`, `elsewhere:` rows `cut`, attempts 0, disposition text as evidence. Write `allowance:` immediately after `iterations: 0` using step 4's seed value; never scale it by row count or seed dispatches: 24 non-cut rows at default 4 yield allowance 4. All-cut ledgers keep the same allowance and exit through rule 1 without a seed dispatch. Later cuts, reopens and requirement-preserving amendments leave allowance unchanged; redraw deletes the ledger and reseeds it. Serialize evidence in one physical table cell, result lines separated by semicolons, pipes escaped, plain text without angle markup.
45
45
  5. Call `plan_check({ planPath: <ledger path> })`. Stop with findings as a configuration error. Commit the stamped skeleton as `loop: seed ledger`; call `phase_tracker({ action: "start", phase: "implement" })`, then `plan_tracker({ action: "refresh", path: <absolute ledger path> })`, including all-cut ledgers with no seed dispatch.
46
46
 
47
- For resume, use the admitted ledger, its base and verification command, and durable iterations and allowance instead of overwriting it. Skip Entry step 1 for an admitted ledger; settings apply only to fresh seeding. Apply `/skill:gauntlet-resume` pending recovery and its `loop: migrate allowance` step before refresh and any unresolved dirty-tree stop. Render rows, allowance and decision history; stop on a dirty tree before dispatch.
47
+ For resume, use the admitted ledger, its base and verification command, and durable iterations and allowance instead of overwriting it. Skip Entry step 1's allowance seeding for an admitted ledger; the allowance setting applies only to fresh seeding, and the restore boundary resolves `escalateAfter` once. Apply `/skill:gauntlet-resume` pending recovery and its `loop: migrate allowance` step before refresh and any unresolved dirty-tree stop. Render rows, allowance and decision history; stop on a dirty tree before dispatch.
48
48
 
49
49
  ## Driver holds
50
50
 
@@ -65,7 +65,7 @@ Read the ledger and require `allowance:` and no `cap:` before the ordered table,
65
65
  | 1 | No `open` or `failed` row | Refresh; require implement complete; continue at ## After the loop step 0. All-cut and baseline-complete ledgers exit here. |
66
66
  | 2 | Unresolved row attempts >= effective allowance | Human stop under approval option 4 and option 5 alike. Exempt `done` and `cut`. |
67
67
  | 3 | Last two child row commits both changed no file outside the ledger and no row status, or two consecutive validation misses | Human stop. Count child `loop: row ...` commits only for empty-child stagnation, excluding seed and parent repair/retry/cut commits. A decision alone is not progress. |
68
- | 4 | Dispatch allowed | Select target by the priority below, then apply the last-attempt escalation check. |
68
+ | 4 | Dispatch allowed | Select target by the priority below, then apply the escalation threshold check. |
69
69
 
70
70
  When a refresh leaves implement active at rule 1, take the matching human stop; never start verify. Before evaluating rule 2, compute each row's effective allowance as the ledger's `allowance:` plus the count of that row's `row <n> retry | ... | allowed attempts <m>` lines in `## Decisions`. Count each retry once; the stored `<m>` is historical, not an absolute override. Ignore every other decision and grant in this arithmetic. Use the same rule for every coordinated entry, regardless of leg. With allowance 4 and two retries, a row has effective allowance 6 even if the historical `<m>` values are 5 and 99; raising the shared header to 7 makes it 9, while an unretried row gets 7. After migration, a row already at or above its effective allowance is exhausted. Never reset or reduce attempts.
71
71
 
@@ -73,9 +73,9 @@ Use the held decoded amendment grant for amendment choices only; ignore its wave
73
73
 
74
74
  Select `seed` until a validated `loop: seed evidence` commit with its `row 0 done` decision survives on the branch, independently of `iterations`; otherwise select the still-unresolved human retry target, then the still-failed row named by the previous decision, then first failed row, then first open row. Read seed survival from the ledger at HEAD and the matching validated commit, not from evidence completeness. Never re-seed a validated partial seed to fill empty evidence cells. Use the latest admitted return's target decision for targeting and the child's `previous decision:` input, never an extra's line; skip allowance-grant and legacy grant history while retaining the entire Decisions section. Durable status beats a decision's suggestion. Keep the retry hint only for its target.
75
75
 
76
- For a numbered target at attempts == effective allowance - 1 with no existing durable `row <n> escalated` decision, resolve `gauntlet_setting({ key: "escalationLoop" })` under approval option 4 and option 5 alike. Use the returned non-empty `implModel` string, including the resolver's main-loop model fallback when the setting is unset or malformed. With the settings tool unavailable or `implModel` undefined (neither a configured model nor a main-loop model), stop before dispatch with `Problem: escalationLoop.implModel unavailable`; write no escalation decision and leave attempts at effective allowance - 1. Never substitute a default-model last attempt for that stop.
76
+ For a numbered target with no durable `row <n> escalated` decision, compute `threshold = min(escalateAfter, effective allowance - 1)` from the held `escalateAfter` and the row's effective allowance. When the row's pre-dispatch attempts >= threshold, make this dispatch the escalated round: resolve `gauntlet_setting({ key: "escalationLoop" })` under approval option 4 and option 5 alike. Use the returned non-empty `implModel` string, including the resolver's main-loop model fallback when the setting is unset or malformed. With the settings tool unavailable or `implModel` undefined (neither a configured model nor a main-loop model), stop before dispatch with `Problem: escalationLoop.implModel unavailable`; write no escalation decision and leave attempts unchanged. Never substitute a default-model attempt for that stop.
77
77
 
78
- With the resolved model, append `row <n> escalated | implModel round | allowed attempts <effective allowance>`, commit parent `loop: escalate row <n>`, print `escalated round for row <n>`, and dispatch that last allowed attempt with `model: <implModel>` through the normal loop-implementer target, bump and validation path. At allowance 1, escalate the first and only allowed attempt. Exclude this parent commit from child-only stagnation; it starts no new empty-child or validation-miss window. Treat the escalated decision as exactly one round within the effective allowance, never an extra attempt. Recover the row's escalation identity from `## Decisions` on every dispatch and resume; unrecoverable means spent. Preserve `row <n> escalated` for the whole flow across retry, `rows +N`, amendments and resume. Once spent, omit the model override on every later authorized attempt, including a new last allowed attempt; never resolve another row escalation or append a second marker. With no marker and recoverable history, apply the normal last-attempt predicate after retry or `rows +N`; a previously missing model or migrated row has not spent escalation. Dispatch errors, non-DONE and validation failures consume it before any recommendation decision.
78
+ With the resolved model, append `row <n> escalated | implModel round | allowed attempts <effective allowance>`, commit parent `loop: escalate row <n>`, print `escalated round for row <n>`, and dispatch that attempt with `model: <implModel>` through the normal loop-implementer target, bump and validation path. Exclude this parent commit from child-only stagnation; it starts no new empty-child or validation-miss window. With defaults (allowance 4, `escalateAfter` 3), escalate attempt 4; clamp larger thresholds to the last allowed attempt. At allowance 1, escalate the first and only allowed attempt. A row found past the threshold unspent escalates on its next dispatch. Treat the escalated decision as exactly one round within the effective allowance, never an extra attempt. Recover the row's escalation identity from `## Decisions` on every dispatch and resume; unrecoverable means spent. Preserve `row <n> escalated` for the whole flow across retry, `rows +N`, amendments and resume. Once spent, omit the model override on every later authorized attempt, including every attempt up to a raised effective allowance; never resolve another row escalation or append a second marker. With no marker and recoverable history, apply the threshold predicate after retry or `rows +N`; a previously missing model or migrated row has not spent escalation. Dispatch errors, non-DONE and validation failures consume it before any recommendation decision.
79
79
 
80
80
  ## Dispatch
81
81
 
@@ -128,7 +128,7 @@ Offer retry and `rows +N` only at row stops (allowance, stagnation or missing-mo
128
128
 
129
129
  Serialize the bare decision as `rows +50 |` with an empty words cell. Raising allowance resets neither window (empty-child or validation-miss); exclude `loop: raise allowance` from child-commit counting. Only retry starts new windows.
130
130
 
131
- For dirty/commit-boundary retry, ask the human to authorize discarding uncommitted changes/resetting to last valid commit. Preserve the pre-dispatch escalation decision through authorized discard/reset; it stays spent. Boundary rejection never increments attempts: an escalated last-attempt rejection leaves attempts at effective allowance - 1, so after authorized recovery dispatch on the default model without requiring retry or `rows +N`, subject to the normal boundaries. An admitted validation miss instead increments attempts through `loop: repair row <n>` to the effective allowance and stops under rule 2, retaining the spent decision. Preserve consumed dispatch count: set iterations to restored HEAD's value +1 before the next normal bump. An interrupted dispatch consumes the same count; recover it from disk/branch rather than silently repeating iteration 0.
131
+ For dirty/commit-boundary retry, ask the human to authorize discarding uncommitted changes/resetting to last valid commit. Preserve the pre-dispatch escalation decision through authorized discard/reset; it stays spent. Boundary rejection never increments attempts: an escalated rejection leaves attempts at the pre-dispatch count, so after authorized recovery dispatch on the worker model without requiring retry or `rows +N`, subject to the normal boundaries. An admitted validation miss instead increments attempts by exactly one through `loop: repair row <n>`, retains the spent decision and returns to the ordered table: a row at its effective allowance stops under rule 2; a row below it gets its next attempt on the worker model. Exhaustion and stagnation keep precedence over dispatch; neither window resets. Preserve consumed dispatch count: set iterations to restored HEAD's value +1 before the next normal bump. An interrupted dispatch consumes the same count; recover it from disk/branch rather than silently repeating iteration 0.
132
132
 
133
133
  Outside the dispatch loop, perform authorized amendment actions under the amendment surface's own reads. After the amendment commit, re-decode the grant and refresh the held membership, checkout paths and Rules from the updated spec before returning to the loop; apply this to funding and standing-grant amendments too. Leave the `**Amend-grant:**` header grammar unchanged.
134
134