pi-gauntlet 7.43.1 → 8.0.0

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/agents/plan-writer.md +3 -3
  4. package/agents/wave-reviewer.md +111 -0
  5. package/extensions/lib/phase-tracker-helpers.test.ts +7 -8
  6. package/extensions/lib/phase-tracker-helpers.ts +2 -5
  7. package/extensions/lib/plan-check.test.ts +45 -0
  8. package/extensions/lib/plan-check.ts +19 -7
  9. package/extensions/lib/telemetry-collect.test.ts +21 -1
  10. package/extensions/lib/telemetry-collect.ts +23 -1
  11. package/extensions/phase-tracker.test.ts +80 -73
  12. package/extensions/phase-tracker.ts +10 -13
  13. package/extensions/telemetry.test.ts +11 -5
  14. package/extensions/telemetry.ts +2 -2
  15. package/package.json +1 -1
  16. package/skills/brainstorming/reference/amendment-surface.md +9 -1
  17. package/skills/chase-bug/hotfix.md +1 -1
  18. package/skills/dispatching-parallel-agents/SKILL.md +2 -2
  19. package/skills/finishing-a-development-branch/reference/disposition-protocol.md +1 -1
  20. package/skills/gatekeep-pr/SKILL.md +1 -1
  21. package/skills/gatekeep-pr/reference/assessment.md +1 -1
  22. package/skills/gatekeep-pr/reference/sync.md +1 -1
  23. package/skills/loop-implementation/SKILL.md +1 -1
  24. package/skills/requesting-code-review/SKILL.md +9 -9
  25. package/skills/requesting-code-review/{code-reviewer.md → wave-reviewer.md} +2 -2
  26. package/skills/subagent-driven-development/SKILL.md +124 -121
  27. package/skills/subagent-driven-development/reference/dispatch-contract.md +10 -9
  28. package/skills/subagent-driven-development/reference/implement-amendments.md +3 -1
  29. package/skills/subagent-driven-development/stop-note.md +4 -4
  30. package/skills/subagent-driven-development/wave-reviewer-prompt.md +55 -0
  31. package/skills/verification-before-completion/reference/conformance-check.md +2 -2
  32. package/skills/writing-plans/SKILL.md +7 -1
  33. package/skills/writing-plans/reference/plan-contract.md +6 -0
  34. package/agents/code-reviewer.md +0 -115
  35. package/agents/spec-reviewer.md +0 -121
  36. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +0 -67
  37. package/skills/subagent-driven-development/reference/fix-loop.md +0 -45
  38. package/skills/subagent-driven-development/spec-reviewer-prompt.md +0 -125
@@ -685,20 +685,31 @@ const commitResult = (id: string) => ({
685
685
  content: [{ type: "text", text: "ok" }],
686
686
  });
687
687
 
688
- test("implement-phase commit with implementer newer than both reviewers warns", async () => {
689
- const priorDepth = process.env.PI_SUBAGENT_DEPTH;
690
- delete process.env.PI_SUBAGENT_DEPTH; // isolate from an ambient subagent depth in the test-runner's own process
691
- try {
692
- const h = harness({ branch: implementBranch([subagentResult(["implementer"])]) });
693
- await h.emit("session_start");
694
- await h.emitEvent("tool_call", commitCall("c1"));
695
- const warned = (await h.emitEvent("tool_result", commitResult("c1")))[0] as { content: { text: string }[] };
696
- assert.match(warned.content[0].text, /no spec-reviewer or code-reviewer observed/);
697
- } finally {
698
- if (priorDepth === undefined) delete process.env.PI_SUBAGENT_DEPTH;
699
- else process.env.PI_SUBAGENT_DEPTH = priorDepth;
688
+ for (const path of ["README.md", "extensions/example.ts"]) {
689
+ for (const [name, agents, warns] of [
690
+ ["missing review", ["implementer"], true],
691
+ ["stale review", ["wave-reviewer", "implementer"], true],
692
+ ["newer review", ["implementer", "wave-reviewer"], false],
693
+ ] as const) {
694
+ test(`cadence for ${path}: ${name}`, async () => {
695
+ const priorDepth = process.env.PI_SUBAGENT_DEPTH;
696
+ delete process.env.PI_SUBAGENT_DEPTH; // isolate from an ambient subagent depth in the test-runner's own process
697
+ try {
698
+ const h = harness({ branch: implementBranch(agents.map((agent) => subagentResult([agent]))) });
699
+ await h.emit("session_start");
700
+ const call = commitCall("c1");
701
+ call.input.command = `git commit -m 'integrate wave' -- ${path}`;
702
+ await h.emitEvent("tool_call", call);
703
+ const warned = (await h.emitEvent("tool_result", commitResult("c1")))[0] as { content: { text: string }[] } | undefined;
704
+ assert.equal(warned !== undefined, warns);
705
+ if (warned) assert.match(warned.content[0].text, /no wave-reviewer observed/);
706
+ } finally {
707
+ if (priorDepth === undefined) delete process.env.PI_SUBAGENT_DEPTH;
708
+ else process.env.PI_SUBAGENT_DEPTH = priorDepth;
709
+ }
710
+ });
700
711
  }
701
- });
712
+ }
702
713
 
703
714
  test("cadence guard silent in subagent children", async () => {
704
715
  const priorDepth = process.env.PI_SUBAGENT_DEPTH;
@@ -715,15 +726,14 @@ test("cadence guard silent in subagent children", async () => {
715
726
  }
716
727
  });
717
728
 
718
- test("fresh SR and CR after the implementer keep the commit silent", async () => {
729
+ test("fresh wave review after the implementer keeps the commit silent", async () => {
719
730
  const priorDepth = process.env.PI_SUBAGENT_DEPTH;
720
731
  delete process.env.PI_SUBAGENT_DEPTH;
721
732
  try {
722
733
  const h = harness({
723
734
  branch: implementBranch([
724
735
  subagentResult(["implementer"]),
725
- subagentResult(["spec-reviewer"]),
726
- subagentResult(["code-reviewer"]),
736
+ subagentResult(["wave-reviewer"]),
727
737
  ]),
728
738
  });
729
739
  await h.emit("session_start");
@@ -734,12 +744,12 @@ test("fresh SR and CR after the implementer keep the commit silent", async () =>
734
744
  }
735
745
  });
736
746
 
737
- test("AND-logic: fresh SR with stale CR stays silent (doc-only wave shape)", async () => {
747
+ test("fresh wave review stays silent for a doc-only wave", async () => {
738
748
  const priorDepth = process.env.PI_SUBAGENT_DEPTH;
739
749
  delete process.env.PI_SUBAGENT_DEPTH;
740
750
  try {
741
751
  const h = harness({
742
- branch: implementBranch([subagentResult(["implementer"]), subagentResult(["spec-reviewer"])]),
752
+ branch: implementBranch([subagentResult(["implementer"]), subagentResult(["wave-reviewer"])]),
743
753
  });
744
754
  await h.emit("session_start");
745
755
  await h.emitEvent("tool_call", commitCall("c1"));
@@ -754,7 +764,7 @@ test("fused implementer+reviewer results in one dispatch stay silent", async ()
754
764
  delete process.env.PI_SUBAGENT_DEPTH;
755
765
  try {
756
766
  const h = harness({
757
- branch: implementBranch([subagentResult(["implementer", "spec-reviewer", "code-reviewer"])]),
767
+ branch: implementBranch([subagentResult(["implementer", "wave-reviewer"])]),
758
768
  });
759
769
  await h.emit("session_start");
760
770
  await h.emitEvent("tool_call", commitCall("c1"));
@@ -815,7 +825,7 @@ test("live tool_result observation updates the ledger", async () => {
815
825
  toolName: "subagent",
816
826
  toolCallId: "s1",
817
827
  content: [],
818
- details: { results: [{ agent: "spec-reviewer", exitCode: 0 }, { agent: "code-reviewer", exitCode: 0 }] },
828
+ details: { results: [{ agent: "wave-reviewer", exitCode: 0 }, { agent: "wave-reviewer", exitCode: 0 }] },
819
829
  });
820
830
  await h.emitEvent("tool_call", commitCall("c1"));
821
831
  assert.equal((await h.emitEvent("tool_result", commitResult("c1")))[0], undefined);
@@ -831,15 +841,14 @@ test("second implementer after reviews re-arms the warning", async () => {
831
841
  const h = harness({
832
842
  branch: implementBranch([
833
843
  subagentResult(["implementer"]),
834
- subagentResult(["spec-reviewer"]),
835
- subagentResult(["code-reviewer"]),
844
+ subagentResult(["wave-reviewer"]),
836
845
  subagentResult(["implementer"]),
837
846
  ]),
838
847
  });
839
848
  await h.emit("session_start");
840
849
  await h.emitEvent("tool_call", commitCall("c1"));
841
850
  const warned = (await h.emitEvent("tool_result", commitResult("c1")))[0] as { content: { text: string }[] };
842
- assert.match(warned.content[0].text, /no spec-reviewer or code-reviewer observed/);
851
+ assert.match(warned.content[0].text, /no wave-reviewer observed/);
843
852
  } finally {
844
853
  if (priorDepth !== undefined) process.env.PI_SUBAGENT_DEPTH = priorDepth;
845
854
  }
@@ -1596,15 +1605,14 @@ for (const delivery of ["live", "session_start", "session_switch", "session_fork
1596
1605
  const implementer = { agent: "implementer", exitCode: 0 };
1597
1606
  for (const row of [
1598
1607
  { name: "interrupted implementer", results: [{ ...implementer, interrupted: true }], prior: false, warns: false },
1599
- { name: "interrupted SR", results: [{ agent: "spec-reviewer", exitCode: 0, interrupted: true }], prior: true, warns: true },
1600
- { name: "interrupted CR", results: [{ agent: "code-reviewer", exitCode: 0, interrupted: true }], prior: true, warns: true },
1608
+ { name: "interrupted wave reviewer", results: [{ agent: "wave-reviewer", exitCode: 0, interrupted: true }], prior: true, warns: true },
1601
1609
  { name: "legacy implementer", results: [implementer], prior: false, warns: true },
1602
- { name: "explicit uninterrupted SR", results: [{ agent: "spec-reviewer", exitCode: 0, interrupted: false }], prior: true, warns: false },
1603
- { name: "failed reviewer", results: [{ agent: "code-reviewer", exitCode: 1 }], prior: true, warns: true },
1610
+ { name: "explicit uninterrupted wave reviewer", results: [{ agent: "wave-reviewer", exitCode: 0, interrupted: false }], prior: true, warns: false },
1611
+ { name: "failed reviewer", results: [{ agent: "wave-reviewer", exitCode: 1 }], prior: true, warns: true },
1604
1612
  { name: "wrong agent", results: [{ agent: "spec-reviewer-extra", exitCode: 0 }], prior: true, warns: true },
1605
1613
  { name: "empty async/management", results: [], prior: true, warns: true },
1606
- { name: "mixed parallel tie", results: [implementer, { agent: "spec-reviewer", exitCode: 0 }, { agent: "code-reviewer", exitCode: 0, interrupted: true }], prior: false, warns: false },
1607
- { name: "earlier review in errored chain", results: [{ agent: "code-reviewer", exitCode: 0 }, { ...implementer, interrupted: true }], prior: true, isError: true, warns: false },
1614
+ { name: "mixed parallel tie", results: [implementer, { agent: "wave-reviewer", exitCode: 0 }, { agent: "wave-reviewer", exitCode: 0, interrupted: true }], prior: false, warns: false },
1615
+ { name: "earlier review in errored chain", results: [{ agent: "wave-reviewer", exitCode: 0 }, { ...implementer, interrupted: true }], prior: true, isError: true, warns: false },
1608
1616
  ]) {
1609
1617
  const prior = row.prior ? [subagentResult(["implementer"])] : [];
1610
1618
  const result = waveResult("work", row.results, row.isError);
@@ -1618,7 +1626,7 @@ for (const delivery of ["live", "session_start", "session_switch", "session_fork
1618
1626
  await h.emitEvent("tool_call", commitCall("commit"));
1619
1627
  const [warning] = await h.emitEvent("tool_result", commitResult("commit")) as ({ content: { text: string }[] } | undefined)[];
1620
1628
  assert.equal(warning !== undefined, row.warns, row.name);
1621
- if (warning) assert.match(warning.content[0].text, /no spec-reviewer or code-reviewer observed/, row.name);
1629
+ if (warning) assert.match(warning.content[0].text, /no wave-reviewer observed/, row.name);
1622
1630
  }
1623
1631
  });
1624
1632
  }
@@ -1722,7 +1730,7 @@ test("cap guard: non-implementer dispatches never blocked; enforce false passes
1722
1730
  const h = guardedHarness({ piGauntlet: { closureReview: { maxFixRounds: 0 } } });
1723
1731
  await h.emit("session_start");
1724
1732
  assert.equal(await firstCallResult(h, "r1", { agent: "conformance-reviewer", task: "re-audit" }), undefined);
1725
- assert.equal(await firstCallResult(h, "r2", { agent: "code-reviewer", task: "review" }), undefined);
1733
+ assert.equal(await firstCallResult(h, "r2", { agent: "wave-reviewer", task: "review" }), undefined);
1726
1734
 
1727
1735
  const off = guardedHarness({ piGauntlet: { closureReview: { enforce: false, maxFixRounds: 0 } } });
1728
1736
  await off.emit("session_start");
@@ -1735,7 +1743,7 @@ test("counter: blocked, errored, empty, non-implementer, and out-of-window resul
1735
1743
  await h.emit("session_start");
1736
1744
  await h.emitEvent("tool_result", waveResult("e1", [{ agent: "implementer", exitCode: 0 }], true));
1737
1745
  await h.emitEvent("tool_result", waveResult("e2", []));
1738
- await h.emitEvent("tool_result", waveResult("e3", [{ agent: "code-reviewer", exitCode: 0 }]));
1746
+ await h.emitEvent("tool_result", waveResult("e3", [{ agent: "wave-reviewer", exitCode: 0 }]));
1739
1747
  const blockedLone = await firstCallResult(h, "b1", loneImplementer());
1740
1748
  assert.equal(blockedLone?.block, true);
1741
1749
  assert.equal(await firstCallResult(h, "c1", implementerWave(1)), undefined, "budget untouched");
@@ -1795,7 +1803,7 @@ test("counter reset: start implement and reset zero it; start verify --force kee
1795
1803
  test("replay: two implementer waves after the audit in verify restore fixRounds 2; the same waves in ship restore 0", async () => {
1796
1804
  const inVerify = guardedHarness({ piGauntlet: { closureReview: { maxFixRounds: 2 } } }, [
1797
1805
  subagentResult(["implementer"]),
1798
- subagentResult(["implementer", "code-reviewer"]),
1806
+ subagentResult(["implementer", "wave-reviewer"]),
1799
1807
  ]);
1800
1808
  await inVerify.emit("session_start");
1801
1809
  assert.equal((await firstCallResult(inVerify, "c1", implementerWave(1)))?.block, true);
@@ -1910,7 +1918,7 @@ test("grant: only qualifying synchronous parent task waves spend credits", async
1910
1918
  const h = guardedHarness({ piGauntlet: { closureReview: { maxFixRounds: 0 } } });
1911
1919
  await h.emit("session_start");
1912
1920
  await grant(h, "g", { rounds: 1, reason: "ok" });
1913
- await h.emitEvent("tool_result", waveResult("e1", [{ agent: "code-reviewer", exitCode: 0 }]));
1921
+ await h.emitEvent("tool_result", waveResult("e1", [{ agent: "wave-reviewer", exitCode: 0 }]));
1914
1922
  await h.emitEvent("tool_result", waveResult("e2", [{ agent: "implementer", exitCode: 0 }], true));
1915
1923
  await h.emitEvent("tool_result", waveResult("e3", []));
1916
1924
  assert.equal((await firstCallResult(h, "a", { ...implementerWave(1), async: true }))?.block, true);
@@ -2224,14 +2232,14 @@ for (const action of ["complete", "skip"] as const) {
2224
2232
  const h = harness({ hasUI: true });
2225
2233
  const tool = h.tools.find(t => t.name === "phase_tracker")!;
2226
2234
  await startPhase(h, "verify");
2227
- await h.emitEvent("tool_call", subagentCall("review", { agent: "code-reviewer" }));
2235
+ await h.emitEvent("tool_call", subagentCall("review", { agent: "wave-reviewer" }));
2228
2236
  const result = await tool.execute("transition", { action, phase: "ship", reason: "not needed" }, undefined, undefined, h.ctx) as any;
2229
2237
  assert.equal(result.details.error, undefined);
2230
- assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
2238
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review\)/);
2231
2239
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "review", isError, details: {}, content: [] });
2232
2240
  assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
2233
- await h.emitEvent("tool_call", subagentCall("next", { agent: "code-reviewer" }));
2234
- assert.ok(h.widgets.at(-1)!.includes(`verify(code-review${isError ? "" : " #2"})`));
2241
+ await h.emitEvent("tool_call", subagentCall("next", { agent: "wave-reviewer" }));
2242
+ assert.ok(h.widgets.at(-1)!.includes(`verify(wave-review${isError ? "" : " #2"})`));
2235
2243
  });
2236
2244
  }
2237
2245
  }
@@ -2256,17 +2264,17 @@ for (const isError of [false, true]) {
2256
2264
  const shipStart = startPhase(h, "ship");
2257
2265
  await modelEntered;
2258
2266
  await startPhase(h, "verify");
2259
- await h.emitEvent("tool_call", subagentCall("review", { agent: "code-reviewer" }));
2267
+ await h.emitEvent("tool_call", subagentCall("review", { agent: "wave-reviewer" }));
2260
2268
  release();
2261
2269
  assert.equal((await shipStart).status, "in_progress");
2262
- assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
2270
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review\)/);
2263
2271
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "review", isError, details: {}, content: [] });
2264
2272
  assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
2265
2273
  const tool = h.tools.find(t => t.name === "phase_tracker")!;
2266
2274
  const status = await tool.execute("status", { action: "status" }, undefined, undefined, h.ctx) as any;
2267
2275
  assert.equal(status.details.phases.ship.status, "in_progress");
2268
- await h.emitEvent("tool_call", subagentCall("next", { agent: "code-reviewer" }));
2269
- assert.ok(h.widgets.at(-1)!.includes(`verify(code-review${isError ? "" : " #2"})`));
2276
+ await h.emitEvent("tool_call", subagentCall("next", { agent: "wave-reviewer" }));
2277
+ assert.ok(h.widgets.at(-1)!.includes(`verify(wave-review${isError ? "" : " #2"})`));
2270
2278
  });
2271
2279
  }
2272
2280
 
@@ -2292,10 +2300,9 @@ test("live dispatch labels repaint and keep ordinals out of status snapshots", a
2292
2300
  });
2293
2301
 
2294
2302
  for (const row of [
2295
- { phase: "implement", input: { chain: [{ parallel: [{ agent: "implementer" }] }, { parallel: [{ agent: "spec-reviewer" }] }] }, label: "implement+sr" },
2296
- { phase: "implement", input: { agent: "code-reviewer" }, label: "code-review" },
2303
+ { phase: "implement", input: { chain: [{ parallel: [{ agent: "implementer" }] }, { parallel: [{ agent: "wave-reviewer" }] }] }, label: "implement" },
2297
2304
  { phase: "implement", input: { agent: "implementer" }, label: "implement" },
2298
- { phase: "implement", input: { agent: "spec-reviewer" }, label: "spec-review" },
2305
+ { phase: "implement", input: { agent: "wave-reviewer" }, label: "wave-review" },
2299
2306
  { phase: "plan", toolName: "plan_check", input: {}, label: "plan-check" },
2300
2307
  { phase: "verify", toolName: "plan_check", input: {}, label: "plan-check" },
2301
2308
  ]) {
@@ -2317,15 +2324,15 @@ for (const row of [
2317
2324
  }
2318
2325
 
2319
2326
  for (const agents of [
2320
- ["code-reviewer", "implementer", "code-reviewer", "conformance-reviewer"],
2321
- ["conformance-reviewer", "implementer", "conformance-reviewer", "code-reviewer", "code-reviewer", "code-reviewer"],
2327
+ ["wave-reviewer", "implementer", "wave-reviewer", "conformance-reviewer"],
2328
+ ["conformance-reviewer", "implementer", "conformance-reviewer", "wave-reviewer", "wave-reviewer", "wave-reviewer"],
2322
2329
  ]) {
2323
2330
  test(`live repair sequence: ${agents.join(" -> ")}`, async () => {
2324
2331
  const h = harness({ hasUI: true });
2325
2332
  await startPhase(h, "verify");
2326
2333
  const counts: Record<string, number> = {};
2327
2334
  for (const [i, agent] of agents.entries()) {
2328
- const label = ({ "code-reviewer": "code-review", implementer: "implement", "conformance-reviewer": "conformance" })[agent]!;
2335
+ const label = ({ "wave-reviewer": "wave-review", implementer: "implement", "conformance-reviewer": "conformance" })[agent]!;
2329
2336
  const n = counts[label] = (counts[label] ?? 0) + 1;
2330
2337
  await h.emitEvent("tool_call", subagentCall(String(i), { tasks: [{ agent }] }));
2331
2338
  assert.ok(h.widgets.at(-1)!.includes(`verify(${label}${n > 1 ? ` #${n}` : ""})`));
@@ -2343,7 +2350,7 @@ for (const row of [
2343
2350
  test(`live terminal: ${row.name}, execution-end-only=${endOnly}`, async () => {
2344
2351
  const h = harness({ hasUI: true });
2345
2352
  await startPhase(h, "verify");
2346
- await h.emitEvent("tool_call", subagentCall("a", { agent: "code-reviewer" }));
2353
+ await h.emitEvent("tool_call", subagentCall("a", { agent: "wave-reviewer" }));
2347
2354
  const paints = h.widgets.length;
2348
2355
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "other", details: {}, content: [] });
2349
2356
  assert.equal(h.widgets.length, paints);
@@ -2354,8 +2361,8 @@ for (const row of [
2354
2361
  assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
2355
2362
  await h.emitEvent("tool_execution_end", { ...terminal, result: { details: row.details } });
2356
2363
  assert.equal(h.widgets.length, paints + 1);
2357
- await h.emitEvent("tool_call", subagentCall("b", { agent: "code-reviewer" }));
2358
- assert.ok(h.widgets.at(-1)!.includes(`verify(code-review${row.refund ? "" : " #2"})`));
2364
+ await h.emitEvent("tool_call", subagentCall("b", { agent: "wave-reviewer" }));
2365
+ assert.ok(h.widgets.at(-1)!.includes(`verify(wave-review${row.refund ? "" : " #2"})`));
2359
2366
  });
2360
2367
  }
2361
2368
  }
@@ -2366,20 +2373,20 @@ test("live overlap forgets older calls and explicit overwrite survives cleanup",
2366
2373
  const tool = h.tools.find(t => t.name === "phase_tracker")!;
2367
2374
  const explicit = (substep: string) => tool.execute("explicit", { action: "substep", phase: "verify", substep }, undefined, undefined, h.ctx);
2368
2375
  await explicit("before");
2369
- for (const id of ["a", "b"]) await h.emitEvent("tool_call", subagentCall(id, { agent: "code-reviewer", async: true }));
2370
- assert.match(h.widgets.at(-1)!, /verify\(code-review #2\)/);
2376
+ for (const id of ["a", "b"]) await h.emitEvent("tool_call", subagentCall(id, { agent: "wave-reviewer", async: true }));
2377
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review #2\)/);
2371
2378
  const paints = h.widgets.length;
2372
2379
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "a", details: {}, content: [] });
2373
2380
  assert.equal(h.widgets.length, paints);
2374
2381
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "b", details: {}, content: [] });
2375
2382
  assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
2376
- await h.emitEvent("tool_call", subagentCall("c", { agent: "code-reviewer" }));
2383
+ await h.emitEvent("tool_call", subagentCall("c", { agent: "wave-reviewer" }));
2377
2384
  const snapshot = await explicit("custom") as any;
2378
2385
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "c", isError: true, details: {}, content: [] });
2379
2386
  assert.match(h.widgets.at(-1)!, /verify\(custom\)/);
2380
2387
  assert.equal(snapshot.details.phases.verify.substep, "custom");
2381
- await h.emitEvent("tool_call", subagentCall("d", { agent: "code-reviewer" }));
2382
- assert.match(h.widgets.at(-1)!, /verify\(code-review #3\)/);
2388
+ await h.emitEvent("tool_call", subagentCall("d", { agent: "wave-reviewer" }));
2389
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review #3\)/);
2383
2390
  });
2384
2391
 
2385
2392
  for (const action of ["start", "complete", "skip", "reset"]) {
@@ -2387,10 +2394,10 @@ for (const action of ["start", "complete", "skip", "reset"]) {
2387
2394
  const h = harness({ hasUI: true });
2388
2395
  const tool = h.tools.find(t => t.name === "phase_tracker")!;
2389
2396
  await startPhase(h, "verify");
2390
- await h.emitEvent("tool_call", subagentCall("old", { agent: "code-reviewer" }));
2397
+ await h.emitEvent("tool_call", subagentCall("old", { agent: "wave-reviewer" }));
2391
2398
  const rejected = await tool.execute("bad", { action: "start", phase: "plan" }, undefined, undefined, h.ctx) as any;
2392
2399
  assert.ok(rejected.details.error);
2393
- assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
2400
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review\)/);
2394
2401
  await tool.execute("transition", { action, phase: "verify", reason: "test" }, undefined, undefined, h.ctx);
2395
2402
  if (action === "reset") assert.equal(h.widgets.at(-1), undefined);
2396
2403
  if (action !== "start") await tool.execute("restart", { action: "start", phase: "verify", force: true }, undefined, undefined, h.ctx);
@@ -2399,18 +2406,18 @@ for (const action of ["start", "complete", "skip", "reset"]) {
2399
2406
  await h.emitEvent("tool_execution_end", { toolName: "subagent", toolCallId: "old", result: { details: {} } });
2400
2407
  assert.equal(h.widgets.length, paints);
2401
2408
  assert.match(h.widgets.at(-1)!, /verify\(custom\)/);
2402
- await h.emitEvent("tool_call", subagentCall("new", { agent: "code-reviewer" }));
2403
- assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
2409
+ await h.emitEvent("tool_call", subagentCall("new", { agent: "wave-reviewer" }));
2410
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review\)/);
2404
2411
  });
2405
2412
  }
2406
2413
 
2407
2414
  test("live plan backstop clears ownership only on actual implement completion", async () => {
2408
2415
  const h = harness({ hasUI: true });
2409
2416
  await startPhase(h, "implement");
2410
- await h.emitEvent("tool_call", subagentCall("old", { agent: "code-reviewer" }));
2417
+ await h.emitEvent("tool_call", subagentCall("old", { agent: "wave-reviewer" }));
2411
2418
  for (const tasks of [undefined, [{ status: "in_progress" }]]) {
2412
2419
  await h.emitEvent("tool_execution_end", { toolName: "plan_tracker", result: { details: { tasks } } });
2413
- assert.match(h.widgets.at(-1)!, /implement\(code-review\)/);
2420
+ assert.match(h.widgets.at(-1)!, /implement\(wave-review\)/);
2414
2421
  }
2415
2422
  await h.emitEvent("tool_execution_end", { toolName: "plan_tracker", result: { details: { tasks: [{ status: "complete" }] } } });
2416
2423
  await startPhase(h, "verify");
@@ -2418,8 +2425,8 @@ test("live plan backstop clears ownership only on actual implement completion",
2418
2425
  await tool.execute("label", { action: "substep", phase: "verify", substep: "custom" }, undefined, undefined, h.ctx);
2419
2426
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "old", details: {}, content: [] });
2420
2427
  assert.match(h.widgets.at(-1)!, /verify\(custom\)/);
2421
- await h.emitEvent("tool_call", subagentCall("new", { agent: "code-reviewer" }));
2422
- assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
2428
+ await h.emitEvent("tool_call", subagentCall("new", { agent: "wave-reviewer" }));
2429
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review\)/);
2423
2430
  });
2424
2431
 
2425
2432
  test("live observation is dormant without a phase and in a subagent child", async () => {
@@ -2509,7 +2516,7 @@ for (const row of [
2509
2516
  if (status.details.phases[row.phase].status !== "in_progress") await startPhase(h, row.phase);
2510
2517
  const restarted = await tool.execute("status", { action: "status" }, undefined, undefined, h.ctx) as any;
2511
2518
  assert.equal(restarted.details.phases[row.phase].substep, row.expected);
2512
- await h.emitEvent("tool_call", subagentCall("new", { agent: "code-reviewer" }));
2519
+ await h.emitEvent("tool_call", subagentCall("new", { agent: "wave-reviewer" }));
2513
2520
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "new", details: {}, content: [] });
2514
2521
  assert.deepEqual(row.branch, original);
2515
2522
  });
@@ -2521,11 +2528,11 @@ for (const event of ["session_start", "session_switch", "session_fork", "session
2521
2528
  const h = harness({ hasUI: true });
2522
2529
  const tool = h.tools.find(t => t.name === "phase_tracker")!;
2523
2530
  await startPhase(h, "verify");
2524
- await h.emitEvent("tool_call", subagentCall("first", { agent: "code-reviewer" }));
2531
+ await h.emitEvent("tool_call", subagentCall("first", { agent: "wave-reviewer" }));
2525
2532
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "first", details: {}, content: [] });
2526
- await h.emitEvent("tool_call", subagentCall("old", { agent: "code-reviewer" }));
2527
- assert.match(h.widgets.at(-1)!, /verify\(code-review #2\)/);
2528
- const branch = kind === "empty" ? [assistant()] : kind === "explicit" ? explicitReplay("verify", "custom") : [phaseResult("start", replaySnapshot("verify")), phaseResult("status", replaySnapshot("verify", "code-review"))];
2533
+ await h.emitEvent("tool_call", subagentCall("old", { agent: "wave-reviewer" }));
2534
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review #2\)/);
2535
+ const branch = kind === "empty" ? [assistant()] : kind === "explicit" ? explicitReplay("verify", "custom") : [phaseResult("start", replaySnapshot("verify")), phaseResult("status", replaySnapshot("verify", "wave-review"))];
2529
2536
  const original = structuredClone(branch);
2530
2537
  h.setBranch(branch);
2531
2538
  await h.emit(event);
@@ -2540,11 +2547,11 @@ for (const event of ["session_start", "session_switch", "session_fork", "session
2540
2547
  await h.emitEvent("tool_execution_end", { toolName: "subagent", toolCallId: "old", result: { details: {} } });
2541
2548
  assert.equal(h.widgets.length, paints);
2542
2549
  if (kind === "empty") await startPhase(h, "verify");
2543
- await h.emitEvent("tool_call", subagentCall("next", { agent: "code-reviewer" }));
2544
- assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
2550
+ await h.emitEvent("tool_call", subagentCall("next", { agent: "wave-reviewer" }));
2551
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review\)/);
2545
2552
  await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "next", details: {}, content: [] });
2546
- await h.emitEvent("tool_call", subagentCall("again", { agent: "code-reviewer" }));
2547
- assert.match(h.widgets.at(-1)!, /verify\(code-review #2\)/);
2553
+ await h.emitEvent("tool_call", subagentCall("again", { agent: "wave-reviewer" }));
2554
+ assert.match(h.widgets.at(-1)!, /verify\(wave-review #2\)/);
2548
2555
  assert.deepEqual(branch, original);
2549
2556
  });
2550
2557
  }
@@ -133,10 +133,10 @@ const unfinishedTaskLines = (tasks: Task[]): string[] =>
133
133
  );
134
134
 
135
135
  // Review-cadence guard (spec 2026-08-12-execution-fidelity-hardening): presence-only
136
- // advisory ledger of the most recent completed implementer / spec-reviewer /
137
- // code-reviewer dispatch. Agents completing in the SAME dispatch share a sequence
138
- // number, so a fused implementer+reviewer result never reads as reviewer-stale.
139
- const CADENCE_AGENTS = ["implementer", "spec-reviewer", "code-reviewer"] as const;
136
+ // advisory ledger of the most recent completed implementer / wave-reviewer dispatch.
137
+ // Agents completing in the SAME dispatch share a sequence number, so a fused
138
+ // implementer+reviewer result never reads as reviewer-stale.
139
+ const CADENCE_AGENTS = ["implementer", "wave-reviewer"] as const;
140
140
  type CadenceAgent = (typeof CADENCE_AGENTS)[number];
141
141
 
142
142
  const completedCadenceAgents = (details: unknown): CadenceAgent[] => {
@@ -152,9 +152,9 @@ const completedCadenceAgents = (details: unknown): CadenceAgent[] => {
152
152
  };
153
153
 
154
154
  const REVIEW_CADENCE_WARNING =
155
- "⚠️ Reminder: no spec-reviewer or code-reviewer observed since the last implementer.\n" +
156
- "SR is required per task; CR per wave for code waves (doc-only waves are SR-only).\n" +
157
- "Dispatch the missing review(s) before committing integrated work.";
155
+ "Reminder: no wave-reviewer observed since the last implementer.\n" +
156
+ "One wave-reviewer is required per wave, for both code and documentation.\n" +
157
+ "Dispatch the missing wave review before committing integrated work.";
158
158
 
159
159
  const CLOSURE_GATE_ERROR =
160
160
  "Error: cannot complete 'verify': no conformance-reviewer dispatch observed.\n" +
@@ -476,8 +476,7 @@ export default function (pi: ExtensionAPI) {
476
476
  let cadenceSeq = 0;
477
477
  const cadenceLastSeen: Record<CadenceAgent, number> = {
478
478
  implementer: 0,
479
- "spec-reviewer": 0,
480
- "code-reviewer": 0,
479
+ "wave-reviewer": 0,
481
480
  };
482
481
  const observeCadence = (details: unknown) => {
483
482
  const agents = completedCadenceAgents(details);
@@ -550,8 +549,7 @@ export default function (pi: ExtensionAPI) {
550
549
  pendingGuardWarnings.clear();
551
550
  cadenceSeq = 0;
552
551
  cadenceLastSeen.implementer = 0;
553
- cadenceLastSeen["spec-reviewer"] = 0;
554
- cadenceLastSeen["code-reviewer"] = 0;
552
+ cadenceLastSeen["wave-reviewer"] = 0;
555
553
  for (const entry of ctx.sessionManager.getBranch()) {
556
554
  if (entry.type === "custom_message") {
557
555
  if (entry.customType === RECOVERY_CUSTOM_TYPE) {
@@ -813,8 +811,7 @@ export default function (pi: ExtensionAPI) {
813
811
  phases.implement.status === "in_progress" &&
814
812
  parseGitCommit(command) &&
815
813
  cadenceLastSeen.implementer > 0 &&
816
- cadenceLastSeen.implementer > cadenceLastSeen["spec-reviewer"] &&
817
- cadenceLastSeen.implementer > cadenceLastSeen["code-reviewer"]
814
+ cadenceLastSeen.implementer > cadenceLastSeen["wave-reviewer"]
818
815
  ) {
819
816
  warnings.push(REVIEW_CADENCE_WARNING);
820
817
  }
@@ -755,8 +755,8 @@
755
755
  { agent: "implementer", exitCode: 0, model: "p/impl:high", usage: usage(100, 10, 1) },
756
756
  { agent: "implementer", exitCode: 1, model: "p/impl:high", usage: usage(50, 5, 0.5) },
757
757
  { agent: "spec-summarizer", exitCode: 0, model: "p/s", usage: usage(1) },
758
- { agent: "code-reviewer", exitCode: 0, model: "p/r", usage: usage(1) },
759
- ], "- [major] a\n- [minor] b\n- [minor] c"));
758
+ { agent: "wave-reviewer", exitCode: 0, model: "p/r", usage: usage(1) },
759
+ ], `## Outputs\n### review\n\`\`\`json\n{"structured":{"findings":{"critical":0,"moderate":1,"minor":2}}}\n\`\`\`\nOutput saved to: /tmp/review.md\n[major] duplicate [minor] duplicate`));
760
760
  await h.emit("tool_result", subagentResult([]));
761
761
  const rec = h.readRecord();
762
762
  const dispatches = rec.events.filter((e) => e.kind === "dispatch") as any[];
@@ -765,13 +765,19 @@
765
765
  assert.equal(dispatches[1].exit, 1);
766
766
  assert.deepEqual(rec.derived.personas.implementer, { dispatches: 2, models: ["p/impl:high"], tokens: { input: 150, output: 15, cache_read: 0, cache_write: 0, cost: 1.5 } });
767
767
  assert.equal(rec.derived.gates.spec_rounds, 1);
768
- assert.deepEqual(rec.derived.reviews?.["code-reviewer"], { dispatches: 1, nonzero_exit: 0, findings: { blocker: 0, major: 1, minor: 2 } });
768
+ assert.deepEqual(rec.derived.reviews?.["wave-reviewer"], { dispatches: 1, nonzero_exit: 0, findings: { blocker: 0, major: 1, minor: 2 } });
769
769
  assert.equal(rec.derived.conformance_loops, 0);
770
770
  assert.deepEqual(rec.derived.phases.plan?.tokens, { input: 152, output: 15, cache_read: 0, cache_write: 0, cost: 1.5 });
771
771
  });
772
772
 
773
+ test("zero structured findings do not fall back to inline severity tags", async () => {
774
+ const h = await boundInPlan();
775
+ await h.emit("tool_result", subagentResult([{ agent: "wave-reviewer", exitCode: 0 }], '## Outputs\n### Step 1\n#### Task 1 (wave-reviewer)\n```json\n{"structured":{"findings":{"critical":0,"moderate":0,"minor":0}}}\n```\nOutput saved to: /tmp/review.md\n[Critical] duplicate'));
776
+ assert.deepEqual(h.readRecord().derived.reviews?.["wave-reviewer"]?.findings, { blocker: 0, major: 0, minor: 0 });
777
+ });
778
+
773
779
  for (const [name, agents] of [
774
- ["chain", ["implementer", "spec-reviewer", "spec-reviewer"]],
780
+ ["chain", ["implementer", "wave-reviewer", "wave-reviewer"]],
775
781
  ["plain tasks", ["implementer", "implementer"]],
776
782
  ["empty results", []],
777
783
  ["missing results", undefined],
@@ -798,7 +804,7 @@
798
804
  const retained = results!.filter((r) => r.agent === agent);
799
805
  assert.deepEqual(rec.derived.personas[agent], { dispatches: retained.length, models: retained.map((r) => r.model), tokens: { input: 10 * retained.length, output: 2 * retained.length, cache_read: 0, cache_write: 0, cost: 0.5 * retained.length } });
800
806
  }
801
- if (name === "chain") assert.deepEqual(rec.derived.reviews?.["spec-reviewer"], { dispatches: 2, nonzero_exit: 1 });
807
+ if (name === "chain") assert.deepEqual(rec.derived.reviews?.["wave-reviewer"], { dispatches: 2, nonzero_exit: 1 });
802
808
  if (results?.length) assert.deepEqual(rec.derived.phases.implement?.tokens, { input: 10 * results.length, output: 2 * results.length, cache_read: 0, cache_write: 0, cost: 0.5 * results.length });
803
809
  else assert.deepEqual(rec, before);
804
810
  assert.deepEqual(rec.events.filter((e) => e.kind === "phase" || e.kind === "gate"), before.events.filter((e) => e.kind === "phase" || e.kind === "gate"));
@@ -21,7 +21,7 @@
21
21
  import { sha256 } from "./lib/plan-check.ts";
22
22
  import { SUPERSEDED_BY_RE, isPlanPath, isSpecPath, isSupersededByBanner, matchTestStatement, parseSpecLinks, planSpecHeader, recordPathFor, repoRelativeToolPath, toPosix, truncateCommand } from "./lib/telemetry-paths.ts";
23
23
  import { computeJjDiff } from "./lib/telemetry-diff.ts";
24
- import { COUNTED_USER_PHASES, REVIEWER_AGENTS, countFindings, countOpenGaps, hasReopen, insertedText, planTotals, textOf, usageToTokens } from "./lib/telemetry-collect.ts";
24
+ import { COUNTED_USER_PHASES, REVIEWER_AGENTS, countFindings, countStructuredFindings, countOpenGaps, hasReopen, insertedText, planTotals, textOf, usageToTokens } from "./lib/telemetry-collect.ts";
25
25
  import { addTokens, capEvents, compact, currentPhase, derive, diffPhases, emptyAccumulators, emptyPhases, foldAccumulators, implementAutoCompletes, liveShipEvent, newRecord, parseRecord, serializeRecord, type Accumulators, type BaseEvent, type PhaseAcc, type PhaseKey, type PhaseMap, type TelemetryEvent, type TelemetryRecord, type Tokens } from "./lib/telemetry-record.ts";
26
26
  import { guardReason } from "./lib/telemetry-ship.ts";
27
27
  import { buildCouncil, parseAudit, parseChairReport, resolveMembers, type Cluster, type MemberResult } from "./lib/telemetry-council.ts";
@@ -704,7 +704,7 @@
704
704
  const review = (acc.reviews[agent] ??= { dispatches: 0, nonzero_exit: 0 });
705
705
  review.dispatches += 1;
706
706
  if (exit !== undefined && exit !== 0) review.nonzero_exit += 1;
707
- const findings = countFindings(text);
707
+ const findings = countStructuredFindings(text) ?? countFindings(text);
708
708
  if (findings) {
709
709
  review.findings ??= { blocker: 0, major: 0, minor: 0 };
710
710
  review.findings.blocker += findings.blocker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gauntlet",
3
- "version": "7.43.1",
3
+ "version": "8.0.0",
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",
@@ -38,6 +38,14 @@ printf 'grant: %s\n' "<new value>" | git -C <abs worktree path> commit -q -F - -
38
38
 
39
39
  When the spec file is mid-batch (uncommitted amend edits), the consent edit rides in that batch's `amend:` commit and the footer acknowledgment says so. Either way, verify `git -C <abs worktree path> show HEAD:<spec path>` satisfies the predicate with the new value; otherwise stop and report. After the line exists, every later change is a same-line value edit; a grant given in a resumed session is an ordinary mid-flow grant.
40
40
 
41
+ ## Implement-local maintenance boundaries
42
+
43
+ Apply this specialization only to implement callers with a task plan; keep planning callers and ordinary attribution/restoration below unchanged. Patch only affected task entries, coverage and shifted anchors, preserving unaffected ids, titles, wave order, state and historical evidence. Capture anchored obligation text, task bodies and Tests alongside the original inventory in the saved apply report; retain it on failure and use it on idempotent retry. Interpret `tasksAnchorChanged` semantically, not as offset-only or meaning-preserving rewording changes.
44
+
45
+ For spec/plan maintenance requiring no test execution, admit declared `In-flight:` repairs and preserve staged repairs; never commit, restore or discard them merely to plan. Retain original declarations and unchanged evidence at measured revisions under `**Baseline policy:** implement-local`. Execute changed/added/missing/invalid evidence only on an identified execution tree, recording leg, checkout, revision, admitted changes, command, exit and nonzero executed-test count. Never call dirty execution clean HEAD; stop on unidentified trees, unknown dirty paths, contamination or moved test revision.
46
+
47
+ Match a retained failure by failing command, assertion or path together with corresponding repair evidence, never task membership alone. Keep failed revision/output and resolving evidence in the report. Return outstanding repairs to the existing task gate loop, not repeated baseline refreshes or unrelated reopening; keep unrelated regressions substantive blockers. Decode the standing grant at every batch boundary; with amend permission take the named recommendation without reviewer, menu or another confirmation. Repeated authorized intermediate evidence is neither new consent nor a refresh trigger; do not reapply it or force the ordinary repeated-evidence exit for an already authorized intermediate disposition. Preserve budget-only, malformed-grant, redraw-only/mixed, unidentified-test-tree and unrelated-regression stops. Keep one amendment audit and existing funding; integrated tests, review and final verification remain mandatory.
48
+
41
49
  ## 1. Prepare - never apply yet
42
50
 
43
51
  **Implement callers only, with a task plan:** delegate preparation to `plan-writer` in a one-step chain with the ordinary five absolute paths, this surface's absolute path, and an implement-only `Amend-prepare:` block carrying evidence already in context (checker FAIL lines, structured `concerns`/`question`, or baseline rows). Consume `{ items: [{ evidence, recommended: "accept" | "alt-<n>", location: "<spec section>", edit: string }], redraws: [{ location: "<spec section>", reason: string (max 300 characters) }] }`; keep supporting context in the saved report. Require both `items` and `redraws` arrays, including when empty; redraw proposals appear only in `redraws`, never in `items`. Fail malformed preparation output. Include the literal sentence `Do not edit any file.` in the `Amend-prepare:` task text. The child reads spec and plan, writes nothing, and bounds each proposed edit to one paragraph. Enforce the redraw gate: a nonempty `redraws` array holds the whole batch before grant evaluation or applying any sibling item. Hold amend siblings for re-batching after the redraw resolves; the primary never opens the preparation report to discover redraws. Run the standing-grant predicate and Value decoding unchanged; without amend permission, take sections 2-4 and stop for the human's answer. Forward only resolved, authorized items to the apply route in section 5. Planning callers keep the ordinary preparation below. For ledger-engine (`loop-implementation`) callers, prepare through the ordinary path below without a `plan-writer` dispatch.
@@ -145,7 +153,7 @@ Reply grammar: one line per item, `<n>. <letter or free text>`; bare `1` applies
145
153
 
146
154
  Before the aftermath, `read` this section again; never run it from memory.
147
155
 
148
- **Implement callers only, with a task plan:** replace parent spec edits and the ordinary plan aftermath with a one-step chain to `plan-writer` carrying an implement-only `Amend-apply:` block, the granted items verbatim, the five absolute paths, and the parent's explicit `In-flight:` list. Authorize only those named spec sections. Require ordinary `Amend:` preservation of unaffected titles and wave positions, Baseline ownership, dirty-baseline safety, and the child's self-check. Consume `{ amendedSections: string[], tasksAdded: number[], tasksAnchorChanged: number[], tasksRemoved: number[], passed: boolean, failLines?: string[] }`; use pre-apply task numbers, never tracker positions. Require the child to capture the original task numbers, titles, spec anchors, and wave positions in the existing saved apply report before the first apply and retain that original inventory on every return, including failures; add no sidecar. On `passed: false`, repeat once with `failLines` as the rule and `previousApplyReport` set to the first apply report path. Require the fresh retry worker to read that original inventory, apply granted edits idempotently without duplicating already-applied edits, and compute deltas against that original inventory rather than the already-mutated plan. Missing original inventory fails the retry; send a second failure to the human with verbatim findings, including substantive baseline failures. Keep the attribution, protected-path, and dirty-baseline exits below; no delegated restoration permission is granted. On pass, print `amended the spec: <title> - <location>` for each applied item, run the parent's `plan_check` for stamping, and use its fresh manifest with the returned task numbers without reading the plan or spec. Add `tasksAdded`, reopen completed `tasksAnchorChanged` as `in_progress`, and handle `tasksRemoved` through the removal rules below. Require parent PASS before tracker changes. Skip the ordinary parent edit, `Amend:` dispatch, and amended-plan read; preserve companion aftermath, audit, and the single batch commit below. Planning callers keep the ordinary apply and aftermath below; ledger and no-plan routes stay unchanged.
156
+ **Implement callers only, with a task plan:** replace parent spec edits and the ordinary plan aftermath with a one-step chain to `plan-writer` carrying an implement-only `Amend-apply:` block, the granted items verbatim, the five absolute paths, and the parent's explicit `In-flight:` list. Authorize only those named spec sections. Require implement-local patching and historical Baseline under the boundaries above, preservation of unaffected titles and wave positions, Baseline ownership, dirty-baseline safety, and the child's self-check. Consume `{ amendedSections: string[], tasksAdded: number[], tasksAnchorChanged: number[], tasksRemoved: number[], passed: boolean, failLines?: string[] }`; use pre-apply task numbers, never tracker positions. Require the child to capture the original task numbers, titles, spec anchors, and wave positions in the existing saved apply report before the first apply and retain that original inventory on every return, including failures; add no sidecar. On `passed: false`, repeat once with `failLines` as the rule and `previousApplyReport` set to the first apply report path. Require the fresh retry worker to read that original inventory, apply granted edits idempotently without duplicating already-applied edits, and compute deltas against that original inventory rather than the already-mutated plan. Missing original inventory fails the retry; send a second failure to the human with verbatim findings, including substantive baseline failures. Keep the attribution, protected-path, and dirty-baseline exits below; no delegated restoration permission is granted. On pass, print `amended the spec: <title> - <location>` for each applied item, run the parent's `plan_check` for stamping, and use its fresh manifest with the returned task numbers without reading the plan or spec. Add `tasksAdded`, reopen completed `tasksAnchorChanged` as `in_progress`, and handle `tasksRemoved` through the removal rules below. Require parent PASS before tracker changes. Skip the ordinary parent edit, `Amend:` dispatch, and amended-plan read; preserve companion aftermath, audit, and the single batch commit below. Planning callers keep the ordinary apply and aftermath below; ledger and no-plan routes stay unchanged.
149
157
 
150
158
  Apply accepted items only: reviewer clears, `accept`, `alt-n`, and state-changing `custom`. Print one line per applied item: `amended the spec: <title> - <what>`.
151
159
 
@@ -120,7 +120,7 @@ succeeds (it creates both). Every destructive command below is gated on its flag
120
120
  file or ticket is not-a-gap. Real gaps -> one implementer round -> re-run step
121
121
  5 -> no second audit; unfixable -> abort. Dispatch failure or malformed output
122
122
  -> note it, continue.
123
- 7. **Review - last mutation gate.** One fresh `code-reviewer` over
123
+ 7. **Review - last mutation gate.** One fresh `wave-reviewer` over
124
124
  `<base-sha>..HEAD` in the worktree. Task text: the record path,
125
125
  `SCOPED_TEST_COMMANDS`, invariants 1-3 and predicates 4-6 as named review items
126
126
  (a predicate miss is Moderate unless it trips an invariant). `SHIP` -> step 8.
@@ -128,7 +128,7 @@ Grammar (identical across producers, modulo id prefix — `F` for code/spec revi
128
128
  | <id> " conflicts " <id> " (" <reason> ")"
129
129
  ```
130
130
 
131
- **Structural probe:** when the report carries >= 2 actionable finding IDs (Critical/Moderate findings for code-reviewer; PARTIAL/MISSING/scope-creep findings for spec-reviewer; non-DELIVERED gaps for conformance-reviewer), validate the certificate before the fan-out decision: the review must contain **exactly one** line matching `^Parallel-safe: ` whose remainder parses as `<group>[; <group>]*` (grammar above). Zero matching lines, an unparseable remainder, or **any second** `Parallel-safe:` line (identical or not) = malformed -> re-ask the reviewer **once**, quoting the expected grammar, before any disposition or fan-out decision. Still malformed after the re-ask -> fully sequential fixes with an explicit one-line degradation notice in the orchestrator's visible output — never silent. A probe-passing line with no >= 2-ID `disjoint` group is **valid**: sequential fixes, no re-ask, no notice (the certificate says "serial", not a malformation). Fewer than 2 actionable IDs -> skip the probe (nothing to fan out). Reviewer errors (no report at all) are out of scope here — report-shape validation, not report-existence; existing dispatch-failure handling applies.
131
+ **Structural probe:** when the report carries >= 2 actionable finding IDs (Critical/Moderate findings for wave-reviewer; non-DELIVERED gaps for conformance-reviewer), validate the certificate before the fan-out decision: the review must contain **exactly one** line matching `^Parallel-safe: ` whose remainder parses as `<group>[; <group>]*` (grammar above). Zero matching lines, an unparseable remainder, or **any second** `Parallel-safe:` line (identical or not) = malformed -> re-ask the reviewer **once**, quoting the expected grammar, before any disposition or fan-out decision. Still malformed after the re-ask -> fully sequential fixes with an explicit one-line degradation notice in the orchestrator's visible output — never silent. A probe-passing line with no >= 2-ID `disjoint` group is **valid**: sequential fixes, no re-ask, no notice (the certificate says "serial", not a malformation). Fewer than 2 actionable IDs -> skip the probe (nothing to fan out). Reviewer errors (no report at all) are out of scope here — report-shape validation, not report-existence; existing dispatch-failure handling applies.
132
132
 
133
133
  **After the fix wave:** integrate patches serially per "Review and Integrate" above (mis-partition is self-healing: integrate the successes, re-run the conflicting finding sequentially on integrated HEAD); run the consuming loop's scoped test gate on the integrated tree; then one re-review of the integrated fix delta, per the consuming loop's own rules. The fan-out counts as one fix round against the consuming loop's budget — it grants no extra rounds.
134
134
 
@@ -191,7 +191,7 @@ Parallel dispatch rides on the `subagent` tool (the pi-cohort package). Mechanic
191
191
  - **Context** — `context: "fresh"` at the top level applies to every task. Mandatory here (see above); without it `worker` forks parent history and you lose isolation.
192
192
  - **Filesystem isolation** — `worktree: true` runs each task in its own git worktree so concurrent edits can't collide. Requires clean git state; each task's diff returns separately for you to integrate. Omit it for read-only investigations.
193
193
  - **Worktree base / `cwd`** — under `worktree: true` the base commit is `HEAD` resolved from the **top-level `cwd`**, which defaults to the orchestrator's process cwd. When you orchestrate from inside a git worktree, pass that worktree's absolute path as the top-level `cwd`, or children branch from the wrong checkout. Don't set per-task `cwd` with `worktree: true` — it must equal the shared cwd or the run errors.
194
- - **Agent choice** — `worker` is the pi-cohort builtin generalist. Use a persona (`implementer`, `code-reviewer`) when you want its system prompt and tool profile. Persona frontmatter (tools, thinking, context) is fixed; only `model`, `task`, `output`, `reads`, `progress`, `skill` are callable per task.
194
+ - **Agent choice** — `worker` is the pi-cohort builtin generalist. Use a persona (`implementer`, `wave-reviewer`) when you want its system prompt and tool profile. Persona frontmatter (tools, thinking, context) is fixed; only `model`, `task`, `output`, `reads`, `progress`, `skill` are callable per task.
195
195
  - **Output capture** - `output: "<file>"` writes a task's summary to a file instead of inline; add `outputMode: "file-only"` for large results. When a batch uses `worktree: true`, each `output:` path must be absolute and outside every isolated checkout; a relative report is captured as helper work and then deleted with the checkout. In a non-isolated batch, a relative report lands in the shared working tree and risks being committed or overwritten by a later task.
196
196
 
197
197
  ```bash
@@ -43,7 +43,7 @@ Take **no** disposition action before the reply. Then, once, in order:
43
43
  1. **Normalize** every `custom(...)` into explicit operations; classify state-changing (edits code or spec) vs not. Clarify only an ambiguous or unexecutable effect.
44
44
  2. **Commit spec edits** (`accept-into-spec`, `rescope-into-spec`, state-changing spec `custom`) - the main session edits the spec directly, before any fix dispatch (a dirty tree rejects `worktree: true`, and the re-audit must read the amended spec). A disposition chosen here is already approved: it bypasses the amendment surface's reviewer and batch. Items the pre-menu funnel auto-applied are recorded `Gn - <title>: accept-into-spec (auto-applied)`.
45
45
  3. **Re-audit if step 2 changed the spec**; regenerate the inventory and re-render if it changed. Project `fix-now` only from the refreshed inventory.
46
- 4. **fix-now + code-changing custom:** project the selected concerns per gap into the reference's concern-scoped fix contract (excluding accepted/rescoped/followed-up siblings); run the reference "Fix loop" (unchanged - do not re-describe it). A code-changing `custom` runs the project's tests + `code-reviewer` on its delta before proceeding. Re-run Step 1's canonical tests.
46
+ 4. **fix-now + code-changing custom:** project the selected concerns per gap into the reference's concern-scoped fix contract (excluding accepted/rescoped/followed-up siblings); run the reference "Fix loop" (unchanged - do not re-describe it). A code-changing `custom` runs the project's tests + `wave-reviewer` on its delta before proceeding. Re-run Step 1's canonical tests.
47
47
  5. **Re-audit after all state-changing work;** obtain fresh decisions **only if** the refreshed inventory differs from the approved one, else proceed.
48
48
  6. **follow-up** from the current inventory: create the item via the project's issue-tracker convention (the gauntlet overrides file, see Project overrides in `finishing-a-development-branch/SKILL.md`), record the ticket ID/URL; on failure keep the concern open.
49
49
  7. **Non-state-changing custom:** execute and record the result.