pi-gauntlet 8.1.7 → 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,11 @@
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
+
3
9
  ## v8.1.7 - 2026-11-02
4
10
 
5
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)
@@ -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
 
@@ -1541,37 +1541,38 @@ const verifyBranch = (extra: unknown[] = []) => [
1541
1541
 
1542
1542
  const subagentCall = (id: string, input: unknown) => ({ toolName: "subagent", toolCallId: id, input });
1543
1543
  const loneImplementer = (extra: Record<string, unknown> = {}) => ({ agent: "implementer", task: "fix G1", ...extra });
1544
- const implementerWave = (n = 1) => ({
1545
- 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 })),
1546
1546
  });
1547
1547
 
1548
1548
  const firstCallResult = async (h: ReturnType<typeof harness>, id: string, input: unknown) =>
1549
1549
  (await h.emitEvent("tool_call", subagentCall(id, input)))[0] as { block?: boolean; reason?: string } | undefined;
1550
1550
 
1551
- 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 () => {
1552
1553
  for (const settings of [
1553
1554
  { piGauntlet: { closureReview: { enforce: true } } },
1554
1555
  { piGauntlet: { closureReview: { enforce: true, model: "x/y" } } },
1555
1556
  ]) {
1556
1557
  const h = harness({ cwd: tempCwd(settings), branch: verifyBranch([subagentResult(["conformance-reviewer"])]) });
1557
1558
  await h.emit("session_start");
1558
- const sync = await firstCallResult(h, "s1", loneImplementer());
1559
+ const sync = await firstCallResult(h, "s1", loneImplementer({ agent }));
1559
1560
  assert.equal(sync?.block, true);
1560
1561
  assert.match(sync?.reason ?? "", /dispatch implementers as a one-task tasks wave/);
1561
1562
  assert.match(sync?.reason ?? "", /piGauntlet\.closureReview\.enforce: false/);
1562
- const async = await firstCallResult(h, "s2", loneImplementer({ async: true }));
1563
+ const async = await firstCallResult(h, "s2", loneImplementer({ agent, async: true }));
1563
1564
  assert.equal(async?.block, true);
1564
1565
  }
1565
1566
  });
1566
1567
 
1567
- 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 () => {
1568
1569
  const preLatch = harness({ branch: verifyBranch() });
1569
1570
  await preLatch.emit("session_start");
1570
- assert.equal(await firstCallResult(preLatch, "s1", loneImplementer()), undefined);
1571
+ assert.equal(await firstCallResult(preLatch, "s1", loneImplementer({ agent })), undefined);
1571
1572
 
1572
1573
  const implement = harness({ branch: implementBranch([subagentResult(["conformance-reviewer"])]) });
1573
1574
  await implement.emit("session_start");
1574
- assert.equal(await firstCallResult(implement, "s1", loneImplementer()), undefined);
1575
+ assert.equal(await firstCallResult(implement, "s1", loneImplementer({ agent })), undefined);
1575
1576
 
1576
1577
  const ship = harness({
1577
1578
  branch: verifyBranch([
@@ -1581,12 +1582,13 @@ test("shape guard: lone implementer passes before the audit, in implement, in sh
1581
1582
  ]),
1582
1583
  });
1583
1584
  await ship.emit("session_start");
1584
- assert.equal(await firstCallResult(ship, "s1", loneImplementer()), undefined);
1585
+ assert.equal(await firstCallResult(ship, "s1", loneImplementer({ agent })), undefined);
1585
1586
 
1586
1587
  const mgmt = harness({ branch: verifyBranch([subagentResult(["conformance-reviewer"])]) });
1587
1588
  await mgmt.emit("session_start");
1588
- assert.equal(await firstCallResult(mgmt, "s1", { action: "status", agent: "implementer" }), undefined);
1589
+ assert.equal(await firstCallResult(mgmt, "s1", { action: "status", agent }), undefined);
1589
1590
  });
1591
+ }
1590
1592
 
1591
1593
  test("shape guard: dormant when the flow was never entered (cold start verify) and when closureReview.enforce is false", async () => {
1592
1594
  const cold = harness({
@@ -1713,19 +1715,21 @@ test("interrupted implementer waves still spend rounds and granted credits and n
1713
1715
  const guardedHarness = (settings: unknown, extra: unknown[] = []) =>
1714
1716
  harness({ cwd: tempCwd(settings), branch: verifyBranch([subagentResult(["conformance-reviewer"]), ...extra]) });
1715
1717
 
1716
- 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 () => {
1717
1720
  const h = guardedHarness({ piGauntlet: { closureReview: { enforce: true } } });
1718
1721
  await h.emit("session_start");
1719
1722
  for (let i = 1; i <= 5; i++) {
1720
- assert.equal(await firstCallResult(h, `c${i}`, implementerWave(2)), undefined, `round ${i} passes`);
1721
- 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 }]));
1722
1725
  }
1723
- const blocked = await firstCallResult(h, "c6", implementerWave(1));
1726
+ const blocked = await firstCallResult(h, "c6", implementerWave(1, agent));
1724
1727
  assert.equal(blocked?.block, true);
1725
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/);
1726
1729
  assert.ok((blocked?.reason ?? "").includes("Start the escalation message with the line: gauntlet: gate=verification-before-completion/step-6"));
1727
1730
  assert.doesNotMatch(blocked?.reason ?? "", /^gauntlet:/);
1728
1731
  });
1732
+ }
1729
1733
 
1730
1734
  test("cap guard: maxFixRounds 0 blocks the first wave; 1 blocks the second; a chain implementer is cap-checked and counted", async () => {
1731
1735
  const zero = guardedHarness({ piGauntlet: { closureReview: { maxFixRounds: 0 } } });
@@ -1849,13 +1853,14 @@ test("counter reset: start implement and reset zero it; start verify --force kee
1849
1853
  assert.equal(await firstCallResult(reset, "c1", loneImplementer()), undefined, "dormant after reset");
1850
1854
  });
1851
1855
 
1852
- 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 () => {
1853
1858
  const inVerify = guardedHarness({ piGauntlet: { closureReview: { maxFixRounds: 2 } } }, [
1854
- subagentResult(["implementer"]),
1855
- subagentResult(["implementer", "wave-reviewer"]),
1859
+ subagentResult([agent]),
1860
+ subagentResult([agent, "wave-reviewer"]),
1856
1861
  ]);
1857
1862
  await inVerify.emit("session_start");
1858
- assert.equal((await firstCallResult(inVerify, "c1", implementerWave(1)))?.block, true);
1863
+ assert.equal((await firstCallResult(inVerify, "c1", implementerWave(1, agent)))?.block, true);
1859
1864
 
1860
1865
  const inShip = harness({
1861
1866
  cwd: tempCwd({ piGauntlet: { closureReview: { maxFixRounds: 2 } } }),
@@ -1863,14 +1868,15 @@ test("replay: two implementer waves after the audit in verify restore fixRounds
1863
1868
  subagentResult(["conformance-reviewer"]),
1864
1869
  phaseResult("complete", phases({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete" })),
1865
1870
  phaseResult("start", phases({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "complete", ship: "in_progress" })),
1866
- subagentResult(["implementer"]),
1867
- subagentResult(["implementer"]),
1871
+ subagentResult([agent]),
1872
+ subagentResult([agent]),
1868
1873
  phaseResult("start", phases({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "in_progress", ship: "in_progress" })),
1869
1874
  ]),
1870
1875
  });
1871
1876
  await inShip.emit("session_start");
1872
- assert.equal(await firstCallResult(inShip, "c1", implementerWave(1)), undefined);
1877
+ assert.equal(await firstCallResult(inShip, "c1", implementerWave(1, agent)), undefined);
1873
1878
  });
1879
+ }
1874
1880
 
1875
1881
  // --- Human overrule of the fix-round cap (spec 2026-09-14-fix-round-human-overrule) ---
1876
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.7",
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.