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 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. On completion, pi receives a summary with each task's **complete, untruncated output**
45
- 6. Multiple squads can run concurrently across different projects
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
5
5
  "type": "module",
6
6
  "scripts": {
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
- if (!squadEnabled) return;
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) return;
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) return;
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 the squad completes, review evidence like a QA agent before reporting success",
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: "What the squad should accomplish" }),
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 "squad_completed": {
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
- // followUp + triggerTurn: wake an idle main agent to review;
527
- // if it's mid-conversation with the human, deliver after it finishes
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-completed",
530
- content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\nSummary:\n${summary}\n\nTotal cost: $${totalCost.toFixed(4)}\n\n${REVIEW_INSTRUCTIONS}`,
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 "squad_completed": {
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
- // followUp + triggerTurn: wake an idle main agent to review; if it's
1578
- // mid-conversation with the human, deliver after it finishes
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-completed",
1581
- content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
1582
- `Summary:\n${summary}\n\n` +
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
- REVIEW_INSTRUCTIONS,
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
- | "squad_completed"
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
- squad.status = "done";
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: "squad_completed", squadId: this.squadId });
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 Squad CompletesReviewing Completed Work
156
-
157
- When you receive `[squad] Squad completed`, you are the last line of review. Do NOT just relay the summary:
158
- 1. **Check verdicts**: scan task outputs for QA verdicts (`## Verdict: PASS/FAIL/PASS WITH ISSUES`) and note any `PASS WITH ISSUES` minor issues
159
- 2. **Check evidence**: each task's output should include verification evidence (commands + results). If a task claimed done without evidence, run its Verify command yourself (build, test, curl — whatever the task description specified)
160
- 3. **Spot-check integration**: individual tasks passing doesn't guarantee the integrated result works run the end-to-end check when one exists (app builds, server starts, main flow works)
161
- 4. **Report with evidence**: tell the user what was verified (with the commands/results), and explicitly flag anything unverified or concerning
162
- 5. Suggest next steps if applicable
163
-
164
- Example:
165
- > Squad finished all 4 tasks:
166
- > - API endpoints created at `/api/auth` and `/api/users`
167
- > - JWT middleware with RS256 validation
168
- > - 12 tests passing
169
- > - README updated with API docs
155
+ ## After Agents FinishMandatory 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
- > Total cost: $1.23. Want me to run the full test suite or deploy?
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
- | Squad completes | Summarize results, suggest next steps |
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": failure is recoverable (resume resets failed tasks), so
273
- // failed squads must stay discoverable. All callers filter by status.
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: false,
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
  // ============================================================================