pi-squad 0.15.1 → 0.16.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.
- package/README.md +15 -2
- package/package.json +1 -1
- package/src/index.ts +128 -28
- package/src/panel/squad-widget.ts +4 -1
- package/src/review.ts +107 -0
- package/src/scheduler.ts +7 -3
- package/src/skills/squad-supervisor/SKILL.md +24 -17
- package/src/store.ts +3 -3
- package/src/types.ts +17 -2
package/README.md
CHANGED
|
@@ -41,8 +41,21 @@ The planner agent reads your codebase and creates a task breakdown automatically
|
|
|
41
41
|
2. A **live widget** appears above the editor showing task progress
|
|
42
42
|
3. **Specialist agents** spawn as separate pi processes, working in parallel where dependencies allow
|
|
43
43
|
4. QA agents can trigger **automatic rework loops** when they find bugs
|
|
44
|
-
5.
|
|
45
|
-
6.
|
|
44
|
+
5. When agents finish, the squad enters **`review`**, not `done`; main Pi receives every complete task output as untrusted review input
|
|
45
|
+
6. Main Pi independently checks the original user contract, actual diff/source, verification commands, and integration/E2E, then records acceptance with `squad_review`
|
|
46
|
+
7. Multiple squads can run concurrently across different projects
|
|
47
|
+
|
|
48
|
+
### Mandatory Orchestrator Review Gate
|
|
49
|
+
|
|
50
|
+
Squad agents—including QA/reviewer agents—produce candidate work and evidence claims. They cannot mark a squad accepted. After all tasks finish:
|
|
51
|
+
|
|
52
|
+
- Persisted status becomes `review`, never directly `done`.
|
|
53
|
+
- A persistent `<squad_review_required>` system reminder tells main Pi to re-read the original conversation contract, inspect the actual diff/source, rerun verification independently, and run integration/E2E where applicable.
|
|
54
|
+
- Main Pi must call `squad_review` with requirement-by-requirement contract checks, diff review, actual command/result evidence, integration/E2E evidence, and issues.
|
|
55
|
+
- Only `pass` or `pass_with_issues` changes the squad to `done`; `fail` leaves it review-blocked for fixes and re-review.
|
|
56
|
+
- Pending review survives Pi restarts and is restored on the next session.
|
|
57
|
+
|
|
58
|
+
The completion report is explicitly labeled **untrusted and not yet accepted**. Main Pi must never merely relay it or ask whether verification should be run.
|
|
46
59
|
|
|
47
60
|
### No-Truncation Contract
|
|
48
61
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { setupSquadWidget, type SquadWidgetState } from "./panel/squad-widget.js
|
|
|
26
26
|
import * as store from "./store.js";
|
|
27
27
|
import { debug, logError } from "./logger.js";
|
|
28
28
|
import { buildCompletionSummary, buildFailureSummary } from "./report.js";
|
|
29
|
+
import { buildOrchestratorReviewGate, recordOrchestratorReview } from "./review.js";
|
|
29
30
|
|
|
30
31
|
// ============================================================================
|
|
31
32
|
// State
|
|
@@ -45,14 +46,6 @@ let uiCtx: import("@earendil-works/pi-coding-agent").ExtensionContext | null = n
|
|
|
45
46
|
const widgetState: SquadWidgetState = { squadId: null, enabled: true };
|
|
46
47
|
let widgetControls: { requestUpdate: () => void; dispose: () => void } | null = null;
|
|
47
48
|
|
|
48
|
-
/** Reviewer instructions appended to squad-completed notifications —
|
|
49
|
-
* makes the main session behave like the QA/verification agents do. */
|
|
50
|
-
const REVIEW_INSTRUCTIONS =
|
|
51
|
-
"Before reporting success to the user, REVIEW the work like a QA agent would: " +
|
|
52
|
-
"(1) check task outputs for QA verdicts (## Verdict: PASS/FAIL) and verification evidence (commands + results); " +
|
|
53
|
-
"(2) if a task claimed done without evidence, run its Verify command yourself; " +
|
|
54
|
-
"(3) report to the user what was verified (with evidence) and flag anything unverified — don't just relay the summary.";
|
|
55
|
-
|
|
56
49
|
/**
|
|
57
50
|
* Resolve a model string (or null = session default) to its context window.
|
|
58
51
|
* Reads uiCtx lazily so it always uses the live session's registry.
|
|
@@ -215,14 +208,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
215
208
|
|
|
216
209
|
// Inject squad awareness before each LLM call
|
|
217
210
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
218
|
-
|
|
211
|
+
// Review gates are project-wide and survive focus changes, new squads, and
|
|
212
|
+
// disabling normal squad operations. Unaccepted work must stay visible.
|
|
213
|
+
const pendingReviewGates = store.findActiveSquads()
|
|
214
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "review")
|
|
215
|
+
.map((s) => ({ squad: s, gate: buildOrchestratorReviewGate(s, store.loadAllTasks(s.id)) }));
|
|
216
|
+
if (!squadEnabled) {
|
|
217
|
+
if (pendingReviewGates.length === 0) return;
|
|
218
|
+
return { systemPrompt: event.systemPrompt + "\n\n" + pendingReviewGates.map(({ gate }) => gate).join("\n\n") };
|
|
219
|
+
}
|
|
219
220
|
|
|
220
221
|
// When a squad is active, inject its status
|
|
221
222
|
if (activeSquadId) {
|
|
222
223
|
const squad = store.loadSquad(activeSquadId);
|
|
223
|
-
if (!squad)
|
|
224
|
+
if (!squad) {
|
|
225
|
+
activeSquadId = null;
|
|
226
|
+
if (pendingReviewGates.length > 0) {
|
|
227
|
+
return { systemPrompt: event.systemPrompt + "\n\n" + pendingReviewGates.map(({ gate }) => gate).join("\n\n") };
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
224
231
|
const tasks = store.loadAllTasks(activeSquadId);
|
|
225
|
-
if (tasks.length === 0)
|
|
232
|
+
if (tasks.length === 0) {
|
|
233
|
+
if (pendingReviewGates.length > 0) {
|
|
234
|
+
return { systemPrompt: event.systemPrompt + "\n\n" + pendingReviewGates.map(({ gate }) => gate).join("\n\n") };
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
226
238
|
|
|
227
239
|
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
228
240
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
@@ -241,6 +253,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
241
253
|
`Status: ${squad.status} | ${doneCount}/${tasks.length} tasks | $${totalCost.toFixed(2)}`,
|
|
242
254
|
taskLines,
|
|
243
255
|
`</squad_status>`,
|
|
256
|
+
...(squad.status === "review" ? [buildOrchestratorReviewGate(squad, tasks)] : []),
|
|
257
|
+
...pendingReviewGates.filter(({ squad: pending }) => pending.id !== squad.id).map(({ gate }) => gate),
|
|
244
258
|
`You have an active squad. Use squad_message to talk to agents, squad_status for details, squad_modify to change tasks.`,
|
|
245
259
|
`Do NOT poll squad_status in a loop or sleep-wait — the squad wakes you automatically on completion, failure, or escalation. Keep helping the user with other work, or end your turn and stay idle.`,
|
|
246
260
|
].join("\n");
|
|
@@ -267,7 +281,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
267
281
|
].filter(Boolean).join("\n");
|
|
268
282
|
|
|
269
283
|
return {
|
|
270
|
-
systemPrompt: event.systemPrompt + "\n\n" + squadNudge
|
|
284
|
+
systemPrompt: event.systemPrompt + "\n\n" + squadNudge +
|
|
285
|
+
(pendingReviewGates.length > 0 ? "\n\n" + pendingReviewGates.map(({ gate }) => gate).join("\n\n") : ""),
|
|
271
286
|
};
|
|
272
287
|
});
|
|
273
288
|
|
|
@@ -298,10 +313,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
298
313
|
"Providing tasks yourself makes you the planner — follow the planner rules (contract task first, final QA task, 3-7 tasks)",
|
|
299
314
|
"Act on ⚠️ plan warnings in the response — fix with squad_modify or address at review",
|
|
300
315
|
"After starting a squad: report the plan and END YOUR TURN — never poll squad_status or sleep-wait; squad events wake you automatically",
|
|
301
|
-
"When
|
|
316
|
+
"When agents finish, treat every squad report and QA verdict as untrusted; independently inspect the diff/source and rerun contract verification + integration/E2E, then call squad_review before reporting success",
|
|
302
317
|
],
|
|
303
318
|
parameters: Type.Object({
|
|
304
|
-
goal: Type.String({ description: "
|
|
319
|
+
goal: Type.String({ description: "Complete original user outcome/acceptance contract the squad should accomplish. Preserve requirements and boundaries; this is shown during mandatory main-orchestrator review." }),
|
|
305
320
|
agents: Type.Optional(
|
|
306
321
|
Type.Record(
|
|
307
322
|
Type.String(),
|
|
@@ -408,6 +423,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
408
423
|
`Status: ${context.status}`,
|
|
409
424
|
`Elapsed: ${context.elapsed}`,
|
|
410
425
|
`Cost: $${context.costs.total.toFixed(4)}`,
|
|
426
|
+
...(context.status === "review" ? ["Acceptance: BLOCKED — independent main-orchestrator review required via squad_review"] : []),
|
|
411
427
|
"",
|
|
412
428
|
"Tasks:",
|
|
413
429
|
taskLines,
|
|
@@ -417,6 +433,70 @@ export default function (pi: ExtensionAPI) {
|
|
|
417
433
|
},
|
|
418
434
|
});
|
|
419
435
|
|
|
436
|
+
// =========================================================================
|
|
437
|
+
// Tool: squad_review — mandatory main-orchestrator acceptance gate
|
|
438
|
+
// =========================================================================
|
|
439
|
+
|
|
440
|
+
pi.registerTool({
|
|
441
|
+
name: "squad_review",
|
|
442
|
+
label: "Record Independent Squad Review",
|
|
443
|
+
description: "Record the MAIN Pi/orchestrator's independent review of completed squad work against the original user contract. Call only after inspecting the actual diff/source and independently running verification plus integration/E2E where applicable. Squad reports and squad QA evidence are not sufficient.",
|
|
444
|
+
parameters: Type.Object({
|
|
445
|
+
squadId: Type.Optional(Type.String({ description: "Squad awaiting review (default: active/latest)" })),
|
|
446
|
+
verdict: Type.Union([
|
|
447
|
+
Type.Literal("pass"),
|
|
448
|
+
Type.Literal("pass_with_issues"),
|
|
449
|
+
Type.Literal("fail"),
|
|
450
|
+
]),
|
|
451
|
+
contractChecks: Type.Array(Type.String(), { minItems: 1, description: "Requirement-by-requirement checks against the ORIGINAL user request and later clarifications; include observed result for each" }),
|
|
452
|
+
diffReview: Type.String({ description: "What you independently inspected in the actual diff/source, including scope and integration concerns" }),
|
|
453
|
+
verificationEvidence: Type.Array(Type.String(), { minItems: 1, description: "Commands/checks YOU ran and their actual results; do not copy squad claims" }),
|
|
454
|
+
integrationEvidence: Type.String({ description: "Integration/E2E result from the real target or production-like environment, or a precise reason it is not applicable/impossible and therefore unverified" }),
|
|
455
|
+
issues: Type.Array(Type.String(), { description: "Every discovered or remaining issue; required for fail/pass_with_issues" }),
|
|
456
|
+
}),
|
|
457
|
+
|
|
458
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
459
|
+
let id = params.squadId;
|
|
460
|
+
if (!id && activeSquadId && store.loadSquad(activeSquadId)?.status === "review") {
|
|
461
|
+
id = activeSquadId;
|
|
462
|
+
}
|
|
463
|
+
if (!id) {
|
|
464
|
+
id = store.listSquadsForProject(ctx.cwd)
|
|
465
|
+
.filter((s) => s.status === "review")
|
|
466
|
+
.sort((a, b) => b.created.localeCompare(a.created))[0]?.id;
|
|
467
|
+
}
|
|
468
|
+
if (!id) {
|
|
469
|
+
return { content: [{ type: "text" as const, text: "No squad is awaiting orchestrator review." }], details: undefined };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const squad = store.loadSquad(id);
|
|
473
|
+
if (!squad) {
|
|
474
|
+
return { content: [{ type: "text" as const, text: `Squad '${id}' not found.` }], details: undefined };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
recordOrchestratorReview(squad, {
|
|
479
|
+
verdict: params.verdict,
|
|
480
|
+
contractChecks: params.contractChecks,
|
|
481
|
+
diffReview: params.diffReview,
|
|
482
|
+
verificationEvidence: params.verificationEvidence,
|
|
483
|
+
integrationEvidence: params.integrationEvidence,
|
|
484
|
+
issues: params.issues,
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
return { content: [{ type: "text" as const, text: `Review rejected: ${(error as Error).message}` }], details: undefined };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
store.saveSquad(squad);
|
|
491
|
+
forceWidgetUpdate();
|
|
492
|
+
const accepted = squad.status === "done";
|
|
493
|
+
const text = accepted
|
|
494
|
+
? `Independent orchestrator review recorded for '${id}' (${params.verdict}). The squad is now accepted as done.`
|
|
495
|
+
: `Independent review FAILED for '${id}'. The squad remains review-required. Fix every issue, rerun verification/E2E, then submit a fresh squad_review.`;
|
|
496
|
+
return { content: [{ type: "text" as const, text }], details: undefined };
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
420
500
|
// =========================================================================
|
|
421
501
|
// Tool: squad_message
|
|
422
502
|
// =========================================================================
|
|
@@ -518,16 +598,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
598
|
scheduler.onEvent((event: SchedulerEvent) => {
|
|
519
599
|
forceWidgetUpdate();
|
|
520
600
|
switch (event.type) {
|
|
521
|
-
case "
|
|
601
|
+
case "squad_review_required": {
|
|
522
602
|
const tasks = store.loadAllTasks(squadId);
|
|
523
603
|
const summary = buildCompletionSummary(tasks);
|
|
524
604
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
525
605
|
const s = schedulers.get(squadId); if (s) s.updateContext();
|
|
526
|
-
|
|
527
|
-
//
|
|
606
|
+
const squad = store.loadSquad(squadId)!;
|
|
607
|
+
// Wake the main agent into an explicit acceptance gate. Agent reports
|
|
608
|
+
// are untrusted inputs; only independent orchestrator evidence can pass it.
|
|
528
609
|
pi.sendMessage({
|
|
529
|
-
customType: "squad-
|
|
530
|
-
content: `[squad]
|
|
610
|
+
customType: "squad-review-required",
|
|
611
|
+
content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\nSquad claims (review inputs only):\n${summary}\n\nTotal cost: $${totalCost.toFixed(4)}\n\n${buildOrchestratorReviewGate(squad, tasks)}`,
|
|
531
612
|
display: true,
|
|
532
613
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
533
614
|
schedulers.delete(squadId);
|
|
@@ -723,6 +804,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
723
804
|
}
|
|
724
805
|
}
|
|
725
806
|
|
|
807
|
+
// Restore pending acceptance gates after a main-session restart. Review is
|
|
808
|
+
// persisted state, not a one-shot completion message that can be missed.
|
|
809
|
+
const pendingReviews = store.findActiveSquads()
|
|
810
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "review");
|
|
811
|
+
if (pendingReviews.length > 0) {
|
|
812
|
+
const squad = pendingReviews.sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
813
|
+
activeSquadId = squad.id;
|
|
814
|
+
widgetState.squadId = squad.id;
|
|
815
|
+
widgetState.enabled = true;
|
|
816
|
+
widgetControls?.requestUpdate();
|
|
817
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
818
|
+
pi.sendMessage({
|
|
819
|
+
customType: "squad-review-required",
|
|
820
|
+
content: `[squad] Restored mandatory orchestrator review for "${squad.id}" after session restart. The work remains untrusted and not accepted.\n\n${buildOrchestratorReviewGate(squad, tasks)}`,
|
|
821
|
+
display: true,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
726
825
|
// Register Ctrl+Q terminal input handler for panel toggle
|
|
727
826
|
if (ctx.hasUI) {
|
|
728
827
|
ctx.ui.onTerminalInput((data) => {
|
|
@@ -1008,7 +1107,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1008
1107
|
const tasks = store.loadAllTasks(s.id);
|
|
1009
1108
|
const done = tasks.filter((t) => t.status === "done").length;
|
|
1010
1109
|
const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
1011
|
-
const icon = s.status === "done" ? "✓" : s.status === "running" ? "⏳" : s.status === "failed" ? "✗" : "·";
|
|
1110
|
+
const icon = s.status === "done" ? "✓" : s.status === "running" ? "⏳" : s.status === "review" ? "◆" : s.status === "failed" ? "✗" : "·";
|
|
1012
1111
|
return `${icon} ${s.id} [${s.status}] ${done}/${tasks.length} $${cost.toFixed(2)}`;
|
|
1013
1112
|
}),
|
|
1014
1113
|
];
|
|
@@ -1318,7 +1417,7 @@ async function pickSquad(
|
|
|
1318
1417
|
const tasks = store.loadAllTasks(s.id);
|
|
1319
1418
|
const done = tasks.filter((t) => t.status === "done").length;
|
|
1320
1419
|
const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
1321
|
-
const icon = s.status === "done" ? "✓" : s.status === "running" ? "⏳" : s.status === "failed" ? "✗" : "·";
|
|
1420
|
+
const icon = s.status === "done" ? "✓" : s.status === "running" ? "⏳" : s.status === "review" ? "◆" : s.status === "failed" ? "✗" : "·";
|
|
1322
1421
|
const project = showProject ? ` — ${s.cwd.split("/").pop()}` : "";
|
|
1323
1422
|
return `${icon} ${s.id} [${s.status}] ${done}/${tasks.length} $${cost.toFixed(2)}${project}`;
|
|
1324
1423
|
});
|
|
@@ -1563,7 +1662,7 @@ async function startSquad(
|
|
|
1563
1662
|
// Update widget on every scheduler event
|
|
1564
1663
|
forceWidgetUpdate();
|
|
1565
1664
|
switch (event.type) {
|
|
1566
|
-
case "
|
|
1665
|
+
case "squad_review_required": {
|
|
1567
1666
|
const tasks = store.loadAllTasks(squadId);
|
|
1568
1667
|
const summary = buildCompletionSummary(tasks);
|
|
1569
1668
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
@@ -1573,15 +1672,16 @@ async function startSquad(
|
|
|
1573
1672
|
if (completedSched) {
|
|
1574
1673
|
completedSched.updateContext();
|
|
1575
1674
|
}
|
|
1675
|
+
const squad = store.loadSquad(squadId)!;
|
|
1576
1676
|
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1677
|
+
// Wake the main agent into an explicit acceptance gate. Never frame
|
|
1678
|
+
// agent execution or squad QA as trusted completion.
|
|
1579
1679
|
pi.sendMessage({
|
|
1580
|
-
customType: "squad-
|
|
1581
|
-
content: `[squad]
|
|
1582
|
-
`
|
|
1680
|
+
customType: "squad-review-required",
|
|
1681
|
+
content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\n` +
|
|
1682
|
+
`Squad claims (review inputs only):\n${summary}\n\n` +
|
|
1583
1683
|
`Total cost: $${totalCost.toFixed(4)}\n\n` +
|
|
1584
|
-
|
|
1684
|
+
buildOrchestratorReviewGate(squad, tasks),
|
|
1585
1685
|
display: true,
|
|
1586
1686
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1587
1687
|
|
|
@@ -79,6 +79,7 @@ export function setupSquadWidget(
|
|
|
79
79
|
|
|
80
80
|
const sIcon = squad.status === "done" ? th.fg("success", "✓")
|
|
81
81
|
: squad.status === "failed" ? th.fg("error", "✗")
|
|
82
|
+
: squad.status === "review" ? th.fg("warning", "◆")
|
|
82
83
|
: th.fg("warning", "⏳");
|
|
83
84
|
|
|
84
85
|
lines.push(
|
|
@@ -141,6 +142,8 @@ export function setupSquadWidget(
|
|
|
141
142
|
? th.fg("success", `✓ squad ${doneCount}/${tasks.length}`)
|
|
142
143
|
: squad.status === "failed"
|
|
143
144
|
? th.fg("error", `✗ squad ${doneCount}/${tasks.length}`)
|
|
145
|
+
: squad.status === "review"
|
|
146
|
+
? th.fg("warning", `◆ squad review required`)
|
|
144
147
|
: th.fg("accent", `⏳ squad ${doneCount}/${tasks.length} $${totalCost.toFixed(2)}`);
|
|
145
148
|
|
|
146
149
|
return { lines, cacheKey, statusText };
|
|
@@ -215,7 +218,7 @@ export function setupSquadWidget(
|
|
|
215
218
|
return;
|
|
216
219
|
}
|
|
217
220
|
const squad = store.loadSquad(state.squadId);
|
|
218
|
-
const isActive = squad && (squad.status === "running" || squad.status === "paused");
|
|
221
|
+
const isActive = squad && (squad.status === "running" || squad.status === "paused" || squad.status === "review");
|
|
219
222
|
if (isActive && !durationTimer) {
|
|
220
223
|
durationTimer = setInterval(() => render(), 5000);
|
|
221
224
|
} else if (!isActive && durationTimer) {
|
package/src/review.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Squad, Task } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type OrchestratorReviewVerdict = "pass" | "pass_with_issues" | "fail";
|
|
4
|
+
|
|
5
|
+
export interface OrchestratorReviewInput {
|
|
6
|
+
verdict: OrchestratorReviewVerdict;
|
|
7
|
+
contractChecks: string[];
|
|
8
|
+
diffReview: string;
|
|
9
|
+
verificationEvidence: string[];
|
|
10
|
+
integrationEvidence: string;
|
|
11
|
+
issues: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Move a squad from agent execution into mandatory independent main-session review. */
|
|
15
|
+
export function beginOrchestratorReview(squad: Squad): void {
|
|
16
|
+
squad.status = "review";
|
|
17
|
+
squad.review = {
|
|
18
|
+
status: "pending",
|
|
19
|
+
requestedAt: new Date().toISOString(),
|
|
20
|
+
completedAt: null,
|
|
21
|
+
verdict: null,
|
|
22
|
+
contractChecks: [],
|
|
23
|
+
diffReview: "",
|
|
24
|
+
verificationEvidence: [],
|
|
25
|
+
integrationEvidence: "",
|
|
26
|
+
issues: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate and record the orchestrator's independent review evidence.
|
|
32
|
+
* A failing review deliberately leaves the squad behind the review gate.
|
|
33
|
+
*/
|
|
34
|
+
export function recordOrchestratorReview(squad: Squad, input: OrchestratorReviewInput): void {
|
|
35
|
+
if (squad.status !== "review" || !squad.review) {
|
|
36
|
+
throw new Error(`Squad '${squad.id}' is not awaiting orchestrator review`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const contractChecks = cleanList(input.contractChecks);
|
|
40
|
+
const verificationEvidence = cleanList(input.verificationEvidence);
|
|
41
|
+
const issues = cleanList(input.issues);
|
|
42
|
+
const diffReview = input.diffReview.trim();
|
|
43
|
+
const integrationEvidence = input.integrationEvidence.trim();
|
|
44
|
+
|
|
45
|
+
if (contractChecks.length === 0) {
|
|
46
|
+
throw new Error("contractChecks must map the original user requirements to observed results");
|
|
47
|
+
}
|
|
48
|
+
if (!diffReview) {
|
|
49
|
+
throw new Error("diffReview must describe the independently inspected changes and scope");
|
|
50
|
+
}
|
|
51
|
+
if (verificationEvidence.length === 0) {
|
|
52
|
+
throw new Error("verificationEvidence must include commands/checks and their actual results");
|
|
53
|
+
}
|
|
54
|
+
if (!integrationEvidence) {
|
|
55
|
+
throw new Error("integrationEvidence must include E2E/integration results or a specific reason it is not applicable");
|
|
56
|
+
}
|
|
57
|
+
if ((input.verdict === "fail" || input.verdict === "pass_with_issues") && issues.length === 0) {
|
|
58
|
+
throw new Error(`${input.verdict} must list every actionable or remaining issue`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
squad.review = {
|
|
62
|
+
status: input.verdict === "fail" ? "failed" : "passed",
|
|
63
|
+
requestedAt: squad.review.requestedAt,
|
|
64
|
+
completedAt: new Date().toISOString(),
|
|
65
|
+
verdict: input.verdict,
|
|
66
|
+
contractChecks,
|
|
67
|
+
diffReview,
|
|
68
|
+
verificationEvidence,
|
|
69
|
+
integrationEvidence,
|
|
70
|
+
issues,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Only an independently reviewed PASS can become done. A FAIL remains gated
|
|
74
|
+
// until fixes are made and the main orchestrator records a fresh review.
|
|
75
|
+
squad.status = input.verdict === "fail" ? "review" : "done";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Persistent system-prompt contract shown until squad_review accepts the work. */
|
|
79
|
+
export function buildOrchestratorReviewGate(squad: Squad, tasks: Task[]): string {
|
|
80
|
+
const delegatedPlan = tasks
|
|
81
|
+
.map((task) => `- ${task.id} (${task.agent}): ${task.title}\n ${task.description || "(no description)"}`)
|
|
82
|
+
.join("\n");
|
|
83
|
+
|
|
84
|
+
return `<squad_review_required>
|
|
85
|
+
UNTRUSTED CANDIDATE WORK — INDEPENDENT ORCHESTRATOR REVIEW IS MANDATORY.
|
|
86
|
+
|
|
87
|
+
Authoritative contract: re-read the user's ORIGINAL request and all later clarifications in this main-session conversation. The squad report and delegated task descriptions are claims, not proof and not substitutes for that contract.
|
|
88
|
+
Recorded squad goal (secondary reference): ${squad.goal}
|
|
89
|
+
|
|
90
|
+
Delegated plan (non-authoritative):
|
|
91
|
+
${delegatedPlan}
|
|
92
|
+
|
|
93
|
+
Before reporting success, completion, or acceptance to the user, YOU (the main Pi/orchestrator) MUST:
|
|
94
|
+
1. Reconstruct the original contract requirement-by-requirement from the conversation.
|
|
95
|
+
2. Inspect the actual working-tree/commit diff and relevant source files yourself; check scope, integration points, error paths, regressions, and unintended changes.
|
|
96
|
+
3. Independently run the original Verify commands and appropriate build/tests. Do not rely on pasted squad output.
|
|
97
|
+
4. Run integration/E2E in the real target or production-like environment when the request affects runtime behavior. If genuinely not applicable or impossible, record the precise reason and mark it unverified.
|
|
98
|
+
5. Fix discovered defects and repeat checks. Squad QA PASS does not override your findings.
|
|
99
|
+
6. Call squad_review with contract checks, diff review, actual command/result evidence, integration/E2E evidence, and remaining issues.
|
|
100
|
+
|
|
101
|
+
Do not ask the user whether you should verify. Do not merely summarize the squad report. Until squad_review records PASS/PASS_WITH_ISSUES, the work is not accepted.
|
|
102
|
+
</squad_review_required>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cleanList(values: string[]): string[] {
|
|
106
|
+
return values.map((value) => value.trim()).filter(Boolean);
|
|
107
|
+
}
|
package/src/scheduler.ts
CHANGED
|
@@ -19,6 +19,7 @@ import * as store from "./store.js";
|
|
|
19
19
|
import { debug, logError } from "./logger.js";
|
|
20
20
|
import { buildAgentSystemPrompt } from "./protocol.js";
|
|
21
21
|
import { buildAdvisorConsultText, formatAdvisorSteerMessage, adviceNeedsHuman, type AdvisorConsultInput } from "./advisor.js";
|
|
22
|
+
import { beginOrchestratorReview } from "./review.js";
|
|
22
23
|
|
|
23
24
|
// ============================================================================
|
|
24
25
|
// Types
|
|
@@ -31,7 +32,7 @@ export type SchedulerEventType =
|
|
|
31
32
|
| "task_blocked"
|
|
32
33
|
| "task_unblocked"
|
|
33
34
|
| "task_rework"
|
|
34
|
-
| "
|
|
35
|
+
| "squad_review_required"
|
|
35
36
|
| "squad_failed"
|
|
36
37
|
| "escalation"
|
|
37
38
|
| "activity";
|
|
@@ -996,9 +997,12 @@ export class Scheduler {
|
|
|
996
997
|
);
|
|
997
998
|
|
|
998
999
|
if (allDone) {
|
|
999
|
-
|
|
1000
|
+
// Agent execution is only a candidate result. It cannot become "done"
|
|
1001
|
+
// until the main Pi independently reviews it against the original contract.
|
|
1002
|
+
if (squad.status === "review") return;
|
|
1003
|
+
beginOrchestratorReview(squad);
|
|
1000
1004
|
store.saveSquad(squad);
|
|
1001
|
-
this.emit({ type: "
|
|
1005
|
+
this.emit({ type: "squad_review_required", squadId: this.squadId });
|
|
1002
1006
|
} else if (anyFailed && !anyInProgress) {
|
|
1003
1007
|
// All remaining tasks are blocked/failed with no way forward
|
|
1004
1008
|
const blockedCount = tasks.filter((t) => t.status === "blocked").length;
|
|
@@ -152,23 +152,30 @@ Use `squad_modify` when:
|
|
|
152
152
|
- **`pause`** / **`resume`**: Stop/restart the entire squad
|
|
153
153
|
- **`cancel`**: Abort everything (user changed their mind)
|
|
154
154
|
|
|
155
|
-
## After
|
|
156
|
-
|
|
157
|
-
When you receive `[squad]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
155
|
+
## After Agents Finish — Mandatory Independent Orchestrator Review
|
|
156
|
+
|
|
157
|
+
Agent execution finishing is **not completion or acceptance**. When you receive `[squad] TASK EXECUTION FINISHED` / `<squad_review_required>`, every squad output—including QA PASS—is an untrusted claim. You are the independent acceptance authority.
|
|
158
|
+
|
|
159
|
+
You MUST do all of this before telling the user the work succeeded:
|
|
160
|
+
1. **Re-read the original contract**: reconstruct every requirement, boundary, and later clarification from the user's main-session conversation. The squad goal/task plan is only a secondary aid and cannot narrow the original request.
|
|
161
|
+
2. **Inspect reality, not reports**: read the actual working-tree/commit diff and relevant source. Check every changed line for correctness, scope, integration points, unintended changes, error paths, security, and regressions.
|
|
162
|
+
3. **Independently verify**: run the original Verify commands, build, and relevant tests yourself. Do not count command output pasted by squad agents as your evidence.
|
|
163
|
+
4. **Run integration/E2E**: individual task/QA passes do not prove the integrated system. Exercise the real user flow in the target or production-like environment whenever runtime behavior changed. If impossible or genuinely inapplicable, state exactly why and mark it unverified—never silently skip it.
|
|
164
|
+
5. **Fix and repeat**: if you find defects, fix them (or route explicit rework), then rerun the affected checks. Squad QA does not overrule your findings.
|
|
165
|
+
6. **Record the gate**: call `squad_review` with requirement-by-requirement contract checks, your diff review, actual command/result evidence, integration/E2E evidence, and all issues. Until that tool records PASS/PASS_WITH_ISSUES, the squad remains `review` rather than `done`.
|
|
166
|
+
7. **Report only reviewed facts**: clearly separate what you personally verified, remaining issues, and anything unverified.
|
|
167
|
+
|
|
168
|
+
Never ask “Want me to run the tests/E2E?” after agents finish. Run required acceptance checks immediately. Never merely relay or paraphrase the squad summary.
|
|
169
|
+
|
|
170
|
+
Example after independent review:
|
|
171
|
+
> Independent orchestrator review complete against the original 7 acceptance criteria:
|
|
172
|
+
> - Inspected `git diff --stat` and the auth/router/test changes; no unrelated files changed
|
|
173
|
+
> - `npm test`: 42/42 passed; `npm run build`: passed
|
|
174
|
+
> - Production-like E2E: login → refresh → protected route → logout passed
|
|
175
|
+
> - Found and fixed a cookie-domain defect missed by squad QA; reran unit + E2E successfully
|
|
176
|
+
> - `squad_review`: PASS
|
|
170
177
|
>
|
|
171
|
-
>
|
|
178
|
+
> Remaining unverified: none.
|
|
172
179
|
|
|
173
180
|
## Decision Framework
|
|
174
181
|
|
|
@@ -181,5 +188,5 @@ Example:
|
|
|
181
188
|
| User says "tell the backend agent to..." | `squad_message` to that agent |
|
|
182
189
|
| User says "add a task for..." | `squad_modify` with `add_task` |
|
|
183
190
|
| User says "cancel/stop" | `squad_modify` with `cancel` |
|
|
184
|
-
|
|
|
191
|
+
| Agents finish | Independently review against original contract, inspect diff, run verification + integration/E2E, call `squad_review`; only then report acceptance |
|
|
185
192
|
| Squad fails | Report what failed, offer options (retry, modify, cancel) |
|
package/src/store.ts
CHANGED
|
@@ -269,11 +269,11 @@ export function listSquads(): string[] {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
export function findActiveSquads(): Squad[] {
|
|
272
|
-
// Includes "failed"
|
|
273
|
-
//
|
|
272
|
+
// Includes "failed" (recoverable) and "review" (main-orchestrator gate), so
|
|
273
|
+
// neither state disappears across session restarts. All callers filter by status.
|
|
274
274
|
return listSquads()
|
|
275
275
|
.map((id) => loadSquad(id))
|
|
276
|
-
.filter((s): s is Squad => s !== null && (s.status === "running" || s.status === "paused" || s.status === "failed"));
|
|
276
|
+
.filter((s): s is Squad => s !== null && (s.status === "running" || s.status === "paused" || s.status === "failed" || s.status === "review"));
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/** List squads filtered by project cwd. If no cwd, returns all. */
|
package/src/types.ts
CHANGED
|
@@ -71,11 +71,24 @@ export const DEFAULT_SQUAD_SETTINGS: SquadSettings = {
|
|
|
71
71
|
// Squad
|
|
72
72
|
// ============================================================================
|
|
73
73
|
|
|
74
|
-
export type SquadStatus = "planning" | "running" | "paused" | "done" | "failed";
|
|
74
|
+
export type SquadStatus = "planning" | "running" | "paused" | "review" | "done" | "failed";
|
|
75
|
+
|
|
76
|
+
export interface SquadReview {
|
|
77
|
+
status: "pending" | "passed" | "failed";
|
|
78
|
+
requestedAt: string;
|
|
79
|
+
completedAt: string | null;
|
|
80
|
+
verdict: "pass" | "pass_with_issues" | "fail" | null;
|
|
81
|
+
contractChecks: string[];
|
|
82
|
+
diffReview: string;
|
|
83
|
+
verificationEvidence: string[];
|
|
84
|
+
integrationEvidence: string;
|
|
85
|
+
issues: string[];
|
|
86
|
+
}
|
|
75
87
|
|
|
76
88
|
export interface SquadConfig {
|
|
77
89
|
maxConcurrency: number;
|
|
78
90
|
autoUnblock: boolean;
|
|
91
|
+
/** @deprecated Independent main-orchestrator review is always required. */
|
|
79
92
|
reviewOnComplete: boolean;
|
|
80
93
|
/** Max rework attempts when QA fails a task (0 = no rework, just fail) */
|
|
81
94
|
maxRetries: number;
|
|
@@ -84,7 +97,7 @@ export interface SquadConfig {
|
|
|
84
97
|
export const DEFAULT_SQUAD_CONFIG: SquadConfig = {
|
|
85
98
|
maxConcurrency: 2,
|
|
86
99
|
autoUnblock: true,
|
|
87
|
-
reviewOnComplete:
|
|
100
|
+
reviewOnComplete: true,
|
|
88
101
|
maxRetries: 2,
|
|
89
102
|
};
|
|
90
103
|
|
|
@@ -99,6 +112,8 @@ export interface Squad {
|
|
|
99
112
|
/** Agent name → overrides. Keys must exist in .pi/squad/agents/ */
|
|
100
113
|
agents: Record<string, SquadAgentEntry>;
|
|
101
114
|
config: SquadConfig;
|
|
115
|
+
/** Mandatory independent main-session review; absent on legacy/running squads. */
|
|
116
|
+
review?: SquadReview;
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
// ============================================================================
|