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 +6 -0
- package/agents/pr-gatekeeper.md +1 -1
- package/extensions/phase-tracker.test.ts +27 -21
- package/extensions/phase-tracker.ts +12 -9
- package/package.json +1 -1
- package/skills/gatekeep-pr/SKILL.md +3 -1
- package/skills/gatekeep-pr/reference/batch.md +2 -2
- package/skills/gatekeep-pr/reference/findings.md +4 -4
- package/skills/gatekeep-pr/reference/fix-wave.md +140 -11
- package/skills/gatekeep-pr/reference/post-selection-loop.md +8 -6
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)
|
package/agents/pr-gatekeeper.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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([
|
|
1855
|
-
subagentResult([
|
|
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([
|
|
1867
|
-
subagentResult([
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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,
|
|
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
|
@@ -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
|
|
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
|
|
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.
|
|
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;
|
|
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.
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|
|
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.
|