pi-gauntlet 8.1.2 → 8.1.3

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v8.1.3 - 2026-11-01
4
+
5
+ - The loop engine runs on one per-row budget: a fresh ledger carries `allowance: <maxIterationsPerRow>` (default 4) in place of `cap:`, a `4; allowance N` or `5; allowance N` approval reply overrides the setting, and each `retry <n> | <hint>` decision adds one attempt to that row. The driver drops funded rounds, `cap +N`, the total-iteration cap and amend-gated escalation; a row's last allowed attempt runs once on `escalationLoop.implModel` under option 4 and option 5 alike, and exit-verification failure gets one escalated repair per flow. Human stops offer only `retry <n> | <hint>` and `rows +N` and print each open row as `<attempts> of <allowance>`; the task tracker labels rows `<attempts>/<allowance>: <criterion text>` while task names stay `row N`. A legacy `cap:` ledger migrates on resume through a parent `loop: migrate allowance` commit. (#108)
6
+
3
7
  ## v8.1.2 - 2026-11-01
4
8
 
5
9
  - `gatekeep-pr` selects the viewer's newest submitted, non-dismissed review and, when its commit is an ancestor of the assessed head, sends the reviewer its posted items and only the intervening code delta. The report counts addressed items; contract checks still judge the whole head. Assess pins its review to the assessed commit and checks the response; own-PR comment reviews supply the same bounded baseline.
package/README.md CHANGED
@@ -39,7 +39,7 @@ Concretely, one change through the gauntlet:
39
39
  1. Opt in with `/skill:brainstorming` and describe the change. The command is the reliable discovery-and-entry path; explicit prose requesting brainstorming/the gauntlet also supplies consent when the skill is already loaded or an available instruction or route identifies it. An ordinary implementation request or an agent recommendation is not consent. A leading run of GitHub references (`#N`, `owner/repo#N`, or a `/pull/N` or `/issues/N` URL), separated by whitespace or commas, resolves each reference as a PR first: related open, same-repository, non-fork PRs on one base form one rework subject, including drafts. Admission runs in listed order and relatedness is checked before provisioning; the first retained PR is home, whose existing branch rebases onto the fetched PR base by default, then member commits replay onto it in listed order. The first intent word `merge-forward` opts out and keeps the merge path. An unresolved conflict after its scoped helper aborts that step and continues to the spec with the repair as Task 1. A single non-PR reference follows the ticket path. The first non-reference token starts intent text; mid-sentence references and other ticket/free-text subjects keep their existing paths. The combined code, PR-carried specs, claims, and review threads are the brainstorm input, with each PR's provenance retained; only home's bound ticket supplies acceptance criteria, while member tickets supply context. A single PR retains the same rework path without member replays or a close offer. The combined state is judged against constraints from the fetched current base HEAD, not the fork point or the PRs' proposed policy edits; see the [PR-subject contract](skills/brainstorming/reference/pr-subject.md) and [PR-head worktree owner](skills/using-git-worktrees/reference/pr-head.md). **`brainstorming`** sets up an isolated worktree (on the existing branch for PR rework), explores the codebase, and turns the subject into a written spec - when its recon finds a cited case that the ask is the wrong change (misdiagnosed, wrong-sized, already solved, a band-aid, or cutting across a sound pattern - including a repo convention that is itself debt) and can name the alternative, question one is a framing question (`as framed`, `pivot: <alternative>`, or `smaller or none`); otherwise it opens with one sentence naming what it checked, and every approach it proposes ends with a `Pattern:` line naming what it follows or cuts across; when the run starts from a ticket, the spec carries the ticket's acceptance criteria verbatim, each with a disposition (`in-scope`, `deviates:`, `deferred:`, `venue:`, `elsewhere:` for a row another repo's part delivers), scope cuts come only from your answer to a cut question and are listed at the gate, the conformance gate checks the in-scope ones, and `/skill:check-delivery` verifies `venue:` rows before the prod deploy. A multi-model critique runs on it automatically. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; candidates come from brainstorming's scout recon, never a mechanical sweep). **You read and approve the spec - human gate 1.** Options 1/2/4/5 approve and pick an engine; option 6 approves the content only: it writes `**Engine:** deferred` under `**Amend-grant:** none`, pushes the branch, opens a draft PR carrying just the spec, prints the URL, and ends the flow - no plan, no loop, no handoff brief. For a coordinated spec, it first pushes every companion branch to origin, then opens one home draft PR; it creates no companion PRs. A later `brainstorming #N` reconstructs the recorded companions from their origin branches before re-presenting the implementation gate; the [publication and re-gate owner](skills/brainstorming/reference/spec-pr.md) defines all-member preflight, recovery, and cleanup. The branch stays; a gauntlet-owned worktree is removed, a host-owned checkout is reported and left. Option 6 is withheld on a PR subject (`Withheld: draft PR - #N already exists`) and when the branch's diff against the default branch is not spec-only. A later `brainstorming #N` on that PR classifies it from the diff alone: one added spec carrying the marker plus banner-only predecessor edits re-presents the gate with 1/2/4/5 (3 only when the spec commit carries a council audit) and skips gather, questionary, council, and scout; a spec-only diff without the marker prints `spec-only PR without the gate marker - running lint and council` and takes the normal brainstorm path with the carried spec as the draft; any other diff takes the rework path. Approval at that re-gate writes `**Engine:**`, `**Amend-grant:**`, and `**PR:** <owner/repo#N>` as a new commit on the PR branch and chains into planning or the loop as usual. Before approval, PR rework's sole write/run exception outside the spec directories permits only the base sync (rebase or merge), each member replay or merge, their conflict helpers' edits to the conflicted files, and their scoped conflict tests; other subjects have no implementation code yet.
40
40
  2. By default, **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files. Experimental approval options 4/5 instead select **`loop-implementation`**: the plan phase is skipped and a stamped acceptance-criteria ledger replaces the task plan. Option 4 retains amendment review; option 5 grants auto-apply amends, with redraws still stopping. See [Loop engine operations](doc/configuration.md#loop-engine-operations).
41
41
  3. **`subagent-driven-development`** executes the plan in waves, with task implementers followed by one integrated `wave-reviewer` after the scoped test gate passes. The reviewer checks anchored spec clauses and code quality together; repairs stay on the current tree and pass the same gate before re-review. TDD-locked: red, green, refactor. Independent implementation, review, and council batches remain parallel, but their dispatches are foreground: the orchestrator waits for terminal results before accepting work or advancing a phase.
42
- The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. Stagnation stops for a human decision. Under amend permission, row-allowance exhaustion and a failed verify-step gate each get one bounded escalated-model round within the existing cap. Ledger refresh completes implement when every row is closed; verify step 1 runs the gate set: deduplicated (owning checkout, command) pairs from done rows' recorded checks, with ownership from the ledger. Gate output goes to logs; the driver keeps exit codes and paths, not test output. CI on push owns the full run.
42
+ The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. Each row starts with the shared allowance (default 4); approval replies such as `4; allowance 6` override it for one run. Under either loop approval option, the last allowed attempt escalates once per row on `escalationLoop.implModel` (the main-loop model when unset); one exit-verification repair per flow uses the same model. Exhaustion and stagnation stop for a human decision: `retry <n> | <hint>` adds one attempt to that row, while `rows +N` raises every row's allowance. Attempts never reset and there is no total iteration cap. Ledger refresh completes implement when every row is closed; verify step 1 runs the gate set: deduplicated (owning checkout, command) pairs from done rows' recorded checks, with ownership from the ledger. Gate output goes to logs; the driver keeps exit codes and paths, not test output. CI on push owns the full run.
43
43
 
44
44
  4. **verify**: the parent first runs the plan's full verification command set for waves, or the ledger gate set for loop; only a passing result permits whole-diff code review, then the **conformance gate**. A review fix invalidates that result, so the parent reruns the same set before the next review or conformance gate. Loop conformance repairs and finish also use the gate set, never the ledger's full `verification:` command. When the overrides file declares a happy path for the touched paths (see [Project-specific overrides](#project-specific-overrides)), the parent runs it once here, between code review and the conformance gate, and the reviewer reads its transcript as runtime evidence - a hang or a failed request shows up in the audit with the transcript quoted - as a gap when an origin clause ties to it, otherwise as a `happy-path: failed - unattributable` line - never as a silent green. The conformance subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. Unauthorized rows - shipped surface no human input asked for, whether it crept in or was laundered through the spec - follow their recommendation like every other row: a contained removal auto-runs, anything another requirement leans on is deferred with a plain-language "I'd cut it / I'd keep it" recommendation. Inside a brainstorming-entered flow this gate is machine-blocked from being skipped. Compatible executable recommendations auto-run through an isolated fix-and-re-audit loop with no prompt; anything still open surfaces as a numbered decision brief - one item per decision, the problem in plain words first, lettered options each with a one-line consequence, and `Recommended: <letter> - <reason>` or `No recommendation - pick one`. Reply with one line per item (`1. B`), or bare `1` to take every recommendation (no-recommendation items are re-asked by number); a current `CONFORMS` / no-concerns result goes straight to the branch options with no extra conformance sign-off.
45
45
  5. **`finishing-a-development-branch`**: PR, draft PR, squash, keep, or discard; PR rework's row 1 offers a lease push of the rebased home branch to the existing PR (`Base:` records in `merge:` form push non-force). A spec whose header carries `**PR:**` (approved at a spec-PR re-gate) and no `## Rework record` replaces the two create rows with `Push to PR #N and mark it ready for review` and `Push to PR #N, keep it a draft` (`Push to PR #N (already ready for review)` when the PR is already ready): both push without force after strip-and-seal, the first then runs `gh pr ready`, neither rewrites the PR body, and both are withheld when origin's branch head is no longer an ancestor of local HEAD. For coordinated specs, these [header-linked rows 1/2](skills/finishing-a-development-branch/reference/spec-pr.md#push-rows-12) deliver companions in recorded order before one home seal carrying every companion PR URL, then push home and apply its readiness choice. Row 1 makes companion drafts ready too; row 2 creates drafts but never converts an already-ready PR back to draft. Companion URLs appear in the shipped summary, companions first and home last, not in the unchanged home PR body; keep that summary for cross-repo review because successful home delivery removes receipts. A spec still carrying `**Engine:** deferred` stops finishing (and `gauntlet-resume`) with `spec approved for handoff only - run brainstorming #N` before any phase change, verification, or push. The PR-rework push appends a `## Rework` block naming absorbed PRs while retaining the original description bytes; the header-linked `**PR:**` push leaves the body untouched. Only after a successful rework push and home-description edit does one gated confirmation show the exact close comments and eligible member targets, linking home; declining or any other finish option leaves members open. Closing never deletes member branches, and finish posts no review-thread replies. Its `Before:` handle includes the full portable pre-rework SHA and the local `refs/gauntlet/pre-rework/pr-<N>` ref in the rework checkout. The [finishing owner](skills/finishing-a-development-branch/SKILL.md#pr-subject-row-1-push-to-the-pr-branch) defines the landing and body-update contract. Once a PR exists, run `/skill:gatekeep-pr <pr>` to verify it before merging - it judges the diff against the spec at the PR's head (the acceptance-criteria dispositions and Design you approved; the PR description when no spec applies) - rows the spec defers or deviates are settled, every other row needs its mechanism (code, tests, docs) - and against the rubric; whatever can be seen only after merge is left to step 6. **Human gate 2** - the only other decision you make.
@@ -600,7 +600,7 @@ Anything a wrapper skill contains beyond trigger phrases is misplaced - move it
600
600
 
601
601
  Settled skill-owned turns carry a machine-readable first line identifying a gate, question, or terminal outcome. Programs answering these turns use the [gate inventory and reply contract](doc/gate-contract.md); the rendered row bodies define valid replies.
602
602
 
603
- Choose experimental loop options 4/5, configure the iteration cap and worker model, and operate the planless ledger through [Loop engine operations](doc/configuration.md#loop-engine-operations). Waves remains the default.
603
+ Choose experimental loop options 4/5, configure the per-row allowance and worker model, and operate the planless ledger through [Loop engine operations](doc/configuration.md#loop-engine-operations). The menu shows the resolved allowance; `; allowance N` overrides it for one run. Resume preserves an existing allowance or migrates a legacy cap after pending recovery. Waves remains the default.
604
604
 
605
605
  | Want to ... | Read |
606
606
  |---|---|
@@ -615,7 +615,8 @@ The conformance gate's model, the spec council's roster, and the phase-tracker's
615
615
  | `closureReview.enforce` | phase-tracker | `true` | conformance gate on/off - [detail](./doc/configuration.md#conformance-gate-model) |
616
616
  | `closureReview.model` | phase-tracker | unset (inherits) | conformance reviewer model - [detail](./doc/configuration.md#conformance-gate-model) |
617
617
  | `closureReview.maxFixRounds` | phase-tracker | `5` | fix rounds before escalation - [detail](./doc/configuration.md#conformance-gate-model) |
618
- | `escalationLoop.implModel` | phase-tracker | unset (main loop) | implementer model on escalation - [detail](./doc/configuration.md#escalation-loop) |
618
+ | `escalationLoop.implModel` | phase-tracker | unset (main loop) | execution escalation under either loop option; review escalation remains separate - [detail](./doc/configuration.md#escalation-loop) |
619
+ | `loopEngine.maxIterationsPerRow` | phase-tracker | `4` (floor `1`) | fresh-ledger per-row allowance; run override on approval 4/5 - [detail](./doc/configuration.md#setting-and-command-resolution) |
619
620
  | `phaseModels.plan` / `.implement` / `.verify` / `.ship` | phase-tracker | unset (no switch) | `provider/id[:level]` applied persistently at that phase's start - [detail](./doc/configuration.md#phase-models) |
620
621
  | `flowGuards.enforce` | phase-tracker | `true` | phase gates on/off - [detail](./doc/configuration.md#phase-tracker) |
621
622
  | `flowGuards.specDirs` | phase-tracker, plan-tracker; telemetry and spec-index consumers | `["doc/specs", "docs/specs"]` | where specs and sibling plans live - [detail](./doc/configuration.md#phase-tracker) |
@@ -16,7 +16,7 @@ const ledger = (rows = ["1 | open | 0 |", "2 | cut | 0 | deferred: #70", "3 | do
16
16
  "# Ledger - loop engine\n**Spec:** `doc/specs/test.md`\nbase: abc\nverification: check\niterations: 0\n\n| row | status | attempts | evidence |\n|---|---|---|---|\n" + rows.map(r => `| ${r} |`).join("\n") + "\n\n## Decisions\n";
17
17
 
18
18
  test("ledger admission bypasses task checks and permits empty open evidence", () => {
19
- for (const input of [ledger(), ledger().replace("iterations: 0\n", "iterations: 0\ncap: 97\n")]) {
19
+ for (const input of [ledger(), ...["cap: 97", "allowance: 4"].map(header => ledger().replace("iterations: 0\n", `iterations: 0\n${header}\n`))]) {
20
20
  assert.deepEqual(checkPlan(input, ledgerSpec, { exists: () => false, glob: () => [] }).findings, []);
21
21
  }
22
22
  });
@@ -1271,6 +1271,22 @@ const emptyPort: FsPort = { exists: () => false, glob: () => [] };
1271
1271
  test("coordinated ledger expands shared ownership and defaults unlisted ACs home", () => {
1272
1272
  assert.deepEqual(checkPlan(coordinatedLedger(), coordinatedSpec, emptyPort).findings, []);
1273
1273
  });
1274
+ test("coordinated ledger budget header admits allowance or legacy cap, never both", () => {
1275
+ for (const key of ["allowance", "cap"]) {
1276
+ for (const value of ["1", "4", "9007199254740991"]) {
1277
+ const input = coordinatedLedger().replace("cap: 20", `${key}: ${value}`);
1278
+ assert.deepEqual(checkPlan(input, coordinatedSpec, emptyPort).findings, [], `${key}: ${value}`);
1279
+ }
1280
+ for (const value of ["", "0", "-1", "1.5", "04", "four", "9007199254740992"]) {
1281
+ const input = coordinatedLedger().replace("cap: 20", `${key}: ${value}`);
1282
+ assert.ok(checkPlan(input, coordinatedSpec, emptyPort).findings.some(f => f.check === "ledger"), `${key}: ${value}`);
1283
+ }
1284
+ }
1285
+ for (const header of ["", "allowance: 4\nallowance: 4", "cap: 20\ncap: 20", "allowance: 4\ncap: 20"]) {
1286
+ const input = coordinatedLedger().replace("cap: 20", header);
1287
+ assert.ok(checkPlan(input, coordinatedSpec, emptyPort).findings.some(f => f.check === "ledger"), header);
1288
+ }
1289
+ });
1274
1290
  for (const scope of ["", "scope: AC 1\nscope: AC 2", "scope: AC 1, AC 1", "scope: AC 0", "scope: AC -1", "scope: AC 1.5", "scope: AC 4", "scope: 1", "scope: AC 1-2", "scope: AC 1,", "scope: none, AC 1"]) {
1275
1291
  test(`coordinated scope rejects ${JSON.stringify(scope)}`, () => {
1276
1292
  assert.ok(checkPlan(coordinatedLedger(), coordinatedSpec.replace("scope: AC 1, AC 2", scope), emptyPort).findings.some(f => f.check === "leg-scope"));
@@ -1205,9 +1205,10 @@ function coordinatedMetadata(lines: string[], legs: ReturnType<typeof parseLegs>
1205
1205
  const sha = /^[a-f0-9]{7,40}$/;
1206
1206
  const base = one("base", sha);
1207
1207
  const iterations = one("iterations", /^(?:0|[1-9]\d*)$/);
1208
- const cap = one("cap", /^[1-9]\d*$/);
1208
+ const budgets = [...header("allowance"), ...header("cap")];
1209
+ if (budgets.length !== 1) add(budgets[0]?.i ?? 0, "expected exactly one allowance or cap header");
1210
+ else if (!/^[1-9]\d*$/.test(budgets[0].value) || !Number.isSafeInteger(Number(budgets[0].value))) add(budgets[0].i, "allowance or cap must be a positive safe integer");
1209
1211
  if (!Number.isSafeInteger(Number(iterations))) add(0, "iterations must be a safe integer");
1210
- if (!Number.isSafeInteger(Number(cap))) add(0, "cap must be a safe integer");
1211
1212
  for (const [i, l] of lines.entries()) {
1212
1213
  const m = /^(base|seeded) ([^:]+):/.exec(l);
1213
1214
  if (m && !legs.some(leg => leg.repo === m[2].toLowerCase())) add(i, "foreign member header");
@@ -46,6 +46,18 @@ test("reopens match distinct string names, not position", () => {
46
46
  assert.equal(countReopens([{ status: "complete" }], [{ status: "in_progress" }]), 0);
47
47
  });
48
48
 
49
+ test("allowance label changes between ledger refreshes never change reopen identity", () => {
50
+ const before = [
51
+ { name: "row 3", status: "complete", label: "2/5" },
52
+ { name: "row 4 (repo-b)", status: "complete", label: "1/4" },
53
+ { name: "G1: check", status: "complete" },
54
+ ];
55
+ const relabeled = before.map(task => task.label ? { ...task, label: "2/6" } : task);
56
+ assert.equal(countReopens(before, relabeled), 0);
57
+ assert.equal(countReopens(before, relabeled.map(task => ({ ...task, status: "in_progress" }))), 3);
58
+ assert.equal(countReopens(before, relabeled.map(task => ({ ...task, status: "pending" }))), 0);
59
+ });
60
+
49
61
  test("insertedText joins edit newText values or returns write content", () => {
50
62
  assert.equal(insertedText({ path: "x", edits: [{ oldText: "a", newText: "b" }, { oldText: "c", newText: "d" }] }), "b\nd");
51
63
  assert.equal(insertedText({ path: "x", content: "body" }), "body");
@@ -536,7 +536,9 @@ test("ledger refresh completes only started implement and keeps failed, in-fligh
536
536
  for (const allCut of [true, false]) {
537
537
  const dir = tempCwd({ piGauntlet: { flowGuards: { enforce: false } } });
538
538
  const path = join(dir, "ledger.md");
539
- const save = (status: string, pending = "") => writeFileSync(path, `# Ledger - loop engine\n${pending}\n| row | status | attempts | evidence |\n|---|---|---|---|\n| 1 | ${status} | 0 | check |\n\n## Decisions\n`);
539
+ execFileSync("git", ["init", "-q", dir]);
540
+ writeFileSync(join(dir, "spec.md"), "# Spec\n\n## Acceptance criteria\n- [ ] The tracker shows a readable step\n in-scope\n");
541
+ const save = (status: string, pending = "") => writeFileSync(path, `# Ledger - loop engine\n**Spec:** \`spec.md\`\n${pending}\n| row | status | attempts | evidence |\n|---|---|---|---|\n| 1 | ${status} | 0 | check |\n\n## Decisions\n`);
540
542
  save(allCut ? "cut" : "failed");
541
543
  const branch = implementBranch().slice(0, -1);
542
544
  const h = harness({ cwd: dir, branch });
@@ -1,13 +1,14 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
- import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { execFileSync } from "node:child_process";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
7
  import registerPlanTracker, { validateSnapshot } from "./plan-tracker.ts";
7
8
 
8
9
  type ToolResult = {
9
10
  content: { type: string; text: string }[];
10
- details: { action: string; tasks: { name: string; status: string }[]; error?: string };
11
+ details: { action: string; tasks: { name: string; status: string; label?: string }[]; error?: string };
11
12
  };
12
13
 
13
14
  function harness(branch: unknown[] = [], cwd = process.cwd()) {
@@ -384,14 +385,25 @@ test("legacy-invalid snapshot: first update is rejected listing legacy offenders
384
385
  assert.deepEqual(repaired.details.tasks.map((t) => t.status), ["complete", "skipped", "complete"]);
385
386
  });
386
387
 
388
+ const descriptions = [
389
+ "Keep cut requirements visible", "Reject malformed allowance replies", "Verify loop behavior and recovery",
390
+ "Report failed checks", "Preserve completed work", "Start the next criterion",
391
+ ];
392
+ const spec = (criteria = descriptions) => `# Spec\n\n## Acceptance criteria\n${criteria.map(text => `- [ ] ${text}`).join("\n")}\n`;
393
+ function ledgerFixture(prefix: string) {
394
+ const dir = mkdtempSync(join(tmpdir(), prefix));
395
+ execFileSync("git", ["init", "-q", dir]);
396
+ writeFileSync(join(dir, "spec.md"), spec(descriptions.map(text => `${text}\n in-scope`)));
397
+ return dir;
398
+ }
387
399
  const ledger = (rows: string[], tail = "", pending = "") => [
388
- "# Ledger - loop engine", pending,
400
+ "# Ledger - loop engine", "**Spec:** `spec.md`", pending,
389
401
  "| row | status | attempts | evidence |", "|---|---|---|---|",
390
402
  ...rows.map(row => `| ${row} |`), "", "## Decisions", tail,
391
403
  ].join("\n");
392
404
 
393
405
  test("refresh projects ledger states, numeric pending targets and live leg mappings", async () => {
394
- const dir = mkdtempSync(join(tmpdir(), "plan-refresh-"));
406
+ const dir = ledgerFixture("plan-refresh-");
395
407
  const path = join(dir, "ledger.md");
396
408
  const cases = [
397
409
  {
@@ -425,7 +437,7 @@ test("refresh projects ledger states, numeric pending targets and live leg mappi
425
437
  });
426
438
 
427
439
  test("refresh replaces only whole ledger names, preserves Gn statuses and replays its snapshot", async () => {
428
- const dir = mkdtempSync(join(tmpdir(), "plan-refresh-kept-"));
440
+ const dir = ledgerFixture("plan-refresh-kept-");
429
441
  const path = join(dir, "ledger.md");
430
442
  writeFileSync(path, ledger(["1 | open | 0 |", "2 | open | 0 |", "3 | done | 1 | passed"], "row 3 done | passed | row 1"));
431
443
  const h = harness();
@@ -437,9 +449,9 @@ test("refresh replaces only whole ledger names, preserves Gn statuses and replay
437
449
  const result = await h.call({ action: "refresh", path });
438
450
  assert.equal(result.details.error, undefined);
439
451
  assert.deepEqual(result.details.tasks, [
440
- { name: "row 3", status: "complete" },
452
+ { name: "row 3", status: "complete", label: descriptions[2] },
441
453
  { name: "G1: closed", status: "complete" }, { name: "G2: working", status: "in_progress" },
442
- { name: "row 1", status: "pending" }, { name: "row 2", status: "pending" },
454
+ { name: "row 1", status: "pending", label: descriptions[0] }, { name: "row 2", status: "pending", label: descriptions[1] },
443
455
  { name: "row 4 notes", status: "pending" }, { name: "G3: pending", status: "pending" },
444
456
  ]);
445
457
  assert.deepEqual(validateSnapshot(result.details.tasks as any), []);
@@ -449,8 +461,235 @@ test("refresh replaces only whole ledger names, preserves Gn statuses and replay
449
461
  assert.equal(replay.widget(), h.widget());
450
462
  });
451
463
 
464
+ test("allowance labels count retry decisions, preserve identity and render on every status surface", async () => {
465
+ const dir = ledgerFixture("plan-allowance-");
466
+ const path = join(dir, "ledger.md");
467
+ const text = ledger([
468
+ "1 | cut | 3 | deferred: #1", "2 | done | 0 | baseline", "3 | open | 2 | check",
469
+ "4 | failed | 7 | failed", "5 | done | 1 | passed", "6 | open | 0 | check",
470
+ ], [
471
+ "row 3 retry | hint | allowed attempts 5", "row 3 retry | next | allowed attempts 99",
472
+ "row 3 escalated | implModel round | allowed attempts 100", "cap +20 | historical | cap 40",
473
+ "row 4 retry | hint | allowed attempts 8", "row 5 done | passed | row 3",
474
+ "## Notes", "row 3 retry | not a decision | allowed attempts 101",
475
+ "## Leg mapping", "| row | ac | leg |", "|---|---|---|",
476
+ "| 1 | 1 | repo-a |", "| 2 | 2 | repo-a |", "| 3 | 3 | repo-b |",
477
+ "| 4 | 4 | repo-a |", "| 5 | 5 | repo-b |", "| 6 | 6 | repo-a |",
478
+ ].join("\n"), "allowance: 4\npending: 3 | repo-b | abc1234");
479
+ writeFileSync(path, text);
480
+ const h = harness();
481
+ await h.call({ action: "init", tasks: [{ name: "G1: fixed", status: "complete" }, "2/5: G2: pending"] });
482
+ const result = await h.call({ action: "refresh", path });
483
+ assert.equal(result.details.error, undefined);
484
+ assert.deepEqual(result.details.tasks, [
485
+ { name: "row 1 (repo-a)", status: "skipped", label: `${descriptions[0]} (repo-a)` },
486
+ { name: "row 2 (repo-a)", status: "skipped", label: `${descriptions[1]} (repo-a)` },
487
+ { name: "row 3 (repo-b)", status: "in_progress", label: `2/6: ${descriptions[2]} (repo-b)` },
488
+ { name: "row 4 (repo-a)", status: "failed", label: `7/5: ${descriptions[3]} (repo-a)` },
489
+ { name: "row 5 (repo-b)", status: "complete", label: `1/4: ${descriptions[4]} (repo-b)` },
490
+ { name: "G1: fixed", status: "complete" },
491
+ { name: "row 6 (repo-a)", status: "pending", label: `0/4: ${descriptions[5]} (repo-a)` },
492
+ { name: "2/5: G2: pending", status: "pending" },
493
+ ]);
494
+ assert.match(h.widget()!, /2\/6: Verify loop behavior and recovery \(repo-b\)$/);
495
+ assert.doesNotMatch(h.widget()!, /row 3/);
496
+ assert.match(result.content[0].text, /\[2\] row 3 \(repo-b\) \| 2\/6: Verify loop behavior and recovery \(repo-b\)/);
497
+ const status = await h.call({ action: "status" });
498
+ assert.match(status.content[0].text, /\[4\] row 5 \(repo-b\) \| 1\/4: Preserve completed work \(repo-b\)/);
499
+ const rendered = h.tool().renderResult(status, {}, h.theme).text!;
500
+ assert.match(rendered, /row 3 \(repo-b\) \| 2\/6: Verify loop behavior and recovery \(repo-b\)/);
501
+ assert.match(rendered, /row 4 \(repo-a\) \| 7\/5: Report failed checks \(repo-a\)/);
502
+ const replay = harness([{ type: "message", message: { role: "toolResult", toolName: "plan_tracker", details: status.details } }]);
503
+ await replay.fire("session_start");
504
+ assert.equal(replay.widget(), h.widget());
505
+ assert.deepEqual((await replay.call({ action: "status" })).details.tasks, status.details.tasks);
506
+
507
+ writeFileSync(path, text.replace("allowance: 4", "allowance: 7"));
508
+ const raised = await h.call({ action: "refresh", path });
509
+ assert.deepEqual(raised.details.tasks.map(t => [t.name, t.status]), result.details.tasks.map(t => [t.name, t.status]));
510
+ assert.equal(raised.details.tasks.find(t => t.name === "row 3 (repo-b)")!.label, `2/9: ${descriptions[2]} (repo-b)`);
511
+ assert.equal(raised.details.tasks.find(t => t.name === "row 5 (repo-b)")!.label, `1/7: ${descriptions[4]} (repo-b)`);
512
+ assert.equal(raised.details.tasks.length, result.details.tasks.length);
513
+ writeFileSync(path, text.replace("allowance: 4", "cap: 49"));
514
+ const legacy = await h.call({ action: "refresh", path });
515
+ assert.equal(legacy.details.error, undefined);
516
+ assert.deepEqual(legacy.details.tasks.filter(t => t.name.startsWith("row ")).map(t => t.label),
517
+ descriptions.map((description, i) => `${description} (${[2, 4].includes(i) ? "repo-b" : "repo-a"})`));
518
+ });
519
+
520
+ test("allowance labels tolerate surrounding whitespace in the header value", async () => {
521
+ const dir = ledgerFixture("plan-allowance-whitespace-");
522
+ const path = join(dir, "ledger.md");
523
+ const h = harness();
524
+ const labels = [];
525
+ for (const header of ["allowance: 4", "allowance: 4", "allowance: 4 "]) {
526
+ writeFileSync(path, ledger(["1 | open | 0 |"], "", header));
527
+ const result = await h.call({ action: "refresh", path });
528
+ assert.equal(result.details.error, undefined, header);
529
+ labels.push(result.details.tasks[0].label);
530
+ }
531
+ assert.deepEqual(labels, Array(3).fill(`0/4: ${descriptions[0]}`));
532
+ });
533
+
534
+ test("single-repo labels are projection-only and cannot enter through init or add", async () => {
535
+ const dir = ledgerFixture("plan-label-input-");
536
+ const path = join(dir, "ledger.md");
537
+ writeFileSync(path, ledger(["3 | open | 2 | check"], "row 3 retry | hint | allowed attempts 99", "allowance: 4"));
538
+ const h = harness();
539
+ const projected = await h.call({ action: "refresh", path });
540
+ assert.deepEqual(projected.details.tasks, [{ name: "row 3", status: "pending", label: `2/5: ${descriptions[2]}` }]);
541
+ assert.match(h.widget()!, /2\/5: Verify loop behavior and recovery$/);
542
+ const init = await h.call({ action: "init", tasks: [{ name: "row 3", status: "in_progress", label: "9/9" }] });
543
+ assert.deepEqual(init.details.tasks, [{ name: "row 3", status: "in_progress" }]);
544
+ const added = await h.call({ action: "add", tasks: [{ name: "G1: check", status: "pending", label: "9/9" }] });
545
+ assert.ok(added.details.tasks.every(t => !Object.hasOwn(t, "label")));
546
+ const objectSchema = h.parameters().args[0].tasks.args[0].args[0].args[0][1];
547
+ assert.deepEqual(Object.keys(objectSchema.args[0]), ["name", "status"]);
548
+ });
549
+
550
+ test("refresh resolves the owning root, normalizes original AC text, and keeps labels stable across status changes", async () => {
551
+ const dir = ledgerFixture("plan-descriptions-");
552
+ const nested = join(dir, "doc", "plans");
553
+ mkdirSync(nested, { recursive: true });
554
+ const path = join(nested, "ledger.md");
555
+ writeFileSync(join(dir, "spec.md"), [
556
+ "# Spec", "```md", "## Acceptance criteria", "- [ ] Not a criterion", "```",
557
+ "## Acceptance Criteria", "- [x] Keep cut requirements", " visible",
558
+ " deferred: #1", " disposition continuation", "- [ ] Reject malformed allowance replies",
559
+ " elsewhere: repo-b#2", "- [ ] Verify loop behavior", " and recovery | row 99 is only text",
560
+ " venue: staging - observe completion", "## Design", "- [ ] Not an acceptance criterion",
561
+ ].join("\n"));
562
+ const text = ledger(["1 | cut | 0 | deferred", "2 | done | 0 | baseline", "3 | open | 2 | check"], "", "allowance: 4");
563
+ writeFileSync(path, text);
564
+ const h = harness([], nested);
565
+ const first = await h.call({ action: "refresh", path: "ledger.md" });
566
+ assert.equal(first.details.error, undefined);
567
+ assert.deepEqual(first.details.tasks.map(t => t.label), [
568
+ "Keep cut requirements visible", descriptions[1], "2/4: Verify loop behavior and recovery | row 99 is only text",
569
+ ]);
570
+ assert.match(first.content[0].text, /row 3 \| 2\/4: Verify loop behavior and recovery \| row 99 is only text/);
571
+ for (const [status, tail, pending] of [
572
+ ["failed", "", ""], ["failed", "", "pending: 3 | home | abc1234"],
573
+ ["done", "row 3 done | passed | none", ""],
574
+ ]) {
575
+ writeFileSync(path, text.replace("| 3 | open", `| 3 | ${status}`).replace("allowance: 4", `allowance: 4\n${pending}`) + tail);
576
+ const next = await h.call({ action: "refresh", path });
577
+ assert.equal(next.details.error, undefined);
578
+ assert.deepEqual(next.details.tasks.map(t => [t.name, t.label]), first.details.tasks.map(t => [t.name, t.label]));
579
+ }
580
+ writeFileSync(join(dir, "spec.md"), spec([descriptions[0], descriptions[1], "Verify amended behavior"]));
581
+ const amended = await h.call({ action: "refresh", path });
582
+ assert.equal(amended.details.tasks[2].name, "row 3");
583
+ assert.equal(amended.details.tasks[2].label, "2/4: Verify amended behavior");
584
+ assert.equal(amended.details.tasks.length, 3);
585
+ });
586
+
587
+ test("coordinated labels use mapped original AC indices, including shared criteria", async () => {
588
+ const dir = ledgerFixture("plan-original-ac-");
589
+ const path = join(dir, "ledger.md");
590
+ writeFileSync(path, ledger(["1 | cut | 0 | cut", "2 | open | 1 | check", "3 | open | 0 | check"], [
591
+ "## Leg mapping", "| row | ac | leg |", "|---|---|---|",
592
+ "| 1 | 2 | repo-a |", "| 2 | 3 | repo-a |", "| 3 | 3 | repo-b |",
593
+ ].join("\n"), "allowance: 4"));
594
+ const result = await harness().call({ action: "refresh", path });
595
+ assert.equal(result.details.error, undefined);
596
+ assert.deepEqual(result.details.tasks.map(t => [t.name, t.label]), [
597
+ ["row 1 (repo-a)", `${descriptions[1]} (repo-a)`],
598
+ ["row 2 (repo-a)", `1/4: ${descriptions[2]} (repo-a)`],
599
+ ["row 3 (repo-b)", `0/4: ${descriptions[2]} (repo-b)`],
600
+ ]);
601
+ });
602
+
603
+ test("refresh rejects an unresolvable repository root without changing initialized tasks", async (t) => {
604
+ const dir = mkdtempSync(join(tmpdir(), "plan-no-checkout-"));
605
+ try {
606
+ execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], { stdio: "ignore" });
607
+ t.skip("temporary directory is inside a git repository");
608
+ return;
609
+ } catch {}
610
+ writeFileSync(join(dir, "spec.md"), spec(["Keep prior tasks"]));
611
+ const path = join(dir, "ledger.md");
612
+ writeFileSync(path, ledger(["1 | open | 0 |"], "", "allowance: 4"));
613
+ const h = harness([], dir);
614
+ const initial = await h.call({ action: "init", tasks: [{ name: "G1: closed", status: "complete" }, "G2: pending"] });
615
+ const result = await h.call({ action: "refresh", path });
616
+ assert.match(result.details.error!, /cannot resolve repository root for Spec reference/);
617
+ assert.equal(result.details.tasks, undefined);
618
+ assert.deepEqual((await h.call({ action: "status" })).details.tasks, initial.details.tasks);
619
+ });
620
+
621
+ test("refresh rejects missing specs, missing criteria and empty descriptions atomically", async () => {
622
+ const dir = ledgerFixture("plan-spec-errors-");
623
+ const path = join(dir, "ledger.md");
624
+ const valid = ledger(["1 | open | 0 | check"], "", "allowance: 4");
625
+ writeFileSync(path, valid);
626
+ const h = harness();
627
+ const initial = await h.call({ action: "refresh", path });
628
+ assert.equal(initial.details.error, undefined);
629
+ const widget = h.widget();
630
+ const cases = [
631
+ { text: valid.replace("**Spec:** `spec.md`", ""), spec: spec(), error: /missing Spec reference/ },
632
+ { text: valid.replace("`spec.md`", "`absent.md`"), spec: spec(), error: /cannot read spec .*absent\.md:/ },
633
+ { text: valid, spec: "# Spec\n## Design\n- [ ] Not an AC", error: /row 1: spec .*spec\.md AC 1 is missing/ },
634
+ { text: valid, spec: spec([" \n deferred: #1"]), error: /row 1: spec .*spec\.md AC 1 has an empty description/ },
635
+ { text: ledger(["7 | open | 0 | check"]), spec: spec(), error: /row 7: spec .*spec\.md AC 7 is missing/ },
636
+ { text: valid + "\n## Leg mapping\n| row | ac | leg |\n|---|---|---|\n| 1 | 9 | repo-a |", spec: spec(), error: /row 1: spec .*spec\.md AC 9 is missing/ },
637
+ ];
638
+ for (const row of cases) {
639
+ writeFileSync(path, row.text);
640
+ writeFileSync(join(dir, "spec.md"), row.spec);
641
+ const result = await h.call({ action: "refresh", path });
642
+ assert.match(result.details.error!, row.error);
643
+ assert.equal(result.details.tasks, undefined);
644
+ assert.equal(h.widget(), widget);
645
+ assert.deepEqual((await h.call({ action: "status" })).details.tasks, initial.details.tasks);
646
+ }
647
+ });
648
+
649
+ test("status rows round-trip through the brief identity grammar without description drift or duplicate tasks", async () => {
650
+ const contract = readFileSync(new URL("../skills/gauntlet-resume/reference/brief-contract.md", import.meta.url), "utf8");
651
+ const suffix = new RegExp(contract.match(/suffix pattern `([^`]+)`/)![1]);
652
+ const prefix = new RegExp(contract.match(/prefix pattern `([^`]+)`/)![1]);
653
+ const canonical = new RegExp(contract.match(/canonical pattern `([^`]+)`/)![1]);
654
+ const restoreName = (name: string) => {
655
+ const labeled = suffix.exec(name);
656
+ if (labeled) return labeled[1];
657
+ const plain = name.replace(prefix, "");
658
+ return canonical.test(plain) ? plain : name;
659
+ };
660
+ for (const [input, expected] of [
661
+ ["row 3", "row 3"], ["2/5: row 3", "row 3"], ["2/5: row 3 (repo-b)", "row 3 (repo-b)"],
662
+ ["row 3 (repo-b) | 2/5: Verify | row 99", "row 3 (repo-b)"],
663
+ ["2/5: G3: pending", "2/5: G3: pending"], ["G1: row 3 | description", "G1: row 3 | description"],
664
+ ["row 3 notes | description", "row 3 notes | description"],
665
+ ]) assert.equal(restoreName(input), expected);
666
+ const dir = ledgerFixture("plan-brief-roundtrip-");
667
+ const path = join(dir, "ledger.md");
668
+ writeFileSync(join(dir, "spec.md"), spec([descriptions[0], descriptions[1], `${descriptions[2]} | row 99 is text`]));
669
+ const text = ledger(["2 | cut | 0 | cut", "3 | done | 2 | passed"], "row 3 done | passed | none", "allowance: 5");
670
+ writeFileSync(path, text);
671
+ const h = harness();
672
+ await h.call({ action: "init", tasks: [{ name: "G1: failed", status: "failed" }, { name: "G2: repair", status: "in_progress" }, "2/5: G3: pending"] });
673
+ await h.call({ action: "refresh", path });
674
+ const status = await h.call({ action: "status" });
675
+ const brief = status.content[0].text;
676
+ const statuses: Record<string, string> = { "\u2713": "complete", "\u2192": "in_progress", "\u2717": "failed", "\u2298": "skipped", "\u25cb": "pending" };
677
+ const restored = [...brief.matchAll(/^\s*([\u2713\u2192\u2717\u2298\u25cb]) \[\d+\] (.+)$/gm)]
678
+ .map(([, glyph, name]) => ({ name: restoreName(name), status: statuses[glyph] }));
679
+ assert.deepEqual(restored, status.details.tasks.map(({ name, status }) => ({ name, status })));
680
+ const resumed = harness();
681
+ const init = await resumed.call({ action: "init", tasks: restored });
682
+ assert.equal(init.details.error, undefined);
683
+ assert.ok(init.details.tasks.every(t => !Object.hasOwn(t, "label")));
684
+ writeFileSync(path, text.replace("allowance: 5", "allowance: 6"));
685
+ const refreshed = await resumed.call({ action: "refresh", path });
686
+ assert.deepEqual(refreshed.details.tasks.map(({ name, status }) => ({ name, status })), restored);
687
+ assert.match(refreshed.content[0].text, /row 3 \| 2\/6: Verify loop behavior and recovery \| row 99 is text/);
688
+ assert.equal(refreshed.details.tasks.filter(t => t.name === "row 3").length, 1);
689
+ });
690
+
452
691
  test("refresh rejects unreadable or malformed ledgers atomically, including absent numeric pending rows", async () => {
453
- const dir = mkdtempSync(join(tmpdir(), "plan-refresh-error-"));
692
+ const dir = ledgerFixture("plan-refresh-error-");
454
693
  const path = join(dir, "ledger.md");
455
694
  const valid = ledger(["1 | open | 0 |"]);
456
695
  const h = harness();
@@ -461,8 +700,9 @@ test("refresh rejects unreadable or malformed ledgers atomically, including abse
461
700
  { name: "unreadable path", path: join(dir, "absent.md"), text: valid, error: /refresh: cannot read .*absent\.md:/ },
462
701
  { name: "wrong discriminator", path, text: "# Plan", error: /line 1 is "# Plan", not a loop ledger/ },
463
702
  { name: "missing header", path, text: valid.replace("| row | status | attempts | evidence |", "| row | state | attempts | evidence |"), error: /row table malformed at line \d+/ },
464
- { name: "nonnumeric row", path, text: valid.replace("| 1 | open", "| x | open"), error: /row table malformed at line 5/ },
465
- { name: "invalid status", path, text: valid.replace("| 1 | open", "| 1 | unknown"), error: /row table malformed at line 5/ },
703
+ { name: "nonnumeric row", path, text: valid.replace("| 1 | open", "| x | open"), error: /row table malformed at line 6/ },
704
+ { name: "invalid status", path, text: valid.replace("| 1 | open", "| 1 | unknown"), error: /row table malformed at line 6/ },
705
+ ...["x", "-1", "1.5", ""].map(attempts => ({ name: `invalid attempts ${attempts}`, path, text: valid.replace("| 0 |", `| ${attempts} |`), error: /row table malformed at line 6/ })),
466
706
  { name: "absent pending row", path, text: "pending: 2 | home | abc1234", error: /pending: names row 2 which is absent/ },
467
707
  { name: "zero pending row", path, text: "pending: 0 | home | abc1234", error: /pending: names row 0 which is absent/ },
468
708
  { name: "leading-zero absent row", path, text: "pending: 02 | home | abc1234", error: /pending: names row 2 which is absent/ },
@@ -12,11 +12,13 @@ import { StringEnum } from "@earendil-works/pi-ai";
12
12
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
13
13
  import { Text } from "@earendil-works/pi-tui";
14
14
  import { type Static, Type } from "@sinclair/typebox";
15
+ import { checkoutOfSync } from "./lib/checkout.ts";
16
+ import { planSpecHeader } from "./lib/telemetry-paths.ts";
15
17
 
16
18
  const TASK_STATUSES = ["pending", "in_progress", "complete", "failed", "skipped"] as const;
17
19
  type TaskStatus = (typeof TASK_STATUSES)[number];
18
20
 
19
- interface Task { name: string; status: TaskStatus; }
21
+ interface Task { name: string; status: TaskStatus; label?: string; }
20
22
  interface PlanTrackerDetails { action: "init" | "add" | "update" | "status" | "clear" | "refresh"; tasks: Task[]; error?: string; }
21
23
 
22
24
  const PlanTrackerParams = Type.Object({
@@ -43,6 +45,36 @@ export function validateSnapshot(tasks: Task[]): number[] {
43
45
  return offenders;
44
46
  }
45
47
 
48
+ function criterionDescriptions(spec: string): string[] {
49
+ const descriptions: string[] = [];
50
+ let inAcceptance = false;
51
+ let disposition = false;
52
+ let fence: string | undefined;
53
+ for (const line of spec.split("\n")) {
54
+ const delimiter = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
55
+ if (delimiter && !fence) { fence = delimiter; continue; }
56
+ if (fence) {
57
+ if (delimiter?.[0] === fence[0] && delimiter.length >= fence.length) fence = undefined;
58
+ else if (inAcceptance && descriptions.length && !disposition) descriptions[descriptions.length - 1] += ` ${line}`;
59
+ continue;
60
+ }
61
+ if (/^#{1,2}\s/.test(line)) {
62
+ inAcceptance = /^## Acceptance criteria\s*$/i.test(line);
63
+ continue;
64
+ }
65
+ if (!inAcceptance) continue;
66
+ const criterion = /^- \[[ xX]\](?:\s+(.*))?$/.exec(line);
67
+ if (criterion) {
68
+ descriptions.push(criterion[1] ?? "");
69
+ disposition = false;
70
+ } else if (descriptions.length) {
71
+ if (/^\s*(?:in-scope\s*$|(?:deviates|deferred|elsewhere|venue):)/.test(line)) disposition = true;
72
+ if (!disposition) descriptions[descriptions.length - 1] += ` ${line}`;
73
+ }
74
+ }
75
+ return descriptions.map(text => text.replace(/\s+/g, " ").trim());
76
+ }
77
+
46
78
  function ledgerTasks(text: string, path: string): Task[] {
47
79
  const lines = text.split("\n");
48
80
  if (lines[0] !== "# Ledger - loop engine") {
@@ -52,11 +84,13 @@ function ledgerTasks(text: string, path: string): Task[] {
52
84
  const header = lines.findIndex(line => /^\|\s*row\s*\|\s*status\s*\|\s*attempts\s*\|\s*evidence\s*\|$/.test(line));
53
85
  if (header < 0) malformed(0);
54
86
  if (!/^\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|$/.test(lines[header + 1] ?? "")) malformed(header + 1);
55
- const rows: { row: number; status: string }[] = [];
87
+ const allowanceMatch = lines.slice(1, header).map(line => /^allowance:\s*([1-9]\d*)\s*$/.exec(line)).find(Boolean);
88
+ const allowance = allowanceMatch ? Number(allowanceMatch[1]) : undefined;
89
+ const rows: { row: number; status: string; attempts: number }[] = [];
56
90
  for (let i = header + 2; i < lines.length && lines[i].trim() && !/^#{1,2}\s/.test(lines[i]); i++) {
57
91
  const cells = lines[i].split(/(?<!\\)\|/).slice(1, -1).map(cell => cell.trim());
58
- if (!lines[i].startsWith("|") || !lines[i].endsWith("|") || cells.length !== 4 || !/^\d+$/.test(cells[0]) || !/^(open|done|failed|cut)$/.test(cells[1])) malformed(i);
59
- rows.push({ row: Number(cells[0]), status: cells[1] });
92
+ if (!lines[i].startsWith("|") || !lines[i].endsWith("|") || cells.length !== 4 || !/^\d+$/.test(cells[0]) || !/^(open|done|failed|cut)$/.test(cells[1]) || !/^\d+$/.test(cells[2])) malformed(i);
93
+ rows.push({ row: Number(cells[0]), status: cells[1], attempts: Number(cells[2]) });
60
94
  }
61
95
  if (!rows.length) malformed(header + 2);
62
96
  const byRow = new Map(rows.map(row => [row.row, row]));
@@ -66,7 +100,8 @@ function ledgerTasks(text: string, path: string): Task[] {
66
100
  throw new Error(`refresh: ${path} pending: names row ${pending} which is absent`);
67
101
  }
68
102
  const dispatched = new Set<number>();
69
- let mapping: { row: number; repo: string }[] | undefined;
103
+ const retries = new Map<number, number>();
104
+ let mapping: { row: number; ac: number; repo: string }[] | undefined;
70
105
  let section = "";
71
106
  for (const line of lines) {
72
107
  if (/^#{1,2}\s/.test(line)) {
@@ -77,12 +112,17 @@ function ledgerTasks(text: string, path: string): Task[] {
77
112
  if (section === "## Decisions") {
78
113
  const decision = /^row (\d+) done \| .+ \| .+$/.exec(line);
79
114
  if (decision) dispatched.add(Number(decision[1]));
115
+ const retry = /^row (\d+) retry \| .* \| allowed attempts \d+$/.exec(line);
116
+ if (retry) {
117
+ const row = Number(retry[1]);
118
+ retries.set(row, (retries.get(row) ?? 0) + 1);
119
+ }
80
120
  } else if (section === "## Leg mapping") {
81
- const entry = /^\|\s*(\d+)\s*\|\s*\d+\s*\|\s*(\S+)\s*\|$/.exec(line);
121
+ const entry = /^\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*(\S+)\s*\|$/.exec(line);
82
122
  if (!entry) continue;
83
123
  const row = Number(entry[1]);
84
124
  if (!byRow.has(row)) throw new Error(`refresh: ${path} leg mapping names row ${row} which is absent`);
85
- mapping!.push({ row, repo: entry[2] });
125
+ mapping!.push({ row, ac: Number(entry[2]), repo: entry[3] });
86
126
  }
87
127
  }
88
128
  const status = (row: number): TaskStatus => {
@@ -92,11 +132,28 @@ function ledgerTasks(text: string, path: string): Task[] {
92
132
  if (row === pending) return "in_progress";
93
133
  return value === "failed" ? "failed" : "pending";
94
134
  };
95
- const entries = mapping ?? rows.map(({ row }) => ({ row, repo: undefined }));
96
- return entries.sort((a, b) => a.row - b.row).map(({ row, repo }) => ({
97
- name: `row ${row}${repo === undefined ? "" : ` (${repo})`}`,
98
- status: status(row),
99
- }));
135
+ const specRef = planSpecHeader(lines.slice(0, header).join("\n"));
136
+ if (!specRef) throw new Error(`refresh: ${path} missing Spec reference`);
137
+ const checkout = checkoutOfSync(path);
138
+ if (!checkout) throw new Error(`refresh: ${path} cannot resolve repository root for Spec reference ${specRef}`);
139
+ const specPath = resolve(checkout.toplevel, specRef);
140
+ let spec: string;
141
+ try {
142
+ spec = readFileSync(specPath, "utf8");
143
+ } catch (error) {
144
+ throw new Error(`refresh: ${path} cannot read spec ${specPath}: ${error instanceof Error ? error.message : String(error)}`);
145
+ }
146
+ const descriptions = criterionDescriptions(spec);
147
+ const entries = mapping ?? rows.map(({ row }) => ({ row, ac: row, repo: undefined }));
148
+ return entries.sort((a, b) => a.row - b.row).map(({ row, ac, repo }) => {
149
+ const description = descriptions[ac - 1];
150
+ if (!description) throw new Error(`refresh: ${path} row ${row}: spec ${specPath} AC ${ac} ${description === undefined ? "is missing" : "has an empty description"}`);
151
+ const value = byRow.get(row)!;
152
+ const owner = repo === undefined ? "" : ` (${repo})`;
153
+ const budget = allowance !== undefined && value.status !== "cut" && (value.status !== "done" || value.attempts > 0)
154
+ ? `${value.attempts}/${allowance + (retries.get(row) ?? 0)}: ` : "";
155
+ return { name: `row ${row}${owner}`, status: status(row), label: `${budget}${description}${owner}` };
156
+ });
100
157
  }
101
158
 
102
159
  const RULE_LINE = "Rule: pending tasks must trail every started or finished task.";
@@ -104,6 +161,8 @@ const snapshotLine = (tasks: Task[]): string => tasks.map((t, i) => `${i} ${t.na
104
161
  const offenderList = (tasks: Task[], indices: number[]): string => indices.map((i) => `${i} "${tasks[i].name}"`).join(", ");
105
162
  const normalizeTask = (t: string | { name: string; status: TaskStatus }): Task => typeof t === "string" ? { name: t, status: "pending" } : { name: t.name, status: t.status };
106
163
 
164
+ const statusName = (task: Task): string => task.label ? `${task.name} | ${task.label}` : task.name;
165
+
107
166
  const glyph = (status: TaskStatus, theme?: Theme): string => {
108
167
  const [color, icon]: [string, string] = status === "complete" ? ["success", "✓"] : status === "in_progress" ? ["warning", "→"] : status === "failed" ? ["error", "✗"] : status === "skipped" ? ["dim", "⊘"] : ["dim", "○"];
109
168
  return theme ? theme.fg(color as Parameters<Theme["fg"]>[0], icon) : icon;
@@ -121,7 +180,7 @@ function formatWidget(tasks: Task[], theme: Theme): string {
121
180
  const { done } = counts(tasks);
122
181
  const icons = tasks.map((t) => glyph(t.status, theme)).join("");
123
182
  const current = tasks.find((t) => t.status === "in_progress") ?? tasks.find((t) => t.status === "pending");
124
- const currentName = current ? ` ${current.name}` : "";
183
+ const currentName = current ? ` ${current.label ?? current.name}` : "";
125
184
  return `${theme.fg("muted", "Tasks:")} ${icons} ${theme.fg("muted", `(${done}/${tasks.length})`)}${currentName}`;
126
185
  }
127
186
 
@@ -131,7 +190,7 @@ function formatStatus(tasks: Task[]): string {
131
190
  const groups = [[c.inProgress, "in progress"], [c.pending, "pending"], [c.failed, "failed"], [c.skipped, "skipped"]] as const;
132
191
  const detail = groups.filter(([n]) => n > 0).map(([n, label]) => `${n} ${label}`);
133
192
  const lines: string[] = [`Plan: ${c.done}/${tasks.length} done${detail.length > 0 ? ` (${detail.join(", ")})` : ""}`, ""];
134
- for (let i = 0; i < tasks.length; i++) lines.push(` ${glyph(tasks[i].status)} [${i}] ${tasks[i].name}`);
193
+ for (let i = 0; i < tasks.length; i++) lines.push(` ${glyph(tasks[i].status)} [${i}] ${statusName(tasks[i])}`);
135
194
  return lines.join("\n");
136
195
  }
137
196
 
@@ -440,7 +499,7 @@ export default function (pi: ExtensionAPI) {
440
499
  let text = theme.fg("muted", `${c.done}/${taskList.length} done${c.suffix}`);
441
500
  for (const t of taskList) {
442
501
  const icon = glyph(t.status, theme);
443
- text += `\n${icon} ${theme.fg("muted", t.name)}`;
502
+ text += `\n${icon} ${theme.fg("muted", statusName(t))}`;
444
503
  }
445
504
  return new Text(text, 0, 0);
446
505
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "8.1.2",
3
+ "version": "8.1.3",
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",
@@ -1,6 +1,6 @@
1
1
  # Amendment surface
2
2
 
3
- `read` this file (the `read` tool at its absolute path, never a shell command) from `skills/brainstorming/SKILL.md` section Amending an approved spec, from any phase, for amend-class changes; at approval, load only [Standing grants](#standing-grants), not the batch procedure. Keep redraws outside this procedure. Classify a text correction to an approved drafted acceptance-criteria row backed by implementer evidence (a quoted test, assertion, or code line in a `BLOCKED`/`NEEDS_CONTEXT` return) as an amend item. Keep ticket rows, row add/remove, reviewer-only or taste objections, problem statement changes, and component boundary changes as redraws at batch preparation, before any standing grant: re-draft from the new design and ratify at the full gate. Keep a disposition-only change to such a row in this grammar so it keeps the cut question. Apply an item the human has answered by letter or custom text as recorded. Keep human-facing amendment text with the main loop holding `edit`/`write`; delegate implement-phase spec authorship only through the implement-only route below. Keep all other callers' authorship with the main loop. Use the human last: a fresh reviewer clears evidence-backed factual corrections; present the rest to the human once per batch plainly.
3
+ `read` this file (the `read` tool at its absolute path, never a shell command) from `skills/brainstorming/SKILL.md` section Amending an approved spec, from any phase, for amend-class changes; at approval, load only [Standing grants](#standing-grants), not the batch procedure. Keep redraws outside this procedure; stop for human authorization before redraw resets or deletions. Classify a text correction to an approved drafted acceptance-criteria row backed by implementer evidence (a quoted test, assertion, or code line in a `BLOCKED`/`NEEDS_CONTEXT` return) as an amend item. Keep ticket rows, row add/remove, reviewer-only or taste objections, problem statement changes, and component boundary changes as redraws at batch preparation, before any standing grant: re-draft from the new design and ratify at the full gate. Keep a disposition-only change to such a row in this grammar so it keeps the cut question. Apply an item the human has answered by letter or custom text as recorded. Keep human-facing amendment text with the main loop holding `edit`/`write`; delegate implement-phase spec authorship only through the implement-only route below. Keep all other callers' authorship with the main loop. Use the human last: a fresh reviewer clears evidence-backed factual corrections; present the rest to the human once per batch plainly.
4
4
 
5
5
  ## Standing grants
6
6
 
@@ -19,7 +19,7 @@ A grant is the spec's `**Amend-grant:**` line read through the shared predicate;
19
19
  | exactly one line whose trimmed value is any other non-empty single-line text | grant active; the sentence is that value |
20
20
  | two or more lines, one line with an empty value, or a value continued onto the next physical line (any non-blank line directly below the field that is neither a `**Key:**` header line nor a `> **Superseded by:**` banner line) | malformed -> stop, print the offending line(s) |
21
21
 
22
- **Value decoding**. Run the structural predicate first; missing or `none` gives neither capability, and malformed lines stop with the offending lines quoted. Decode every active value: section 1 at each batch boundary, SDD Fix-Loop Rounds at each funded dispatch, and `spec-finalization.md` before writing the value. Match exactly one `fund N fix rounds` or `N more fix rounds` anywhere on the line, with N matching `[1-9][0-9]*`; no match funds nothing. Stop and quote the line before any item applies or any round is funded on two or more matches or a token-shaped invalid phrase (`fund 0 fix rounds`, `fund three fix rounds`, `-2 more fix rounds`). Remove the matched token and its `; ` / `, ` separators for amend decoding: a non-empty remainder keeps wording-agnostic amend permission (`auto-apply amends`, `apply spec fixes without asking`, or equivalent); an empty remainder is budget-only, with no amend permission. Keep grants without a budget unchanged. Decode `auto-apply amends; fund 3 fix rounds` as both capabilities and `2 more fix rounds` as budget-only. The budget is non-retroactive: rounds spent before the line was written do not count against or toward it. Write `none` on revocation such as `revoke auto-apply amends`, clearing both capabilities; retain funding alone by writing the token alone.
22
+ **Value decoding**. Run the structural predicate first; missing or `none` gives neither capability, and malformed lines stop with the offending lines quoted. Decode every active value: section 1 at each batch boundary, SDD Fix-Loop Rounds at each funded dispatch, and `spec-finalization.md` before writing the value. Match exactly one `fund N fix rounds` or `N more fix rounds` anywhere on the line, with N matching `[1-9][0-9]*`; no match funds nothing. Stop and quote the line before any item applies or any round is funded on two or more matches or a token-shaped invalid phrase (`fund 0 fix rounds`, `fund three fix rounds`, `-2 more fix rounds`). Remove the matched token and its `; ` / `, ` separators for amend decoding: a non-empty remainder keeps wording-agnostic amend permission (`auto-apply amends`, `apply spec fixes without asking`, or equivalent); an empty remainder is budget-only, with no amend permission. Keep grants without a budget unchanged. Decode `auto-apply amends; fund 3 fix rounds` as both capabilities and `2 more fix rounds` as budget-only. Apply the budget to the waves engine only; loop ignores the budget token and keeps only the decoded amendment permission (none for a budget-only value). The waves budget is non-retroactive: rounds spent before the line was written do not count against or toward it. Write `none` on revocation such as `revoke auto-apply amends`, clearing both capabilities; retain funding alone by writing the token alone.
23
23
 
24
24
  **Writers.** Spec-writing and the spec gate are owned by `spec-finalization.md`; this file owns the mid-flow changes:
25
25
 
@@ -169,7 +169,7 @@ reread rule; amend the block before using a changed contract. Legacy specs
169
169
  without that section retain finish-time rereading. A one-member run has no
170
170
  companion artifact or companion aftermath.
171
171
 
172
- Aftermath, once per batch, only when at least one item applied (nothing applied -> skip to the commit below). Ledger exists (line 1 `# Ledger - loop engine` in the resolved plan dir) -> skip `plan-writer` and `plan_check`. Reconcile the ledger against the approved spec dispositions: `deviates:`, `deferred:`, or `elsewhere:` -> `cut`, evidence = the disposition; factual or requirement-preserving amendment -> preserve rows; re-bounded `done` row -> `open`, evidence = `reopened by amendment`. Preserve every row's attempts and the ledger's iterations and cap. Apply evidenced drafted-row text corrections as amend items while preserving row identity; retain the cut/reopen rules above. Ticket text and row add/remove changes remain redraws. Commit spec + ledger together with subject `loop: cut row <n>` for a batch containing only one cut, otherwise `loop: amend` (including mixed batches), carrying the amendment audit body below (including each `granted` record and granting sentence); after the commit, use the driver's next ledger-path refresh to project the new statuses and continue without task-plan aftermath. Keep task-plan `add`/`update`/`init` out of this route. Project `done -> cut` as `complete -> skipped` and a re-bound `done -> open` as `complete -> pending`; count neither as a reopen. During verify, stop on a re-bound row with `Stopped: row <n> reopened by amendment during verify`; keep verify active and offer authorization to return that row to ledger implementation or stop. On authorization, invoke `loop-implementer` through the driver's `## Dispatch` and `## Validate the return` (tuple, refresh, admission, refresh), with no phase reset; close the row on disk, then rerun the step 1 gate before verify continues. Never close it with `plan_tracker update`. No plan yet -> commit the spec; continue. Plan exists -> read `git -C "<abs worktree path>" status --porcelain` (plain jj: `jj -R "<abs worktree path>" diff --summary`); collect the parent's in-flight workflow edits as worktree-relative paths, including the amended spec and staged in-place wave-repair deliverables. Dispatch `plan-writer` per `writing-plans` `## Author the plan` step 3 with an `Amend:` block naming the amended spec sections, an `In-flight:` list of those paths (the parent supplies the set; the child never infers it), and the rule "preserve every task title and wave position the amendment does not touch; edit only affected anchors and tasks", then run that section's steps 5-6 (the parent `plan_check` and its branches).
172
+ Aftermath, once per batch, only when at least one item applied (nothing applied -> skip to the commit below). Ledger exists (line 1 `# Ledger - loop engine` in the resolved plan dir) -> skip `plan-writer` and `plan_check`. Reconcile the ledger against the approved spec dispositions: `deviates:`, `deferred:`, or `elsewhere:` -> `cut`, evidence = the disposition; factual or requirement-preserving amendment -> preserve rows; re-bounded `done` row -> `open`, evidence = `reopened by amendment`. Preserve every row's attempts and the ledger's iterations and allowance. Apply evidenced drafted-row text corrections as amend items while preserving row identity; retain the cut/reopen rules above. Ticket text and row add/remove changes remain redraws. Commit spec + ledger together with subject `loop: cut row <n>` for a batch containing only one cut, otherwise `loop: amend` (including mixed batches), carrying the amendment audit body below (including each `granted` record and granting sentence); after the commit, use the driver's next ledger-path refresh to project the new statuses and continue without task-plan aftermath. Keep task-plan `add`/`update`/`init` out of this route. Project `done -> cut` as `complete -> skipped` and a re-bound `done -> open` as `complete -> pending`; count neither as a reopen. During verify, stop on a re-bound row with `Stopped: row <n> reopened by amendment during verify`; keep verify active and offer authorization to return that row to ledger implementation or stop. On authorization, invoke `loop-implementer` through the driver's `## Dispatch` and `## Validate the return` (tuple, refresh, admission, refresh), with no phase reset; close the row on disk, then rerun the step 1 gate before verify continues. Never close it with `plan_tracker update`. No plan yet -> commit the spec; continue. Plan exists -> read `git -C "<abs worktree path>" status --porcelain` (plain jj: `jj -R "<abs worktree path>" diff --summary`); collect the parent's in-flight workflow edits as worktree-relative paths, including the amended spec and staged in-place wave-repair deliverables. Dispatch `plan-writer` per `writing-plans` `## Author the plan` step 3 with an `Amend:` block naming the amended spec sections, an `In-flight:` list of those paths (the parent supplies the set; the child never infers it), and the rule "preserve every task title and wave position the amendment does not touch; edit only affected anchors and tasks", then run that section's steps 5-6 (the parent `plan_check` and its branches).
173
173
 
174
174
  At that return point, classify human-facing outcomes with this table; let writing-plans' first `Findings:` repair run as written.
175
175
 
@@ -137,6 +137,8 @@ If the council path ran, the body is its audit (`Coverage:` when present, then `
137
137
 
138
138
  Evaluate the summary in two stages (the **Degrade path** referenced in each is defined just below):
139
139
 
140
+ Before the summary `Read` below, resolve `gauntlet_setting({ key: "loopEngine" })` once for this gate. Retain `maxIterationsPerRow` (default 4) as `<n>` for both loop options. Render any settings warning as one adjacent line. Stop on an unavailable settings tool; never guess the allowance.
141
+
140
142
  1. **From the dispatch tool result, before the `Read`.** If the result is **not** an `"Output saved to: <path> (<N> KB, <M> lines)"` reference (an exit-0 save error returns the save error plus a `Full reply preserved at` / `not preserved` pointer, no inline output), or the reference reports under ~500 bytes, or over ~45 KB (the `Read` truncates at 50KB / 2000 lines, so a larger file cannot render whole) - skip the `Read` and take the degrade path. Use the reference's reported figures; do not re-derive them.
141
143
  2. **The `Read` itself, as the last content-producing tool call before composing the gate.** `Read` `$SUMMARY_PATH` and paste its contents verbatim immediately after the gate marker. If the `Read` fails, returns 0 bytes, or reports truncation - take the degrade path. The `Read` must be last: pi-condense does not protect a `/tmp` read, so any turn boundary between the `Read` and the render lets the read result be pruned, reproducing the bug.
142
144
 
@@ -167,10 +169,10 @@ New predecessor candidates at spec-writing: <path> (<title>), ... - index rows a
167
169
  1 - approve: proceed to planning under the existing amendment review; a fresh reviewer applies evidence-backed factual corrections on its own, and every other spec amendment (scope, acceptance-criteria, or contract edits, and redraws) stops for your review.
168
170
  2 - approve, auto-apply amends: every later spec amendment in this flow (corrected facts, paths, verification lines, and scope, acceptance-criteria, or public-contract edits alike) applies without asking; only a redraw (changed problem statement, component added, removed, or re-bounded) still stops for you, and the grant never stands in for a spec approval.
169
171
  3 - show council dispositions: prints the council audit verbatim, then re-presents this gate. Not an approval.
170
- 4 - approve, loop engine (experimental): skip the plan; iterate a fresh loop-implementer over the acceptance-criteria ledger; amendments stop for your review as under 1.
171
- 5 - approve, loop engine, auto-apply amends: as 4 with option 2's amendment grant.
172
+ 4 - approve, loop engine (experimental): skip the plan; iterate a fresh loop-implementer over the acceptance-criteria ledger; amendments stop for your review as under 1; allowance <n> per row.
173
+ 5 - approve, loop engine, auto-apply amends: as 4 with option 2's amendment grant, allowance <n> per row.
172
174
  6 - approve, hand off as draft PR: approve the spec's content only; push this branch and open a draft PR carrying just the spec; engine and amendment grant are chosen later by whoever runs `brainstorming #N` on that PR. Ends this flow without planning.
173
- Add "; fund N fix rounds" (or "N more fix rounds") to 1, 2, 4, or 5 to fund N extra attempts per stagnant loop row or N implement fix rounds past the escalated waves round.
175
+ Add "; fund N fix rounds" (or "N more fix rounds") to 1 or 2 to fund N implement fix rounds past the escalated waves round. Add "; allowance N" to 4 or 5 to set this run's per-row allowance.
174
176
 
175
177
  Or tell me what to change in the spec, or say "revert applied council edit <X>" to undo a specific applied edit.
176
178
  ```
@@ -185,21 +187,21 @@ If you believe the summary needs correcting, do **not** silently rewrite it - re
185
187
 
186
188
  **Revert valve.** "Revert applied council edit X" is a normal change request: revise the spec to undo edit X, re-dispatch the summarizer with a **fresh** temp path (per the re-dispatch rule below), and re-present the gate. This is cheap here - the spec is not yet plan- or code-bearing.
187
189
 
188
- Wait for the user. Interpret funded replies with this table before routing change requests:
190
+ Wait for the user. Interpret budget replies with this table before routing change requests. For 4/5, accept one optional `; allowance N` or `, allowance N` suffix with N matching `[1-9][0-9]*` and a positive safe integer. Carry the accepted value to loop entry and persist it only in the ledger, not in the spec or settings. On a malformed allowance suffix (zero, negative, non-numeric, unsafe, duplicate or trailing text), re-present the gate with exactly one clarifying question quoting it, then wait without writing; a second invalid reply leaves the gate waiting.
189
191
 
190
192
  | Reply | Action / written value |
191
193
  |---|---|
192
194
  | bare `2` | approve; persist the option-2 description above |
193
195
  | `2; fund 3 fix rounds` or `2, fund 3 fix rounds` | approve; persist that description plus `; fund 3 fix rounds`, not a change request |
194
196
  | `1` with a budget token | approve; persist `fund 3 fix rounds` alone for that token (budget-only) |
195
- | bare `5` | approve; persist the option-2 description above; engine loop |
196
- | `5; fund 3 fix rounds` or `5, fund 3 fix rounds` | approve; persist the option-2 description plus `; fund 3 fix rounds`; engine loop |
197
- | `4` with a budget token | approve; persist the token alone (budget-only); engine loop |
198
- | approval prose with a token | approve; preserve the prose collapsed to one physical line |
199
- | budget token alone (`fund 3 fix rounds`) | re-present the gate asking for `1`, `2`, `4`, or `5`; write nothing |
200
- | token with malformed N | re-present the gate quoting the token; write nothing, never imply a change request |
197
+ | bare `4` or `5` | approve; persist `none` for 4 or the option-2 description above for 5; engine loop; use the settings allowance |
198
+ | `4; allowance 6` / `4, allowance 6` or `5; allowance 6` / `5, allowance 6` | approve as the corresponding bare option; carry `run allowance: 6` to ledger seeding |
199
+ | `4` or `5` with a funding token | Funding is waves-only: re-present the gate quoting the token; write nothing and wait for a valid approval |
200
+ | waves approval prose with a funding token | approve; preserve the prose collapsed to one physical line |
201
+ | budget token alone (`fund 3 fix rounds` or `allowance 6`) | re-present the gate asking for an approval option; write nothing |
202
+ | waves funding token with malformed N, or allowance suffix on an option other than 4/5 | re-present the gate quoting the token; write nothing, never imply a change request |
201
203
 
202
- For every value to persist, read [Standing grants](amendment-surface.md#standing-grants) and run Value decoding. Decode before `edit` or `--amend`; never persist a malformed token. Use the decoded table value in the approval persistence below, including budget-only approvals. In persistence narration, quote the exact resulting `**Amend-grant:**` value in full, including any funding suffix; never substitute an option name or "option-2 description plus suffix" for the persisted text.
204
+ For every value to persist, read [Standing grants](amendment-surface.md#standing-grants) and run Value decoding. Decode before `edit` or `--amend`; never persist a malformed token. Use the decoded table value in the approval persistence below, including waves budget-only approvals. Strip only an accepted loop allowance suffix before decoding the grant; keep the run allowance separate from `**Amend-grant:**`. In persistence narration, quote the exact resulting `**Amend-grant:**` value in full, including any funding suffix; never substitute an option name or "option-2 description plus suffix" for the persisted text.
203
205
 
204
206
  On a change request (including a revert), revise the spec and re-present - mint a **fresh** temp path for the re-dispatched summarizer (never reuse a prior round's path, so stale content can never be mistaken for the new summary). On `3` or its label alone: print the audit verbatim from `git -C <abs worktree path> show -s --format=%b HEAD` (never from a held tool result; the body survives the grant's `--amend --no-edit`), then re-present the same gate - the briefing text of the gate message just rendered, reused verbatim (no summarizer re-dispatch, no read of the removed `$SUMMARY_PATH`; if that text is no longer in context, the one-line degrade note takes its place), the `Council:` line, the adjacent lines, the rendered option rows; nothing in the spec or the commit changes. `3` plus a change request is a change request. `3` plus `1`/`2`/`4`/`5` prints and re-presents, and the approval is read only from a clean `1`/`2`/`4`/`5` on the re-presented gate - a reply carrying `3` never grants. A gate-round recommit (change request or revert) re-runs the same heredoc commit with the audit as its body, so HEAD always carries it; on a revert of edit X, move X's `Applied:` line to `Rejected:` with `-> reverted at gate` and recompute the `Council:` counts from the revised audit. On approval - `1`, `approve`, or equivalent prose approves without a grant; `2`, `approve, auto-apply amends`, or equivalent prose approves and grants; `4` approves without a grant; `5` approves and grants using option 2's description - first read [Standing grants](amendment-surface.md#standing-grants) (stop if unreadable). On every implementation approval (1/2/4/5), `edit` the spec's `**Amend-grant:**` value to the decoded table value for funded replies, otherwise the granting sentence for 2/5 or `none` for 1/4. Write exactly one `**Engine:** loop` for 4/5 or `**Engine:** waves` for 1/2 directly below `**Amend-grant:**` in the header block (readers: absent -> `waves`). Then `git -C <abs worktree path> add -- <spec path>` and `git -C <abs worktree path> commit --amend --no-edit -q`, keeping the council-audit body; confirm `git -C <abs worktree path> show HEAD:<spec path>` contains both fields with the selected values and satisfies the grant predicate and Value decoding. A reply that mixes approval with a change request ("2 but rename the section") is a change request: revise, re-present, and read the grant only from the reply to the re-presented gate. If the amend or the HEAD check fails, stop and report; never proceed as granted. The plan and execution mode are mechanical derivatives, so the only human gate here is spec approval itself. Don't land the spec on `main`; it stays in the worktree and ships in the same squash commit as the implementation.
205
207
 
@@ -211,4 +213,4 @@ After implementation approval (1/2/4/5), mark the brainstorm phase complete:
211
213
  phase_tracker({ action: "complete", phase: "brainstorm" })
212
214
  ```
213
215
 
214
- Then invoke `/skill:writing-plans` on 1/2 or `/skill:loop-implementation` on 4/5 immediately, with no further prompt.
216
+ Then invoke `/skill:writing-plans` on 1/2 or `/skill:loop-implementation` on 4/5 immediately, with no further prompt. For an accepted allowance suffix on 4/5, print exactly one line `run allowance: <n>` immediately before invoking `/skill:loop-implementation`; omit that line on a bare approval. Keep this carrier in the approving turn only, never in the spec or settings.
@@ -147,9 +147,9 @@ owning skill **without** its reset-bearing entry:
147
147
  | brainstorm | [brainstorming checklist](../brainstorming/SKILL.md#checklist) | on-disk state decides the step: draft marker on line 1 of the spec file -> step 4; spec title -> step 8. Never `/skill:brainstorming` entry (it resets both trackers) |
148
148
  | approved spec with `**Engine:** loop`, no ledger | loop-implementation | after check 6, arm with `start brainstorm`, then `skip brainstorm resume: <spec path>` before driver admission; carry absolute worktree/spec paths and plan directory through Entry steps 1-5 per `reference/reconstruction.md` "Spec without plan": label Entry steps 1-5 explicitly in the response as Entry step 1: resolved settings; Entry step 2: verification; Entry step 3: driver's plan skip; Entry step 4: clean named-branch check and ledger creation; Entry step 5: `plan_check`, commit and implement start; no writing-plans body or task tracker initialization; refresh by ledger path after implement starts |
149
149
  | plan with task engine | writing-plans body | skip its `start plan` call (already in_progress) |
150
- | implement with line-1 ledger | loop-implementation | complete `reference/reconstruction.md` "Spec with ledger" ordered restore, cited by `reference/brief-contract.md`'s call table: entry checks 1-6, brainstorm start/skip, plan start/check/skip with reason `resume: <spec> (loop engine)`, implement start, pending recovery, then one ledger-path refresh, no `init`; state the entire ordered sequence explicitly even when entry checks are already reported as passed; then continue at step 5 (`Before each dispatch`), applying the shared cap boundary in reconstruction step 4 before continuation and preserving ledger cap, iterations/retry data; recover home or companion returns before refresh and any unresolved dirty-tree stop; reuse the existing ledger |
150
+ | implement with line-1 ledger | loop-implementation | complete `reference/reconstruction.md` "Spec with ledger" ordered restore, cited by `reference/brief-contract.md`'s call table: entry checks 1-6, brainstorm start/skip, plan start/check/skip with reason `resume: <spec> (loop engine)`, implement start, pending recovery against the unmigrated ledger, clean-tree migration, final allowance validation, then one ledger-path refresh, no `init`; state the entire ordered sequence explicitly even when entry checks are already reported as passed; then continue at step 5 (`Before each dispatch`), applying the shared allowance boundary in reconstruction step 4 before continuation and preserving iterations/attempts/retry decisions; defer migration through recovery stops; reuse the existing ledger |
151
151
  | implement with task plan | subagent-driven-development | re-validate `<task>` first (re-run its `Tests:`), then the task loop from the first non-complete task |
152
- | verify with `Ledger:` (including ship-stage ledger briefs restored at verify) | /skill:loop-implementation | after `start verify`, restore the saved mixed task list with `init` as `reference/brief-contract.md` specifies, then pending recovery and one ledger-path refresh through the shared cap boundary in `reference/reconstruction.md` "Spec with ledger" step 4; legacy briefs without tasks go directly to refresh; render cap and observed task/ledger state; read `After the loop` and its references; continue from `After the loop` step 1, the gate set per the loop skill's `## Gate set`; let that skill own execution, with no resume-owned gate run; retain whole-diff code review, happy path when triggered, fresh home conformance review and finishing; never start implement or synthesize Gn completion |
152
+ | verify with `Ledger:` (including ship-stage ledger briefs restored at verify) | /skill:loop-implementation | after `start verify`, restore the saved mixed task list with `init` as `reference/brief-contract.md` specifies, then pending recovery against the unmigrated ledger, clean-tree migration, final allowance validation and one ledger-path refresh through the shared allowance boundary in `reference/reconstruction.md` "Spec with ledger" step 4; legacy briefs without tasks omit init, not recovery/migration; render allowance and observed task/ledger state; read `After the loop` and its references; continue from `After the loop` step 1, the gate set per the loop skill's `## Gate set`; let that skill own execution, with no resume-owned gate run; retain whole-diff code review, happy path when triggered, fresh home conformance review and finishing; never start implement or synthesize Gn completion |
153
153
  | verify without `Ledger:` | verification-before-completion | full conformance gate; nothing carried over |
154
154
 
155
155
  ## Red flags — STOP
@@ -136,6 +136,8 @@ Plan: 2/5 done (1 in progress, 2 pending)
136
136
  or `No plan active.`. Task glyphs `✓` complete, `→` in_progress, `✗` failed, `⊘` skipped,
137
137
  `○` pending, mapping 1:1 onto `plan_tracker init` `{name, status}` statuses.
138
138
 
139
+ For ledger task rows, match the suffix pattern `^(row \d+(?: \(\S+\))?) \| .+$` against the text after `[i] ` and restore only capture 1. Treat everything after the first canonical name's ` | ` as display text, including pipes and row-like text; never derive identity from the description. For example, `row 3 | 2/5: Verify loop behavior and recovery` restores `row 3`. Retain legacy plain names and the numeric prefix form: remove the prefix pattern `^\d+/\d+: ` only when the remainder matches the canonical pattern `^row \d+(?: \(\S+\))?$`. Strip recognized labels before verify/ship `plan_tracker init` and snapshot name comparisons. Keep Gn and every other task name byte-identical, including numeric prefixes on non-ledger names. Never restore labels from a brief: the post-restore refresh recomputes descriptions and allowance from the current spec and ledger, so a changed description or denominator is not drift or a second task.
140
+
139
141
  Parse stops (print the offending line, make no tracker call):
140
142
 
141
143
  - a task line not matching `^\s*[✓→✗⊘○] \[\d+\] .+$` - the "unparsable process-state
@@ -192,14 +194,14 @@ Honor SKILL.md entry check 6's deferred stop before any process-state arming cal
192
194
  |---|---|
193
195
  | brainstorm | `start brainstorm`; `substep` if the brief shows one |
194
196
  | plan | `start brainstorm`; `skip brainstorm S`; `start plan`; `substep` if shown |
195
- | implement with ledger | Read ledger line 1; resolve its path-only `**Spec:**` header; run entry checks 1-6, naming check 6 (Grant field); `start brainstorm`; `skip brainstorm` reason `resume: <spec path>`; `start plan`; `plan_check` on the absolute ledger path; on PASS `skip plan` reason `resume: <spec> (loop engine)` using the resolved absolute spec path as `<spec>`; `start implement`; pending recovery, then one ledger-path refresh, no `init`; apply the shared cap boundary in `reconstruction.md` "Spec with ledger" step 4 before continuation; render cap, rows and decisions; continue at loop step 5. Follow `reconstruction.md` "Spec with ledger" for this self-contained ordered restore. On FAIL stop with findings, never `## Plan repair` or writing-plans. |
197
+ | implement with ledger | Read ledger line 1; resolve its path-only `**Spec:**` header; run entry checks 1-6, naming check 6 (Grant field); `start brainstorm`; `skip brainstorm` reason `resume: <spec path>`; `start plan`; `plan_check` on the absolute ledger path; on PASS `skip plan` reason `resume: <spec> (loop engine)` using the resolved absolute spec path as `<spec>`; `start implement`; pending recovery against the unmigrated ledger, clean-tree migration, final allowance validation, then one ledger-path refresh, no `init`; apply the shared allowance boundary in `reconstruction.md` "Spec with ledger" step 4 before continuation; render allowance, rows and decisions; continue at loop step 5. Follow `reconstruction.md` "Spec with ledger" for this self-contained ordered restore. On FAIL stop with findings, never `## Plan repair` or writing-plans. |
196
198
  | implement with task plan | `start brainstorm`; `skip brainstorm S`; `start plan`; `plan_check({ planPath })`; on FAIL run `## Plan repair`; on PASS `skip plan R`; `start implement`; `substep` if shown; `plan_tracker init` |
197
199
  | verify | `start brainstorm`; `skip brainstorm S`; `start plan`; `plan_check({ planPath })`; on FAIL run `## Plan repair`; on PASS `skip plan R`, then `skip implement R`; `start verify`; `substep` if shown; `plan_tracker init`. Never call `start implement`. |
198
200
  | ship | With a correctly stripped coordinated plan, apply "Coordinated admission"'s fully receipt-validated retry route instead of plan repair; otherwise as verify. Restoration stops at verify in_progress: `complete verify` needs a fresh conformance dispatch and `skip verify` would bypass a real gate. Announce that the closure review re-runs before ship |
199
201
 
200
202
  Apply reconstruction's pending recovery before dispatch or verification on every ledger restore, including verify/ship. Use the home return contract for a home tuple and the companion protocol only for an actual multi-member companion; record a validated interrupted return before continuing.
201
203
 
202
- For a ledger brief, pair by the ledger's path-only `**Spec:**` header, not basename; require the resolved spec under `flowGuards.specDirs` and the ledger under its sibling plans directory. Keep same-repository binding and human confirmation. For every ledger brief apply the shared cap boundary in `reconstruction.md` "Spec with ledger" step 4 before dispatch or verification, retaining its legacy stop and original-phase grant continuation. For verify/ship ledger briefs, use the ledger restore sequence through `skip plan`, then `skip implement R`; `start verify`; restore the full saved task list with `plan_tracker init` (names, order and mixed statuses unchanged), then apply pending recovery and one ledger-path refresh through reconstruction step 4. A legacy ledger brief without task rows goes directly to refresh after phase restoration and pending recovery. Never call `start implement` on this route or invent completed Gn tasks; refresh replaces only ledger entries and preserves every non-ledger name/status. Never send a ledger task list through task-plan repair, even on a failed ledger check. Continue verify/ship ledger briefs in `/skill:loop-implementation` `After the loop` step 1 (the gate set per the loop skill's `## Gate set`), after the already-restored `start verify`; let that skill own execution, with no resume-owned gate run, retaining whole-diff code review, happy path when triggered, fresh conformance review and finishing. Ship still stops at verify for fresh conformance.
204
+ For a ledger brief, pair by the ledger's path-only `**Spec:**` header, not basename; require the resolved spec under `flowGuards.specDirs` and the ledger under its sibling plans directory. Keep same-repository binding and human confirmation. For every ledger brief apply the shared allowance boundary in `reconstruction.md` "Spec with ledger" step 4 before dispatch or verification: recover against the unmigrated ledger, migrate only after clean admission, validate allowance, then refresh and continue the original phase. Defer migration through retained recovery stops. For verify/ship ledger briefs, use the ledger restore sequence through `skip plan`, then `skip implement R`; `start verify`; restore the full saved task list with `plan_tracker init` (canonical names restored per the task-line grammar above - the ` | ` description suffix or the legacy numeric prefix stripped; order and mixed statuses unchanged), then apply pending recovery, eligible migration/final allowance validation and one ledger-path refresh through reconstruction step 4. A legacy ledger brief without task rows omits init but keeps the same ordered recovery/migration boundary. Never call `start implement` on this route or invent completed Gn tasks; refresh replaces only ledger entries and preserves every non-ledger name/status. Never send a ledger task list through task-plan repair, even on a failed ledger check. Continue verify/ship ledger briefs in `/skill:loop-implementation` `After the loop` step 1 (the gate set per the loop skill's `## Gate set`), after the already-restored `start verify`; let that skill own execution, with no resume-owned gate run, retaining whole-diff code review, happy path when triggered, fresh conformance review and finishing. Ship still stops at verify for fresh conformance.
203
205
 
204
206
  `planPath` (task-plan implement onward): the brief carries none. Resolve as reconstruction does
205
207
  (`reconstruction.md`, "Candidates", including its `flowGuards.specDirs` resolution): the
@@ -92,10 +92,14 @@ After confirmation:
92
92
 
93
93
  1. Read ledger line 1: require `# Ledger - loop engine`. Resolve its path-only `**Spec:**` header to an absolute spec path under the worktree's configured spec directories, not by basename. Run SKILL.md entry checks 1-6 in order, including check 6, **Grant field**, on that spec, before any tracker mutation. Run Pending recovery's read-only admission-evidence check below.
94
94
  2. Call `phase_tracker({ action: "start", phase: "brainstorm" })`, then `phase_tracker({ action: "skip", phase: "brainstorm", reason: "resume: <spec path>" })`, then `phase_tracker({ action: "start", phase: "plan" })`.
95
- 3. Call `plan_check({ planPath: <absolute ledger path> })`. On FAIL, stop with findings, retaining plan in_progress. Start the message with `gauntlet: stuck` followed by the findings reason on that line; never enter `## Plan repair` or writing-plans. On PASS, call `phase_tracker({ action: "skip", phase: "plan", reason: "resume: <spec> (loop engine)" })`, then `phase_tracker({ action: "start", phase: "implement" })`. Use the resolved absolute spec path as `<spec>` in the reason. Restore implement through refresh, not `init`.
96
- 4. **Shared cap boundary (every ledger restore, before phase continuation).** Read `/skill:loop-implementation`. Skip Entry step 1 entirely; settings resolution and its unavailable-tool stop apply only to fresh seeding. Restore base, verification, iterations, `cap:`, row attempts, funding and retry allowances from the ledger/spec and branch without overwriting it. Apply pending recovery below before the refresh, including home returns whose child commit removed the tuple. On unrecoverable attribution or before-image, stop before refresh; otherwise retain any recovery stop for reporting after projection. Call `plan_tracker({ action: "refresh", path: <absolute ledger path> })` exactly once after phase restoration and recovery, before any unresolved dirty-tree stop or dispatch. On verify/ship, restore the saved mixed task list first as `brief-contract.md` specifies; on implement, use no `init`. On refresh failure, stop with its findings and leave the existing task list untouched. Read `plan_tracker status` and report its exact observed state, never an inferred list.
97
- Render `cap: <n>`, rows, iterations, attempts, funding, retry allowances and all decisions; distinguish the latest operational `row <n> ...` decision from the last physical history line. Report a retained recovery stop now, with no redispatch. Any remaining dirty tree takes the driver's discard-authorization stop without offering a cap grant. On a clean ledger missing `cap:`, stop with `ledger has no cap: line - seeded under the old loopEngine contract`, even when every row is closed; projection does not require cap. Use the driver's legacy `cap +N` grant (durable iterations + N), never settings. After the grant continue the original phase: implement at `Before each dispatch`; verify/ship at `After the loop` step 1, never reseed.
98
- 5. Continue at loop step 5, the delivered driver's `Before each dispatch`, with durable iterations and retry data; an all-closed ledger completes implement through refresh and takes `After the loop` step 0. Keep an allowance-stopped row `failed` and a cap stop without an active row; render a row whose `pending:` tuple is restored or retained as `row <n> in_progress (pending tuple restored)`. Never reset counters or reseed a validated seed.
95
+ 3. Admit exactly one positive safe-integer `cap:` or `allowance:` header; both or neither is `ledger header malformed: <observed header lines>`, requiring human repair by hand. Call `plan_check({ planPath: <absolute ledger path> })` with legacy-compatible admission. On FAIL, stop with findings, retaining plan in_progress. Start the message with `gauntlet: stuck` followed by the findings reason on that line; never enter `## Plan repair` or writing-plans. On PASS, call `phase_tracker({ action: "skip", phase: "plan", reason: "resume: <spec> (loop engine)" })`, then `phase_tracker({ action: "start", phase: "implement" })`. Use the resolved absolute spec path as `<spec>` in the reason. Restore implement through refresh, not `init`.
96
+ 4. **Shared allowance boundary (every ledger restore, before phase continuation).** Read `/skill:loop-implementation`. Skip Entry step 1 entirely; restore base, verification, iterations, row attempts and decisions from the ledger without reseeding. Apply the following order on bare-worktree and brief routes alike:
97
+ 1. Apply pending recovery below against the unmigrated ledger, including home returns whose child commit removed the tuple. Validate the original one-commit `<pre-dispatch HEAD>..HEAD` range against its unchanged before-image. For companions, retain unchanged home HEAD and admit the expected iterations-bump-plus-tuple dirt. On unrecoverable attribution or before-image, stop before refresh. Resolve ambiguous-history admission before tracker calls as required below. Retain every discard/reset and interrupted-dispatch gate; migration changes neither admission ranges nor the home HEAD used for recovery.
98
+ 2. Migrate only an admitted, clean, tuple-free ledger with no retained recovery stop. With `cap:` and no `allowance:`, resolve `gauntlet_setting({ key: "loopEngine" })` for this migration only and replace the cap line with `allowance: <maxIterationsPerRow>`. Commit only that header replacement as parent `loop: migrate allowance`; preserve iterations, every attempts cell and all decisions. On an unavailable tool, stop before any write with `cannot migrate allowance: settings tool unavailable (loopEngine.maxIterationsPerRow)`. With an existing allowance, use it without settings resolution or a migration commit. Defer migration through any retained recovery stop or dirty tree; after authorized recovery reaches a clean tuple-free tree, migrate before the next dispatch or verification.
99
+ 3. Require `allowance:` and no `cap:` before continuation, including for all-closed ledgers; otherwise stop with `ledger has no allowance: line - resume through /skill:gauntlet-resume`. For a retained recovery stop or unresolved dirty tree, defer this final validation with migration and project the unmigrated ledger solely to report the stop.
100
+ 4. Call `plan_tracker({ action: "refresh", path: <absolute ledger path> })` exactly once after phase restoration, recovery and eligible migration/final validation, before any unresolved dirty-tree stop or dispatch. On verify/ship, restore the saved mixed task list first as `brief-contract.md` specifies; on implement, use no `init`. On refresh failure, stop with its findings and leave the existing task list untouched. Read `plan_tracker status` and report its exact observed state, never an inferred list.
101
+ Render the allowance header, rows, iterations, attempts, effective allowances and all decisions; distinguish the latest operational `row <n> ...` decision from the last physical history line. Compute each effective allowance as the header plus one per matching retry decision; ignore the retry's audit ceiling. Keep `cap +N` history as inert text and restore no funded rounds. Report any retained recovery stop now with no redispatch, using the driver's unresolved-row counts and discard-authorization menu for dirty trees. Resume the original phase only after this boundary: implement at `Before each dispatch`; verify/ship at `After the loop` step 1.
102
+ 5. Continue at loop step 5, the delivered driver's `Before each dispatch`, with durable iterations and retry data; an all-closed ledger completes implement through refresh and takes `After the loop` step 0. A migrated unresolved row at or above its effective allowance stops under rule 2 without an attempt; keep an allowance-stopped row `failed`. Render a row whose `pending:` tuple is restored or retained as `row <n> in_progress (pending tuple restored)`. Never reset counters or reseed a validated seed.
99
103
 
100
104
  ### Pending recovery
101
105
 
@@ -103,7 +107,7 @@ Recover before dispatch or verification on every ledger restore. Run the read-on
103
107
 
104
108
  With no `pending:` on disk, inspect home commit history before any refresh. Find the latest home child row/seed commit that changed this ledger, even when unrelated commits follow it; read its subject and body with `git -C <worktree> log --format=%H -- <ledger path>` and `git -C <worktree> show -s --format=%B <sha>`. Exclude returns superseded by a parent admission/repair or approved ledger mutation proven by the evidence below; never replay those over the parent's current ledger.
105
109
 
106
- Treat ordinary home admission as a refresh with no parent commit or ledger mutation. Prove a later parent repair or approved mutation from its actual ledger diff, not its subject alone. Use a saved post-admission snapshot only with its observed HEAD and clean `dirty` state: the brief's core fields plus task rows, or existing parent admission/refresh output bound to that checked HEAD. Require the child to be an ancestor of the saved HEAD, that HEAD to be an ancestor of live HEAD, and the ledger to be unchanged from the saved HEAD through live HEAD and the working tree. Match the snapshot's ledger task names/statuses to that HEAD's tuple-free ledger. Treat a matching snapshot as settled evidence and retain the current ledger without replaying the old dispatch. Terminal rows, a verify phase label or the child's attribution alone prove no admission.
110
+ Treat ordinary home admission as a refresh with no parent commit or ledger mutation. Prove a later parent repair or approved mutation from its actual ledger diff, not its subject alone. Use a saved post-admission snapshot only with its observed HEAD and clean `dirty` state: the brief's core fields plus task rows, or existing parent admission/refresh output bound to that checked HEAD. Require the child to be an ancestor of the saved HEAD, that HEAD to be an ancestor of live HEAD, and the ledger to be unchanged from the saved HEAD through live HEAD and the working tree. Match the snapshot's ledger task names/statuses to that HEAD's tuple-free ledger after stripping only the recognized ledger-description suffix or legacy ledger-label prefix defined in `brief-contract.md`; keep Gn and other names byte-identical. Ignore label differences in this comparison; the post-restore refresh recomputes descriptions and allowance labels from the current spec and ledger without duplicating tasks. Treat a matching snapshot as settled evidence and retain the current ledger without replaying the old dispatch. Terminal rows, a verify phase label or the child's attribution alone prove no admission.
107
111
 
108
112
  For an unsuperseded home return, recover the commit body's first two lines as the exact `pending:` tuple and consumed `iteration:`. Require the tuple's home identity and target to match the spec and subject, its pre-dispatch SHA to be an ancestor of live HEAD, and its consumed count to equal the committed ledger's iterations and pre-dispatch ledger's iterations +1. Read the before-image at that SHA, not at `HEAD^`. On missing, malformed or mismatched attribution, or unavailable pre-dispatch bytes, stop before refresh, dispatch or verification; name the commit and failed check rather than inferring admission from terminal rows. With no home child return to recover, retain the existing ledger restore path.
109
113
 
@@ -118,7 +122,7 @@ Revalidate a clean, exactly-one-commit return through the home row below. For la
118
122
  | Tuple target | Recovery |
119
123
  |---|---|
120
124
  | Home leg or single-member `home` | Reconstruct the pre-dispatch ledger from `HEAD:<ledger path>` when HEAD is the tuple SHA, otherwise from `<pre-dispatch HEAD>:<ledger path>`. Compare live HEAD with that SHA. Admit exactly one new home commit with subject `loop: row <n> <status>` or `loop: seed evidence` only through the driver's `## Validate the return`: require a clean tree and the bounded row/decision result, allowing only the consumed iteration and pending removal header deltas. Clear the tuple through the admitted close or ordinary-miss repair. With zero new commits, keep the tuple and consumed iteration and take the interrupted-dispatch stop. Unrelated movement or multiple commits stop without redispatch or double counting. For a rejected terminal child result, restore the target row and the tuple in the working-tree ledger from the pre-dispatch copy before refresh (all dispatched non-cut rows for seed); preserve other child changes, child commits and consumed iterations for the human's discard/reset decision. |
121
- | Actual multi-member companion | Apply `/skill:loop-implementation` `reference/coordinated-legs.md` `## Resume`: require unchanged home HEAD and exactly one new companion commit with tuple attribution and bounded result evidence. Validate and record only that leg's target result or seeded state and SHA, removing the tuple in the home-ledger commit; never rerun the recovered row. Zero, unrelated or multiple commits and missing evidence take that protocol's stop, preserving attempts, consumed iterations and cap. |
125
+ | Actual multi-member companion | Apply `/skill:loop-implementation` `reference/coordinated-legs.md` `## Resume`: require unchanged home HEAD and exactly one new companion commit with tuple attribution and bounded result evidence. Validate and record only that leg's target result or seeded state and SHA, removing the tuple in the home-ledger commit; never rerun the recovered row. Zero, unrelated or multiple commits and missing evidence take that protocol's stop, preserving attempts, consumed iterations and the unmigrated header. |
122
126
 
123
127
  ## Spec with plan
124
128
 
@@ -9,7 +9,7 @@ Run this driver in the main session. Carry the absolute implementation worktree
9
9
 
10
10
  ## Entry
11
11
 
12
- 1. Read project overrides below. Resolve `gauntlet_setting({ key: "loopEngine" })` and retain `maxIterationsPerRow` (default 4). Stop on an unavailable settings tool.
12
+ 1. Read project overrides below. Resolve `gauntlet_setting({ key: "loopEngine" })` and retain `maxIterationsPerRow` (default 4). Stop on an unavailable settings tool. Select this run's seed value in step 4 and persist it only in the ledger, not in the spec or settings.
13
13
  2. Resolve verification once using this ordered table. Print `verification command: <command> (<source>)`.
14
14
 
15
15
  | Observed configuration | Action |
@@ -20,10 +20,10 @@ Run this driver in the main session. Carry the absolute implementation worktree
20
20
 
21
21
  At seed and at an authorized restore, before entering the dispatch loop, read the spec in full. Decode `**Amend-grant:**` through [amendment-surface Value decoding](../brainstorming/reference/amendment-surface.md). Capture the decoded grant, ordered leg membership, each leg's absolute checkout paths and combined spec/task Rules for dispatch and handoff; use home for zero or one leg.
22
22
 
23
- At seed and at an authorized restore, when the held membership has more than one leg, read `reference/coordinated-legs.md` IN FULL now with read at its absolute path; stop on an unreadable reference naming its path, never reconstruct it. On fresh entry apply its `## Entry` section instead of the home-only skeleton and cap calculation below. On a restore apply its `## Resume` section before the dirty-tree stop and before any pending recovery. Keep legacy and single-member ledger bytes unchanged, with no coordinated metadata.
23
+ At seed and at an authorized restore, when the held membership has more than one leg, read `reference/coordinated-legs.md` IN FULL now with read at its absolute path; stop on an unreadable reference naming its path, never reconstruct it. On fresh entry apply its `## Entry` section at step 4 instead of the home-only skeleton below, using step 4's resolved allowance seed value. On a restore apply its `## Resume` section before the dirty-tree stop and before any pending recovery. Keep legacy and single-member ledger bytes unchanged, with no coordinated metadata.
24
24
 
25
25
  3. Call `phase_tracker({ action: "skip", phase: "plan", reason: "loop engine" })`.
26
- 4. Require a clean named-branch implementation worktree. Write `<resolved plan dir>/<spec stem>-ledger.md`. Use this grammar with literal values, not placeholders:
26
+ 4. Require a clean named-branch implementation worktree. Read the literal `run allowance: <n>` line from the invoking approval turn: it carries the approved `; allowance N` (or comma) suffix. Use that value over step 1's setting; with no carrier use `maxIterationsPerRow`. Print `allowance: <n> (reply)` or `allowance: <n> (setting)` with the seeded header. Ignore the carrier once a ledger exists; resume uses its header only. Write `<resolved plan dir>/<spec stem>-ledger.md`. Use this grammar with literal values, not placeholders:
27
27
 
28
28
  ```markdown
29
29
  # Ledger - loop engine
@@ -32,7 +32,7 @@ base: <HEAD SHA before skeleton>
32
32
  verification: <resolved command>
33
33
  checks: commands
34
34
  iterations: 0
35
- cap: <n>
35
+ allowance: <n>
36
36
 
37
37
  | row | status | attempts | evidence |
38
38
  |---|---|---|---|
@@ -41,14 +41,14 @@ cap: <n>
41
41
  ## Decisions
42
42
  ```
43
43
 
44
- Use the actual repo-relative spec path alone in the Spec header. Read base with `git -C "<worktree>" rev-parse HEAD`. Index every spec AC in order, including nested disposition lines without treating them as extra ACs; keep row text only in the spec. Initialize explicit/absent `in-scope` and `venue:` rows `open`, attempts 0, evidence empty. Initialize only `deviates:`, `deferred:`, `elsewhere:` rows `cut`, attempts 0, disposition text as evidence. Compute cap once as (rows not initialized cut) * maxIterationsPerRow + 1, including the seed dispatch. Write `cap:` immediately after `iterations: 0`: 24 non-cut rows at default 4 yield 97; all-cut yields 1 and exits through rule 1 without a seed dispatch. Later cuts, reopens and requirement-preserving amendments leave cap unchanged; redraw deletes the ledger and reseeds it. Serialize evidence in one physical table cell, result lines separated by semicolons, pipes escaped, plain text without angle markup.
44
+ Use the actual repo-relative spec path alone in the Spec header. Read base with `git -C "<worktree>" rev-parse HEAD`. Index every spec AC in order, including nested disposition lines without treating them as extra ACs; keep row text only in the spec. Initialize explicit/absent `in-scope` and `venue:` rows `open`, attempts 0, evidence empty. Initialize only `deviates:`, `deferred:`, `elsewhere:` rows `cut`, attempts 0, disposition text as evidence. Write `allowance:` immediately after `iterations: 0` using step 4's seed value; never scale it by row count or seed dispatches: 24 non-cut rows at default 4 yield allowance 4. All-cut ledgers keep the same allowance and exit through rule 1 without a seed dispatch. Later cuts, reopens and requirement-preserving amendments leave allowance unchanged; redraw deletes the ledger and reseeds it. Serialize evidence in one physical table cell, result lines separated by semicolons, pipes escaped, plain text without angle markup.
45
45
  5. Call `plan_check({ planPath: <ledger path> })`. Stop with findings as a configuration error. Commit the stamped skeleton as `loop: seed ledger`; call `phase_tracker({ action: "start", phase: "implement" })`, then `plan_tracker({ action: "refresh", path: <absolute ledger path> })`, including all-cut ledgers with no seed dispatch.
46
46
 
47
- For resume, use the admitted ledger, its base and verification command, and durable iterations and cap instead of overwriting it. Skip Entry step 1 for an admitted ledger; settings apply only to fresh seeding. Apply `/skill:gauntlet-resume` pending recovery before refresh and any unresolved dirty-tree stop. Render rows, cap and decision history; stop on a dirty tree before dispatch.
47
+ For resume, use the admitted ledger, its base and verification command, and durable iterations and allowance instead of overwriting it. Skip Entry step 1 for an admitted ledger; settings apply only to fresh seeding. Apply `/skill:gauntlet-resume` pending recovery and its `loop: migrate allowance` step before refresh and any unresolved dirty-tree stop. Render rows, allowance and decision history; stop on a dirty tree before dispatch.
48
48
 
49
49
  ## Driver holds
50
50
 
51
- After seeding, restrict driver reads during implement to the ledger (including saved or committed ledger snapshots), the child's return file (one file reference), git boundary commands (`status --porcelain`, `rev-list --count`, `rev-parse`, `show -s --format=%b` for commit bodies), skill resources at their absolute paths and the overrides file. Keep rows by status, iterations, cap, the last decision, the decoded amendment grant and each leg's checkout path for dispatch, handoff and stops. Use Entry's captured grant and membership throughout the loop.
51
+ After seeding, restrict driver reads during implement to the ledger (including saved or committed ledger snapshots), the child's return file (one file reference), git boundary commands (`status --porcelain`, `rev-list --count`, `rev-parse`, `show -s --format=%b` for commit bodies), skill resources at their absolute paths and the overrides file. Keep rows by status, iterations, allowance, the last decision, the decoded amendment grant and each leg's checkout path for dispatch, handoff and stops. Use Entry's captured grant and membership throughout the loop.
52
52
 
53
53
  Never read the spec, source files, test output or logs inside the implement loop. Never diagnose a failure's cause; carry the child's evidence into the next dispatch or human stop. The only log-read exception is the last 20 lines at the human stop in After the loop step 1. Leave spec reads for seed, authorized restore and the amendment action below, outside the loop.
54
54
 
@@ -56,25 +56,26 @@ Never read the spec, source files, test output or logs inside the implement loop
56
56
 
57
57
  When the held membership has more than one leg, read `reference/coordinated-legs.md` IN FULL now with read at its absolute path; stop on an unreadable reference naming its path, never reconstruct it. Apply its `## Resume`, `## Targeting and dispatch`, `## Companion return validation`, `### Companion commit admission` and `## Amendments` sections together with this driver; never apply `## Entry` here.
58
58
 
59
- Read the ledger and require its `checks:` line per ## Gate set, then its `cap:` header before the ordered table, including before rule 1. On a missing header stop with `ledger has no cap: line - seeded under the old loopEngine contract`, even for a completed ledger; use the legacy grant below, never derive cap from settings. Count empty child commits and validation misses only after the latest `loop: retry row <n>` commit. Funding alone resets neither window. Preserve every row's attempts and the total iteration cap. Apply this ordered table, starting here after each return or human decision:
59
+ Read the ledger and require its `checks:` line per ## Gate set.
60
+
61
+ Read the ledger and require `allowance:` and no `cap:` before the ordered table, including for seed and completed ledgers; otherwise stop with `ledger has no allowance: line - resume through /skill:gauntlet-resume`. Keep `iterations:` as a dispatch counter only; it bounds nothing. Count empty child commits and validation misses only after the latest `loop: retry row <n>` commit. Raising allowance resets neither window. Preserve every row's attempts. Apply this ordered table, starting here after each return or human decision:
60
62
 
61
63
  | Order | Observed state | Action |
62
64
  |---|---|---|
63
65
  | 1 | No `open` or `failed` row | Refresh; require implement complete; continue at ## After the loop step 0. All-cut and baseline-complete ledgers exit here. |
64
- | 2 | Unresolved row attempts >= allowance | Apply row escalation below, otherwise human stop. Exempt `done` and `cut`, including third-attempt success. |
65
- | 3 | Last two child row commits both changed no file outside the ledger and no row status | Human stop, regardless of funding. Count child `loop: row ...` commits only, excluding seed and parent repair/retry/cut commits. A decision alone is not progress. |
66
- | 4 | `iterations` >= `cap` | Human stop naming the header cap value and every open/failed row with its attempts, before dispatch and before verification. Seed and interrupted dispatches count. |
67
- | 5 | Dispatch allowed | Select target by the priority below. |
66
+ | 2 | Unresolved row attempts >= effective allowance | Human stop under approval option 4 and option 5 alike. Exempt `done` and `cut`. |
67
+ | 3 | Last two child row commits both changed no file outside the ledger and no row status, or two consecutive validation misses | Human stop. Count child `loop: row ...` commits only for empty-child stagnation, excluding seed and parent repair/retry/cut commits. A decision alone is not progress. |
68
+ | 4 | Dispatch allowed | Select target by the priority below, then apply the last-attempt escalation check. |
68
69
 
69
- When a refresh leaves implement active at rule 1, take the matching human stop; never start verify. Set row allowance to 3 + funded N, increased to the latest `row <n> retry` decision's `allowed attempts <m>` when larger. Recover spent funding per row as `max(0, min(attempts, 3 + N) - 3)`; a mid-loop grant applies prospectively. Before dispatch on a funded row with attempts >= 3 and spent < N print `funded round <k> of <N> for row <n>`, with k = attempts - 3 + 1. Funding buys neither cap bypass nor empty-commit bypass nor an escalated model. Include `budget: <spent> of <N> spent` in funded stops.
70
+ When a refresh leaves implement active at rule 1, take the matching human stop; never start verify. Before evaluating rule 2, compute each row's effective allowance as the ledger's `allowance:` plus the count of that row's `row <n> retry | ... | allowed attempts <m>` lines in `## Decisions`. Count each retry once; the stored `<m>` is historical, not an absolute override. Ignore every other decision and grant in this arithmetic. Use the same rule for every coordinated entry, regardless of leg. With allowance 4 and two retries, a row has effective allowance 6 even if the historical `<m>` values are 5 and 99; raising the shared header to 7 makes it 9, while an unretried row gets 7. After migration, a row already at or above its effective allowance is exhausted. Never reset or reduce attempts.
70
71
 
71
- Use the held decoded amendment grant. A budget-only grant gives no amend permission or escalation. At rule 2, hard boundaries and rules 3-4 win before escalation: cap or child-only stagnation stops without dispatch. Preserve `STAGNANT`, `REJECT`, closureReview and grant_fix_rounds boundaries; this is implement permission, not verify credit.
72
+ Use the held decoded amendment grant for amendment choices only; ignore its waves-only budget token, retaining no amend permission for a budget-only value. Never consult this value for row escalation. Apply hard boundaries and rule 3 before escalation: stagnation stops without dispatch. Preserve `STAGNANT`, `REJECT`, closureReview and grant_fix_rounds boundaries; this is implement permission, not verify credit.
72
73
 
73
- With amend permission and no existing durable `row <n> escalated` decision, resolve `gauntlet_setting({ key: "escalationLoop" })` and require `implModel`; unavailable tool or undefined model stops with the missing capability in Problem. Append `row <n> escalated | implModel round | allowed attempts <attempts + 1>`, commit parent `loop: escalate row <n>`, print `escalated round for row <n>`, and dispatch exactly once through the normal loop-implementer target, bump and validation path. Exclude this parent commit from child-only stagnation; it starts no new empty-child or validation-miss window. Treat the escalated decision as exactly one round without raising the standing allowance. Recover identity from the ledger on resume; unrecoverable means spent. Retry, funding and amendments never erase or rearm it. Dispatch errors, non-DONE and validation failures consume it before any recommendation decision.
74
+ Select `seed` until a validated `loop: seed evidence` commit with its `row 0 done` decision and evidence on every non-cut row survives on the branch, independently of `iterations`; otherwise select the still-unresolved human retry target, then the still-failed row named by the previous decision, then first failed row, then first open row. Use the latest operational `row <n> ...` decision for targeting and the child's `previous decision:` input, skipping allowance-grant and legacy grant history while retaining the entire Decisions section. Durable status beats a decision's suggestion. Keep the retry hint only for its target.
74
75
 
75
- For attempts 5 with fund 2, append `row 2 escalated | implModel round | allowed attempts 6`, commit `loop: escalate row 2`, print `escalated round for row 2`, then attempt 6; attempts 4 and 5 paid the two funded rounds, spent remains 2. Clamp funding as above even beyond allowance, retaining prospective grants. The amend-permission escalated round is the only `implModel` dispatch.
76
+ For a numbered target at attempts == effective allowance - 1 with no existing durable `row <n> escalated` decision, resolve `gauntlet_setting({ key: "escalationLoop" })` under approval option 4 and option 5 alike. Use the returned non-empty `implModel` string, including the resolver's main-loop model fallback when the setting is unset or malformed. With the settings tool unavailable or `implModel` undefined (neither a configured model nor a main-loop model), stop before dispatch with `Problem: escalationLoop.implModel unavailable`; write no escalation decision and leave attempts at effective allowance - 1. Never substitute a default-model last attempt for that stop.
76
77
 
77
- Select `seed` until a validated `loop: seed evidence` commit with its `row 0 done` decision and evidence on every non-cut row survives on the branch, independently of `iterations`; otherwise select the still-unresolved human retry target, then the still-failed row named by the previous decision, then first failed row, then first open row. Use the latest operational `row <n> ...` decision for targeting and the child's `previous decision:` input, skipping cap grant history while retaining the entire Decisions section. Durable status beats a decision's suggestion. Keep the retry hint only for its target.
78
+ With the resolved model, append `row <n> escalated | implModel round | allowed attempts <effective allowance>`, commit parent `loop: escalate row <n>`, print `escalated round for row <n>`, and dispatch that last allowed attempt with `model: <implModel>` through the normal loop-implementer target, bump and validation path. At allowance 1, escalate the first and only allowed attempt. Exclude this parent commit from child-only stagnation; it starts no new empty-child or validation-miss window. Treat the escalated decision as exactly one round within the effective allowance, never an extra attempt. Recover the row's escalation identity from `## Decisions` on every dispatch and resume; unrecoverable means spent. Preserve `row <n> escalated` for the whole flow across retry, `rows +N`, amendments and resume. Once spent, omit the model override on every later authorized attempt, including a new last allowed attempt; never resolve another row escalation or append a second marker. With no marker and recoverable history, apply the normal last-attempt predicate after retry or `rows +N`; a previously missing model or migrated row has not spent escalation. Dispatch errors, non-DONE and validation failures consume it before any recommendation decision.
78
79
 
79
80
  ## Dispatch
80
81
 
@@ -96,7 +97,7 @@ Omit the model override except on an escalated round (`model: <implModel>`); let
96
97
 
97
98
  For home returns, apply the following boundary and table. Never project terminal child results before clean/exactly-one-commit admission.
98
99
  1. Require empty `git -C "<worktree>" status --porcelain` and exactly 1 from `git -C "<worktree>" rev-list HEAD_BEFORE..HEAD --count`. On dirty/zero/multiple commits restore the pre-dispatch target cells from the saved copy and use the separately recorded dispatch identity to restore the `pending:` tuple in the working ledger; for seed restore all dispatched non-cut rows. Preserve consumed iterations and all other child changes and commits. Refresh before the discard/reset stop. Use `Stopped: iteration <i> left the tree dirty` or `Stopped: iteration <i> added <k> commits`. Read the ledger from HEAD only after this boundary passes.
99
- 2. Validate by dispatched `target`, not iteration number, against the saved copy using this table. Require the commit body's first two lines to match the exact dispatched `pending:` tuple and consumed `iteration:` (saved iterations +1), using the persona's home attribution contract for row and seed alike. Admit only `iterations` +1 and removal of the dispatched `pending:` line; reject every other header change, including cap. Preserve row identifiers and prior decisions.
100
+ 2. Validate by dispatched `target`, not iteration number, against the saved copy using this table. Require the commit body's first two lines to match the exact dispatched `pending:` tuple and consumed `iteration:` (saved iterations +1), using the persona's home attribution contract for row and seed alike. Admit only `iterations` +1 and removal of the dispatched `pending:` line; reject every other header change, including allowance. Preserve row identifiers and prior decisions.
100
101
 
101
102
  | Target | Required changes | Miss |
102
103
  |---|---|---|
@@ -106,32 +107,32 @@ For home returns, apply the following boundary and table. Never project terminal
106
107
 
107
108
  Require ordinary decision grammar `^row <target> (done|failed) \| .{1,200} \| .{1,200}$`; an old conforming line is not a new line. Require check name plus at most three result lines in evidence, serialized with semicolons and escaped pipes as in the persona. Reject wrong targets, foreign cells, missing/malformed/new-extra decisions and changed headers.
108
109
 
109
- On an ordinary miss restore every non-target row, headers (including the saved cap) and prior decisions from the saved copy, retaining the incremented iterations. Set the target `failed`, attempts = saved attempts +1, evidence = `iteration <i>: <concrete miss>`. Clear `pending:`, remove malformed appended decisions and commit `loop: repair row <n>`, then refresh. Set previous decision to `none`; otherwise use the new conforming line. Two consecutive validation misses stop because the child is not following the contract. A conforming return resets the consecutive-miss count; parent repair commits do not count toward child-only empty stagnation.
110
+ On an ordinary miss restore every non-target row, headers (including the saved allowance) and prior decisions from the saved copy, retaining the incremented iterations. Set the target `failed`, attempts = saved attempts +1, evidence = `iteration <i>: <concrete miss>`. Clear `pending:`, remove malformed appended decisions and commit `loop: repair row <n>`, then refresh. Set previous decision to `none`; otherwise use the new conforming line. Two consecutive validation misses stop because the child is not following the contract. A conforming return resets the consecutive-miss count; parent repair commits do not count toward child-only empty stagnation.
110
111
 
111
112
  ## Human stop
112
113
 
113
114
  Read and apply [menu-layout](../brainstorming/reference/menu-layout.md). Start the message with `gauntlet: question` at every human stop, including the discard/reset authorization ask for a dirty or commit-boundary return. Put it alone on the first output line, followed by a newline before the stop text, without a driver-state preamble, wrapper or code fence. These decisions are neither done nor stuck outcomes.
114
115
 
115
- Implement-phase stops only: leave implement active and use the row/cap offers below. For verify stops use the menus in After the loop, retaining verify active. Use one screen of plain words: `Stopped on row <n>` for row failures, or the concrete boundary/cap/verification header. `Problem:` quotes the evidence cell and last decision line verbatim; attribute any spec/code contradiction, failing assertion/output or missing capability to that evidence, without investigating it. Include funding budget only when funded. At every stop print `Preserve every row's attempts and the total iteration cap.` Name `escalated round spent` when applicable; omit round history and reasoning traces.
116
+ Implement-phase stops only: leave implement active and use the row offers below. For verify stops use the menus in After the loop, retaining verify active. Use one screen of plain words: `Stopped on row <n>` for row failures, or the concrete boundary/verification header. `Problem:` quotes the evidence cell and last decision line verbatim; attribute any spec/code contradiction, failing assertion/output or missing capability to that evidence, without investigating it. At every human stop, including allowance, stagnation, missing-model, dirty-tree, commit-boundary, interrupted-dispatch and exit-verification stops, list every `open` or `failed` row after the header as `row <n>: <attempts> of <effective allowance>`, then print `Preserve every row's attempts.` Compute each denominator from the ledger using Before each dispatch; keep retry increments in `## Decisions`, never a new table column. Use restored pre-dispatch target cells at rejected-return stops, not unadmitted child results. With no open or failed rows, omit row lines but retain the preservation sentence. Name `escalated round spent` when applicable; omit round history and reasoning traces.
116
117
 
117
- Offer these concrete actions. At a clean cap stop offer `cap +N` alongside the standard `Retry with hint`, Amend and Stop options. Only the verify-step-1 verification-failure stop restricts its menu to amend/stop. Offer `cap +N` only at a clean cap or legacy missing-cap stop; dirty-tree and commit-boundary stops keep discard authorization and never offer `cap +N`. When both the row allowance and total cap are exhausted, list independent funding and cap offers despite rule 2 priority. Take separate replies: funding follows the spec's `**Amend-grant:**` contract, cap follows the row below; define no combined syntax.
118
+ Offer retry and `rows +N` only at row stops (allowance, stagnation or missing-model), alongside Amend and Stop. Dirty-tree and commit-boundary stops keep discard authorization only; interrupted-dispatch stops keep their recovery authorization, never budget offers; the verify-step-1 verification-failure stop keeps amend/stop only. A budget increase never bypasses those gates.
118
119
 
119
120
  | Human choice | Durable action |
120
121
  |---|---|
121
- | Raise cap (`cap +N`) | Parse anchored `cap +<n>` with `<n>` matching `[1-9][0-9]*` and a token boundary (end, whitespace or `\|`); reject `cap +50x`. Trim the optional remainder and optional leading pipe into the words cell. Append `cap +<n> \| <words>` under `## Decisions`; bare `cap +50` writes `cap +50 \|`. Raise the old header cap by N, or set a missing cap to durable iterations + N. Write a missing `cap:` directly after `iterations:`. Commit both changes exactly once as parent `loop: raise cap`, leaving iterations and all attempts unchanged; return to the ordered table. Malformed `cap +0`, `cap +x`, `cap -5` asks once; a second invalid reply holds the same stop with no commit. |
122
- | Retry with hint | Append `row <n> retry | <hint> | allowed attempts <attempts + 1>`, commit `loop: retry row <n>`, resume checks with that target/hint. Never reset attempts or iterations. |
122
+ | Raise allowance (`rows +N`) | Parse anchored `rows +<n>` with `<n>` matching `[1-9][0-9]*` and a token boundary (end, whitespace or `\|`); reject `rows +50x`. Trim the optional remainder and optional leading pipe into the words cell. Append `rows +<n> \| <words>` under `## Decisions`; bare `rows +50` writes `rows +50 \|`. Raise the shared header allowance by N for every row and leg. Commit both changes exactly once as parent `loop: raise allowance`, leaving iterations and all attempts unchanged; return to the ordered table. For malformed replies, including `rows +0`, `rows +x`, `rows -1` and `rows +50x`, print the accepted forms `retry <n> \| <hint>` and `rows +N` once. On the first and every later invalid reply, keep the same stop open with no writes, commit or dispatch; preserve allowance, decisions, iterations and all attempts. |
123
+ | Retry (`retry <n> \| <hint>`) | Parse the row number and hint from that reply. Append `row <n> retry \| <hint> \| allowed attempts <effective allowance + 1>`, commit `loop: retry row <n>`, resume checks with that target/hint. Preserve the header allowance, every attempts cell and iterations; increment only that row's effective allowance by one. |
123
124
  | Amend | Follow `/skill:brainstorming` amendment surface; cut only through an approved disposition amendment. Reconcile ledger and spec in one commit. |
124
125
  | Stop | Keep phase implement and preserve branch state. |
125
126
 
126
- Serialize the bare decision as `cap +50 |` with an empty words cell. A cap grant resets neither window (empty-child or validation-miss); exclude `loop: raise cap` from child-commit counting. Only retry starts new windows.
127
+ Serialize the bare decision as `rows +50 |` with an empty words cell. Raising allowance resets neither window (empty-child or validation-miss); exclude `loop: raise allowance` from child-commit counting. Only retry starts new windows.
127
128
 
128
- For dirty/commit-boundary retry, ask the human to authorize discarding uncommitted changes/resetting to last valid commit. Preserve consumed dispatch count: set iterations to restored HEAD's value +1 before the next normal bump. An interrupted dispatch consumes the same count; recover it from disk/branch rather than silently repeating iteration 0.
129
+ For dirty/commit-boundary retry, ask the human to authorize discarding uncommitted changes/resetting to last valid commit. Preserve the pre-dispatch escalation decision through authorized discard/reset; it stays spent. Boundary rejection never increments attempts: an escalated last-attempt rejection leaves attempts at effective allowance - 1, so after authorized recovery dispatch on the default model without requiring retry or `rows +N`, subject to the normal boundaries. An admitted validation miss instead increments attempts through `loop: repair row <n>` to the effective allowance and stops under rule 2, retaining the spent decision. Preserve consumed dispatch count: set iterations to restored HEAD's value +1 before the next normal bump. An interrupted dispatch consumes the same count; recover it from disk/branch rather than silently repeating iteration 0.
129
130
 
130
131
  Outside the dispatch loop, perform authorized amendment actions under the amendment surface's own reads. After the amendment commit, re-decode the grant and refresh the held membership, checkout paths and Rules from the updated spec before returning to the loop; apply this to funding and standing-grant amendments too. Leave the `**Amend-grant:**` header grammar unchanged.
131
132
 
132
- Retry, cut, amend, raise-cap and escalate commits never clear the tuple. For ledger amendments use its line-1 discriminant: no plan writer or plan_check; refresh after the amendment commit. Approved deviates/deferred/elsewhere becomes `cut` with disposition evidence; requirement-preserving amendments leave rows unchanged; a re-bounded done row becomes `open` with `reopened by amendment`. Preserve attempts, iterations and cap. Commit spec + ledger as `loop: cut row <n>` or `loop: amend`. Option 4 stops for amendments, option 5 auto-applies amends, redraw always stops and deletes the ledger with the plan. Cut only through the amendment surface, including its standing-grant section 5.
133
+ Retry, cut, amend, raise-allowance and escalate commits never clear the tuple. For ledger amendments use its line-1 discriminant: no plan writer or plan_check; refresh after the amendment commit. Approved deviates/deferred/elsewhere becomes `cut` with disposition evidence; requirement-preserving amendments leave rows unchanged; a re-bounded done row becomes `open` with `reopened by amendment`. Preserve attempts, iterations and allowance. Commit spec + ledger as `loop: cut row <n>` or `loop: amend`. Option 4 stops for amendments, option 5 auto-applies amends, redraw always stops and deletes the ledger with the plan. Cut only through the amendment surface, including its standing-grant section 5.
133
134
 
134
- End every stop with `Recommendation: <option> - <why>` or `Recommendation: none - <why>`. An unnamed repair is none. Under amend permission take only a named authorized amend: print `took recommendation: amend for row <n>`, apply amendment-surface section 5 (including its audit), commit `loop: amend` / `loop: cut row <n>`, then return to the ordered table. Whitespace-normalize the row evidence cell and compare with evidence already auto-resolved on that row; repetition renders `Recommendation: none - evidence repeats row <n>`. Recover this evidence from amendment/ledger history; missing history never authorizes a repeated action. A cut leaves the stop; an unresolved row at allowance stops again, not a free retry. Retry, more attempts, cap +N and redraw wait for the human. Fix-first enters `/skill:shape-ticket`'s own gate, never an automatic separate repair. Hard boundaries never auto-take recommendations.
135
+ End every stop with `Recommendation: <option> - <why>` or `Recommendation: none - <why>`. An unnamed repair is none. Under amend permission take only a named authorized amend: print `took recommendation: amend for row <n>`, apply amendment-surface section 5 (including its audit), commit `loop: amend` / `loop: cut row <n>`, then return to the ordered table. Whitespace-normalize the row evidence cell and compare with evidence already auto-resolved on that row; repetition renders `Recommendation: none - evidence repeats row <n>`. Recover this evidence from amendment/ledger history; missing history never authorizes a repeated action. A cut leaves the stop; an unresolved row at allowance stops again, not a free retry. Retry, `rows +N` and redraw wait for the human. Fix-first enters `/skill:shape-ticket`'s own gate, never an automatic separate repair. Hard boundaries never auto-take recommendations.
135
136
 
136
137
  ## Gate set
137
138
 
@@ -151,8 +152,8 @@ For coordinated runs, route each verification and review below to every leg with
151
152
 
152
153
  0. Call `phase_tracker({ action: "start", phase: "verify" })`. Mint `LOG_DIR=$(mktemp -d)` once for this verify session; a resumed session mints a new one at its first gate-set site.
153
154
  1. Run the gate set per ## Gate set. Require every pair to pass. Commit verification-produced tracked changes, including gate-produced changes. Any repair invalidates that run: apply the rerun scope below before the next gate. No task reopen/update is awaited.
154
- On failure use `Stopped: verification failed with every row closed`, `Problem:` naming `gate <i> exit <code> <log path>` and quoting at most the last 20 lines of that log with `tail -n 20`; this human-stop excerpt is the driver's only log read. Keep verify active and implement complete; offer amend or stop only, no `Retry with hint` or `cap +N`. Apply hard boundaries first; at cap stop without repair. With amend permission, `iterations < cap`, resolved `escalationLoop.implModel` and no spent `loop: repair verification` identity, bump iterations once and record `HEAD_BEFORE` in the failing leg. Dispatch exactly one `implementer` with `model: <implModel>`, `context: "fresh"`, `async: false`, the implementation worktree as cwd and its Rules (the failing leg's for coordinated runs), absolute spec path, failing command, its checkout, exit code and log path, never pasted output. The repair child reads the log to derive the files to touch. For a stale selector, name the row whose command no longer selects its test and restore the test or its name so the recorded command selects it again; never edit ledger evidence. Require a clean tree and exactly one commit after HEAD_BEFORE; dirty/zero/multiple commits stop preserving count and state. Require all ledger rows unchanged, preserving mapping, cap and prior decisions. The home child's sole commit includes the already-bumped ledger; a companion child commits its leg only. After validation parent commits `loop: repair verification` with the bumped home ledger, using an empty parent marker when no dirt remains. Refresh after admitted commits. This marker is once per flow.
155
- Rerun the failing checkout's gate pairs once using ## Gate set's log redirection and fail-fast rule. After the failing checkout's rerun passes, run the original fail-fast run's still-unexecuted pairs from other checkouts in ledger order under the same redirection before recording verified HEAD. Never spend verify-phase credits. A second failure retains the original header plus `escalated round spent`, then stops with the verify menu. Dispatch error or non-DONE consumes the round before a decision; preserve the consumed count and stop. Recover spent identity from the parent commit on resume; interrupted or unrecoverable identity is spent, never add a carrier or fabricate success. With no permission or spent identity: no reopening and no automatic rerun. Missing settings/model stops with the capability in Problem. On success record verified HEAD and clean status for every leg.
155
+ On failure automatically take the one repair below under approval option 4 and option 5 alike; never consult amend permission or gate on the iteration count. Keep verify active and implement complete. Apply dirty-tree and commit-boundary stops first. With spent `loop: repair verification` identity, stop without dispatch or automatic rerun. Otherwise resolve `gauntlet_setting({ key: "escalationLoop" })`; require its non-empty `implModel`, including the resolver's main-loop model fallback. Missing settings/model stops with `Problem: escalationLoop.implModel unavailable`, without a bump or spent marker. With the resolved model and no spent identity, bump iterations once and record `HEAD_BEFORE` in the failing leg. Dispatch exactly one `implementer` with `model: <implModel>`, `context: "fresh"`, `async: false`, the implementation worktree as cwd and its Rules (the failing leg's for coordinated runs), absolute spec path, failing command, its checkout, exit code and log path, never pasted output. The repair child reads the log to derive the files to touch. For a stale selector, name the row whose command no longer selects its test and restore the test or its name so the recorded command selects it again; never edit ledger evidence. Require a clean tree and exactly one commit after HEAD_BEFORE; dirty/zero/multiple commits stop preserving count and state. Require all ledger rows unchanged, preserving attempts, mapping, allowance and prior decisions. The home child's sole commit includes the already-bumped ledger; a companion child commits its leg only. After validation parent commits `loop: repair verification` with the bumped home ledger, using an empty parent marker when no dirt remains. Refresh after admitted commits. This marker is once per flow.
156
+ Rerun the failing checkout's gate pairs once using ## Gate set's log redirection and fail-fast rule. After the failing checkout's rerun passes, run the original fail-fast run's still-unexecuted pairs from other checkouts in ledger order under the same redirection before recording verified HEAD. Never spend verify-phase credits. On a second failure or a spent-identity stop, render `Stopped: verification failed with every row closed` plus `escalated round spent`, with `Problem:` naming `gate <i> exit <code> <log path>` and quoting at most the last 20 lines of that log with `tail -n 20`; this human-stop excerpt is the driver's only log read. Keep verify active and implement complete; offer amend or stop only, no `Retry with hint` or `rows +N`. Use the same restricted menu for missing capabilities. Dispatch error or non-DONE consumes the round before a decision; preserve the consumed count and stop. Recover spent identity from the parent commit on resume; interrupted or unrecoverable identity is spent, never add a carrier or fabricate success. A rejected dirty/zero/multiple-commit return also keeps the round spent through authorized recovery. Never repair again for another leg, reopen rows or reset the count. On success record verified HEAD and clean status for every leg.
156
157
  2. Dispatch foreground whole-diff `wave-reviewer` per `/skill:requesting-code-review`, from ledger base through committed HEAD, with `SCOPED_TEST_COMMANDS: none`. Apply its fix rounds. Rerun the complete gate set after every fix round before re-review/next gate. Carry final SHIP Minor lines verbatim as `whole-diff minors:`, or `whole-diff minors: none`.
157
158
  3. Run happy path per the fully read reference when overrides has `## Happy path` or the diff selects a row; use the diff-derived row, not a plan header. Pass summary and outcome to the separate fresh `conformance-reviewer`, along with spec path, verbatim original prompt and full diff. Use spec AC rows against the ledger as the checklist. Follow the reference's partition, concern decomposition, fix rounds and Convergence with the gate set replacing plan Verification at every test site per the reference's ledger-subject override. Use normal conformance Gn tasks on the same tracker and keep durable Gn identities in reports. Rerun the complete gate set after repairs. Resolve closureReview settings and reviewer dispatch per the reference. Reviews happen only after complete implement; none inside the iteration loop.
158
159
  On an approved re-bound (`reopened by amendment`), refresh and stop with `Stopped: row <n> reopened by amendment during verify`; offer authorize the row's return to ledger implementation or stop. On approval apply the normal Before each dispatch boundaries, then Dispatch, Validate the return and refresh with verify still active and no phase reset; close on disk and rerun step 1 before continuing verify. Keep completion blocked until closure; never substitute a tracker write for the ledger return.
@@ -4,13 +4,13 @@ Read this reference with the loop driver when its held membership has more than
4
4
 
5
5
  At seed, read `## Legs` with [the scope grammar](../../brainstorming/reference/legs.md#scope-grammar-and-validation) before skeleton commit. Require exactly one `scope:` per block: `scope: AC 1, AC 3` or `scope: none`. Reject missing/duplicate scope lines, duplicate indices within a list, malformed tokens and out-of-range original indices through the amendment stop. Include cut original AC indices; never infer ownership from a `design:` mention or path. Default only indices absent from every valid list to home; repeat shared indices in every owner's list.
6
6
 
7
- Retain the home skeleton's `**Spec:**`, `verification:`, `checks: commands`, `iterations:`, `cap:` and `## Decisions` fields and its `plan_check`/commit admission. Use the driver's gate set grouped by `## Leg mapping` for verification; keep the home verification header as the CI command.
7
+ Retain the home skeleton's `**Spec:**`, `verification:`, `checks: commands`, `iterations:`, `allowance:` and `## Decisions` fields and its `plan_check`/commit admission. Use the driver's gate set grouped by `## Leg mapping` for verification; keep the home verification header as the CI command.
8
8
 
9
9
  For coordinated exit, apply the driver's `## Gate set` and `## After the loop` step 1 in verify, routing each pair through its mapped checkout. Keep `loop: repair verification` once per flow, not per failing leg. Success only after every leg passes; retain each verified HEAD and clean status.
10
10
 
11
11
  Assign numeric entries in original AC order, then membership order for shared ACs, independent of scope-list order. Leave spec rows untouched and copy each cut disposition to every expanded entry. Keep the four-column table and add `## Leg mapping` with `row | ac | leg`, one mapping per entry exactly once. Reject duplicate/missing mappings. Keep `base:` as home base; record one actual `base repo-a: <sha>` / `seeded repo-a: no` pair per member, including home. Resolve bases with each member's `git -C`, not home HEAD.
12
12
 
13
- Compute initial cap as non-cut entry count times `maxIterationsPerRow` plus one seed dispatch per leg with non-cut entries. One shared home/companion AC and one home-only AC produce three entries, two seeds and cap 14 at default 4; a wholly cut third leg adds no seed. All-cut cap stays 1 and exits without seed. Preserve cap after cuts, reopens, amendments, funding and recovery; only an explicit cap grant raises it.
13
+ Use the same allowance seed value and source echo as the home driver's Entry step 4; never multiply by entry or leg count or add seed dispatches. One shared home/companion AC and one home-only AC produce three entries, two seeds and allowance 4 at default 4; a wholly cut third leg adds no seed. All-cut ledgers exit without seed and retain that allowance. Preserve allowance after cuts, reopens, requirement-preserving amendments and recovery.
14
14
 
15
15
  ## Resume
16
16
 
@@ -24,7 +24,7 @@ For coordinated dispatch, capture home and selected leg heads and save a pre-dis
24
24
 
25
25
  ## Companion return validation
26
26
 
27
- For a companion, validate from its commit, not the reply. Require a clean companion and exactly one commit after pending HEAD; require unchanged home HEAD and no home edits except the expected persisted bump/tuple. Match subject `loop: row <n> <status>` or `loop: seed evidence`, exact pending tuple and consumed `iteration:` in the body. Parse only the resulting four-column target row (all and only that leg's non-cut rows for seed), followed by one bounded decision. Apply the evidence and decision bounds in the driver's `## Validate the return` table. Reject a wrong target, foreign entry, header changes, fabricated counts, malformed evidence or extra results. Validate seed counts against actual rows and baseline closures; preserve attempts and set only this leg's seeded value to `yes` after every non-cut entry validates. Keep seed misses a human stop. Apply ordinary repair only to the target, retaining consumed iterations and saved cap.
27
+ For a companion, validate from its commit, not the reply. Require a clean companion and exactly one commit after pending HEAD; require unchanged home HEAD and no home edits except the expected persisted bump/tuple. Match subject `loop: row <n> <status>` or `loop: seed evidence`, exact pending tuple and consumed `iteration:` in the body. Parse only the resulting four-column target row (all and only that leg's non-cut rows for seed), followed by one bounded decision. Apply the evidence and decision bounds in the driver's `## Validate the return` table. Reject a wrong target, foreign entry, header changes, fabricated counts, malformed evidence or extra results. Validate seed counts against actual rows and baseline closures; preserve attempts and set only this leg's seeded value to `yes` after every non-cut entry validates. Keep seed misses a human stop. Apply ordinary repair only to the target, retaining consumed iterations and saved allowance.
28
28
 
29
29
  After validation update only that leg's target or seed entries, append the validated decision, record companion SHA in evidence, and remove `pending:` in one home-ledger commit. Preserve every other row, header and prior decision. That home-ledger commit (`loop: record <repo> row <n>`) is a parent commit, not a child `loop: row ...` commit: it never counts toward child-only empty-commit stagnation; only the companion's own commits are counted at the boundary. Record a rejected companion return in the same parent commit while preserving the pending tuple and every row. Recover interrupted returns by this same validation, never trusted reply text.
30
30
 
@@ -91,4 +91,4 @@ NODE
91
91
 
92
92
  ## Amendments
93
93
 
94
- For coordinated amendments, reconcile by original AC/leg mapping without resetting attempts, iterations or cap. Re-read explicit scope lists after approved ownership amendments and reconcile expanded entries; a disposition-only cut changes neither original indices nor ownership. Preserve unchanged mapped entries and their histories.
94
+ For coordinated amendments, reconcile by original AC/leg mapping without resetting attempts, iterations or allowance. Re-read explicit scope lists after approved ownership amendments and reconcile expanded entries; a disposition-only cut changes neither original indices nor ownership. Preserve unchanged mapped entries and their histories.