pi-gauntlet 7.43.1 → 8.0.1
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 +9 -0
- package/README.md +2 -2
- package/agents/plan-writer.md +3 -3
- package/agents/wave-reviewer.md +111 -0
- package/extensions/lib/phase-tracker-helpers.test.ts +7 -8
- package/extensions/lib/phase-tracker-helpers.ts +2 -5
- package/extensions/lib/plan-check.test.ts +45 -0
- package/extensions/lib/plan-check.ts +19 -7
- package/extensions/lib/telemetry-collect.test.ts +21 -1
- package/extensions/lib/telemetry-collect.ts +23 -1
- package/extensions/phase-tracker.test.ts +80 -73
- package/extensions/phase-tracker.ts +10 -13
- package/extensions/telemetry.test.ts +11 -5
- package/extensions/telemetry.ts +2 -2
- package/package.json +1 -1
- package/skills/brainstorming/reference/amendment-surface.md +9 -1
- package/skills/chase-bug/hotfix.md +1 -1
- package/skills/dispatching-parallel-agents/SKILL.md +2 -2
- package/skills/finishing-a-development-branch/reference/disposition-protocol.md +1 -1
- package/skills/gatekeep-pr/SKILL.md +1 -1
- package/skills/gatekeep-pr/reference/assessment.md +1 -1
- package/skills/gatekeep-pr/reference/sync.md +1 -1
- package/skills/loop-implementation/SKILL.md +8 -87
- package/skills/loop-implementation/reference/coordinated-legs.md +104 -0
- package/skills/requesting-code-review/SKILL.md +9 -9
- package/skills/requesting-code-review/{code-reviewer.md → wave-reviewer.md} +2 -2
- package/skills/subagent-driven-development/SKILL.md +124 -121
- package/skills/subagent-driven-development/reference/dispatch-contract.md +10 -9
- package/skills/subagent-driven-development/reference/implement-amendments.md +3 -1
- package/skills/subagent-driven-development/stop-note.md +4 -4
- package/skills/subagent-driven-development/wave-reviewer-prompt.md +55 -0
- package/skills/verification-before-completion/reference/conformance-check.md +2 -2
- package/skills/writing-plans/SKILL.md +7 -1
- package/skills/writing-plans/reference/plan-contract.md +6 -0
- package/agents/code-reviewer.md +0 -115
- package/agents/spec-reviewer.md +0 -121
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +0 -67
- package/skills/subagent-driven-development/reference/fix-loop.md +0 -45
- 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
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|
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(["
|
|
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("
|
|
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(["
|
|
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", "
|
|
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: "
|
|
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(["
|
|
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
|
|
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
|
|
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
|
|
1603
|
-
{ name: "failed reviewer", results: [{ agent: "
|
|
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: "
|
|
1607
|
-
{ name: "earlier review in errored chain", results: [{ agent: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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", "
|
|
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: "
|
|
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: "
|
|
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\(
|
|
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: "
|
|
2234
|
-
assert.ok(h.widgets.at(-1)!.includes(`verify(
|
|
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: "
|
|
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\(
|
|
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: "
|
|
2269
|
-
assert.ok(h.widgets.at(-1)!.includes(`verify(
|
|
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: "
|
|
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: "
|
|
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
|
-
["
|
|
2321
|
-
["conformance-reviewer", "implementer", "conformance-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 = ({ "
|
|
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: "
|
|
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: "
|
|
2358
|
-
assert.ok(h.widgets.at(-1)!.includes(`verify(
|
|
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: "
|
|
2370
|
-
assert.match(h.widgets.at(-1)!, /verify\(
|
|
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: "
|
|
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: "
|
|
2382
|
-
assert.match(h.widgets.at(-1)!, /verify\(
|
|
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: "
|
|
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\(
|
|
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: "
|
|
2403
|
-
assert.match(h.widgets.at(-1)!, /verify\(
|
|
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: "
|
|
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\(
|
|
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: "
|
|
2422
|
-
assert.match(h.widgets.at(-1)!, /verify\(
|
|
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: "
|
|
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: "
|
|
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: "
|
|
2527
|
-
assert.match(h.widgets.at(-1)!, /verify\(
|
|
2528
|
-
const branch = kind === "empty" ? [assistant()] : kind === "explicit" ? explicitReplay("verify", "custom") : [phaseResult("start", replaySnapshot("verify")), phaseResult("status", replaySnapshot("verify", "
|
|
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: "
|
|
2544
|
-
assert.match(h.widgets.at(-1)!, /verify\(
|
|
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: "
|
|
2547
|
-
assert.match(h.widgets.at(-1)!, /verify\(
|
|
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 /
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
const CADENCE_AGENTS = ["implementer", "
|
|
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
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"Dispatch the missing review
|
|
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
|
-
"
|
|
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["
|
|
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["
|
|
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: "
|
|
759
|
-
],
|
|
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?.["
|
|
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", "
|
|
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?.["
|
|
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"));
|
package/extensions/telemetry.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
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 `
|
|
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
|
|
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`, `
|
|
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 + `
|
|
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.
|