pi-squad 0.15.0 → 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,25 @@ 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 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.
59
+
60
+ ### No-Truncation Contract
61
+
62
+ Task messages, task outputs, dependency/rework handoffs, QA feedback, advisor handoffs, completion reports, failure diagnostics, and planner errors are persisted and forwarded in full. There is no character or task-count limit on report data. TUI widgets may show width/height-limited **views** to fit the terminal, but the underlying data and agent/main-session handoffs remain complete.
46
63
 
47
64
  ## Features
48
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.15.0",
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/advisor.ts CHANGED
@@ -78,18 +78,10 @@ Output format:
78
78
 
79
79
  Keep it short. The agent reads your advice and immediately acts on it.`;
80
80
 
81
- const MAX_MSG_CHARS = 400;
82
- const MAX_MESSAGES = 12;
83
- const MAX_TOOL_CALLS = 10;
84
-
85
- function clamp(text: string, max: number): string {
86
- const t = text.trim();
87
- return t.length > max ? `${t.slice(0, max).trimEnd()}…` : t;
88
- }
89
-
90
81
  /**
91
82
  * Build the user-message digest sent to the advisor model.
92
- * Curated and bounded — summaries, not full transcripts.
83
+ * Preserve complete descriptions, messages, and tool history: advisor handoffs
84
+ * follow the same no-truncation contract as task and completion reports.
93
85
  */
94
86
  export function buildAdvisorConsultText(input: AdvisorConsultInput): string {
95
87
  const lines: string[] = [];
@@ -102,22 +94,21 @@ export function buildAdvisorConsultText(input: AdvisorConsultInput): string {
102
94
  lines.push(`Progress: ${input.turnCount} turns, ~${Math.round(input.elapsedMinutes)} min elapsed`);
103
95
  lines.push("");
104
96
  lines.push(`## Task Description`);
105
- lines.push(clamp(input.taskDescription || "(no description)", 1500));
97
+ lines.push((input.taskDescription || "(no description)").trim());
106
98
 
107
99
  if (input.recentToolCalls.length > 0) {
108
100
  lines.push("");
109
101
  lines.push(`## Recent Tool Activity (newest last)`);
110
- for (const call of input.recentToolCalls.slice(-MAX_TOOL_CALLS)) {
111
- lines.push(`- ${clamp(call, 160)}`);
102
+ for (const call of input.recentToolCalls) {
103
+ lines.push(`- ${call.trim()}`);
112
104
  }
113
105
  }
114
106
 
115
- const messages = input.recentMessages.slice(-MAX_MESSAGES);
116
- if (messages.length > 0) {
107
+ if (input.recentMessages.length > 0) {
117
108
  lines.push("");
118
109
  lines.push(`## Recent Messages (newest last)`);
119
- for (const msg of messages) {
120
- lines.push(`[${msg.from}/${msg.type}] ${clamp(msg.text, MAX_MSG_CHARS)}`);
110
+ for (const msg of input.recentMessages) {
111
+ lines.push(`[${msg.from}/${msg.type}] ${msg.text.trim()}`);
121
112
  }
122
113
  }
123
114
 
@@ -141,5 +132,5 @@ export function formatAdvisorSteerMessage(advice: string, reason: string): strin
141
132
 
142
133
  /** True when the advisor's verdict says a human decision is required. */
143
134
  export function adviceNeedsHuman(advice: string): boolean {
144
- return /needs human input/i.test(advice.slice(0, 200));
135
+ return /needs human input/i.test(advice);
145
136
  }
package/src/agent-pool.ts CHANGED
@@ -217,7 +217,7 @@ export class AgentPool {
217
217
  proc.on("exit", (code, signal) => {
218
218
  // Log diagnostic info for debugging spawn failures
219
219
  if (code !== 0 && code !== null) {
220
- logError("squad-pool", `${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr.slice(0, 300) || "(empty)"}`);
220
+ logError("squad-pool", `${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr || "(empty)"}`);
221
221
  }
222
222
  // Capture activity stats BEFORE deleting the agent
223
223
  const finalActivity = agentProc.activity;
@@ -232,7 +232,8 @@ export class AgentPool {
232
232
  agentName: agentDef.name,
233
233
  data: {
234
234
  exitCode: code,
235
- stderr: stderr.slice(-2000),
235
+ // Preserve complete diagnostics for task failure reports and recovery.
236
+ stderr,
236
237
  turnCount: finalActivity.turnCount,
237
238
  toolCallCount: finalActivity.recentToolCalls.length,
238
239
  filesModified: finalActivity.modifiedFiles.size,
package/src/index.ts CHANGED
@@ -25,6 +25,8 @@ import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
25
25
  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
+ import { buildCompletionSummary, buildFailureSummary } from "./report.js";
29
+ import { buildOrchestratorReviewGate, recordOrchestratorReview } from "./review.js";
28
30
 
29
31
  // ============================================================================
30
32
  // State
@@ -44,14 +46,6 @@ let uiCtx: import("@earendil-works/pi-coding-agent").ExtensionContext | null = n
44
46
  const widgetState: SquadWidgetState = { squadId: null, enabled: true };
45
47
  let widgetControls: { requestUpdate: () => void; dispose: () => void } | null = null;
46
48
 
47
- /** Reviewer instructions appended to squad-completed notifications —
48
- * makes the main session behave like the QA/verification agents do. */
49
- const REVIEW_INSTRUCTIONS =
50
- "Before reporting success to the user, REVIEW the work like a QA agent would: " +
51
- "(1) check task outputs for QA verdicts (## Verdict: PASS/FAIL) and verification evidence (commands + results); " +
52
- "(2) if a task claimed done without evidence, run its Verify command yourself; " +
53
- "(3) report to the user what was verified (with evidence) and flag anything unverified — don't just relay the summary.";
54
-
55
49
  /**
56
50
  * Resolve a model string (or null = session default) to its context window.
57
51
  * Reads uiCtx lazily so it always uses the live session's registry.
@@ -214,14 +208,33 @@ export default function (pi: ExtensionAPI) {
214
208
 
215
209
  // Inject squad awareness before each LLM call
216
210
  pi.on("before_agent_start", async (event, ctx) => {
217
- 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
+ }
218
220
 
219
221
  // When a squad is active, inject its status
220
222
  if (activeSquadId) {
221
223
  const squad = store.loadSquad(activeSquadId);
222
- 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
+ }
223
231
  const tasks = store.loadAllTasks(activeSquadId);
224
- 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
+ }
225
238
 
226
239
  const doneCount = tasks.filter((t) => t.status === "done").length;
227
240
  const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
@@ -229,8 +242,8 @@ export default function (pi: ExtensionAPI) {
229
242
  const taskLines = tasks.map((t) => {
230
243
  const icon = t.status === "done" ? "✓" : t.status === "in_progress" ? "⏳" : t.status === "failed" ? "✗" : t.status === "blocked" ? "◻" : "·";
231
244
  let line = ` ${icon} ${t.id} (${t.agent}) [${t.status}]`;
232
- if (t.output) line += ` — ${t.output.split("\n")[0].slice(0, 80)}`;
233
- if (t.error) line += ` ERROR: ${t.error.slice(0, 60)}`;
245
+ if (t.output) line += ` — ${t.output}`;
246
+ if (t.error) line += ` ERROR: ${t.error}`;
234
247
  return line;
235
248
  }).join("\n");
236
249
 
@@ -240,6 +253,8 @@ export default function (pi: ExtensionAPI) {
240
253
  `Status: ${squad.status} | ${doneCount}/${tasks.length} tasks | $${totalCost.toFixed(2)}`,
241
254
  taskLines,
242
255
  `</squad_status>`,
256
+ ...(squad.status === "review" ? [buildOrchestratorReviewGate(squad, tasks)] : []),
257
+ ...pendingReviewGates.filter(({ squad: pending }) => pending.id !== squad.id).map(({ gate }) => gate),
243
258
  `You have an active squad. Use squad_message to talk to agents, squad_status for details, squad_modify to change tasks.`,
244
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.`,
245
260
  ].join("\n");
@@ -266,7 +281,8 @@ export default function (pi: ExtensionAPI) {
266
281
  ].filter(Boolean).join("\n");
267
282
 
268
283
  return {
269
- 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") : ""),
270
286
  };
271
287
  });
272
288
 
@@ -297,10 +313,10 @@ export default function (pi: ExtensionAPI) {
297
313
  "Providing tasks yourself makes you the planner — follow the planner rules (contract task first, final QA task, 3-7 tasks)",
298
314
  "Act on ⚠️ plan warnings in the response — fix with squad_modify or address at review",
299
315
  "After starting a squad: report the plan and END YOUR TURN — never poll squad_status or sleep-wait; squad events wake you automatically",
300
- "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",
301
317
  ],
302
318
  parameters: Type.Object({
303
- 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." }),
304
320
  agents: Type.Optional(
305
321
  Type.Record(
306
322
  Type.String(),
@@ -407,6 +423,7 @@ export default function (pi: ExtensionAPI) {
407
423
  `Status: ${context.status}`,
408
424
  `Elapsed: ${context.elapsed}`,
409
425
  `Cost: $${context.costs.total.toFixed(4)}`,
426
+ ...(context.status === "review" ? ["Acceptance: BLOCKED — independent main-orchestrator review required via squad_review"] : []),
410
427
  "",
411
428
  "Tasks:",
412
429
  taskLines,
@@ -416,6 +433,70 @@ export default function (pi: ExtensionAPI) {
416
433
  },
417
434
  });
418
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
+
419
500
  // =========================================================================
420
501
  // Tool: squad_message
421
502
  // =========================================================================
@@ -517,19 +598,17 @@ export default function (pi: ExtensionAPI) {
517
598
  scheduler.onEvent((event: SchedulerEvent) => {
518
599
  forceWidgetUpdate();
519
600
  switch (event.type) {
520
- case "squad_completed": {
601
+ case "squad_review_required": {
521
602
  const tasks = store.loadAllTasks(squadId);
522
- const summary = tasks
523
- .filter((t) => t.status === "done")
524
- .map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
525
- .join("\n");
603
+ const summary = buildCompletionSummary(tasks);
526
604
  const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
527
605
  const s = schedulers.get(squadId); if (s) s.updateContext();
528
- // followUp + triggerTurn: wake an idle main agent to review;
529
- // 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.
530
609
  pi.sendMessage({
531
- customType: "squad-completed",
532
- 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)}`,
533
612
  display: true,
534
613
  }, { triggerTurn: true, deliverAs: "followUp" });
535
614
  schedulers.delete(squadId);
@@ -542,7 +621,7 @@ export default function (pi: ExtensionAPI) {
542
621
  const done = tasks.filter((t) => t.status === "done");
543
622
  pi.sendMessage({
544
623
  customType: "squad-failed",
545
- content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\nFailed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}`,
624
+ content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\nFailed: ${buildFailureSummary(tasks)}`,
546
625
  display: true,
547
626
  }, { triggerTurn: true, deliverAs: "followUp" });
548
627
  forceWidgetUpdate();
@@ -725,6 +804,24 @@ export default function (pi: ExtensionAPI) {
725
804
  }
726
805
  }
727
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
+
728
825
  // Register Ctrl+Q terminal input handler for panel toggle
729
826
  if (ctx.hasUI) {
730
827
  ctx.ui.onTerminalInput((data) => {
@@ -932,7 +1029,7 @@ export default function (pi: ExtensionAPI) {
932
1029
  const msgSched = getActiveScheduler();
933
1030
  if (msgSched) {
934
1031
  await msgSched.sendHumanMessage(targetTaskId, msgText);
935
- ctx.ui.notify(`Sent to ${targetAgent}: "${msgText.slice(0, 50)}"`, "info");
1032
+ ctx.ui.notify(`Sent to ${targetAgent}: "${msgText}"`, "info");
936
1033
  } else {
937
1034
  store.appendMessage(activeSquadId, targetTaskId, {
938
1035
  ts: store.now(),
@@ -1010,7 +1107,7 @@ export default function (pi: ExtensionAPI) {
1010
1107
  const tasks = store.loadAllTasks(s.id);
1011
1108
  const done = tasks.filter((t) => t.status === "done").length;
1012
1109
  const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
1013
- 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" ? "✗" : "·";
1014
1111
  return `${icon} ${s.id} [${s.status}] ${done}/${tasks.length} $${cost.toFixed(2)}`;
1015
1112
  }),
1016
1113
  ];
@@ -1210,7 +1307,7 @@ export default function (pi: ExtensionAPI) {
1210
1307
  `Tags: ${agent.tags.join(", ")}`,
1211
1308
  ``,
1212
1309
  `Prompt:`,
1213
- `${agent.prompt.slice(0, 300)}${agent.prompt.length > 300 ? "..." : ""}`,
1310
+ agent.prompt,
1214
1311
  ``,
1215
1312
  `File: ${store.getGlobalAgentsDir()}/${agent.name}.json`,
1216
1313
  ].join("\n");
@@ -1320,7 +1417,7 @@ async function pickSquad(
1320
1417
  const tasks = store.loadAllTasks(s.id);
1321
1418
  const done = tasks.filter((t) => t.status === "done").length;
1322
1419
  const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
1323
- 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" ? "✗" : "·";
1324
1421
  const project = showProject ? ` — ${s.cwd.split("/").pop()}` : "";
1325
1422
  return `${icon} ${s.id} [${s.status}] ${done}/${tasks.length} $${cost.toFixed(2)}${project}`;
1326
1423
  });
@@ -1399,7 +1496,7 @@ function openPanel(
1399
1496
  const panelSched = schedulers.get(squadId);
1400
1497
  if (input && panelSched) {
1401
1498
  await panelSched.sendHumanMessage(taskId, input);
1402
- ctx.ui.notify(`Sent to ${agentName}: "${input.slice(0, 50)}"`, "info");
1499
+ ctx.ui.notify(`Sent to ${agentName}: "${input}"`, "info");
1403
1500
  } else if (input) {
1404
1501
  store.appendMessage(squadId, taskId, {
1405
1502
  ts: store.now(),
@@ -1565,12 +1662,9 @@ async function startSquad(
1565
1662
  // Update widget on every scheduler event
1566
1663
  forceWidgetUpdate();
1567
1664
  switch (event.type) {
1568
- case "squad_completed": {
1665
+ case "squad_review_required": {
1569
1666
  const tasks = store.loadAllTasks(squadId);
1570
- const summary = tasks
1571
- .filter((t) => t.status === "done")
1572
- .map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
1573
- .join("\n");
1667
+ const summary = buildCompletionSummary(tasks);
1574
1668
  const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
1575
1669
 
1576
1670
  // Final context update before clearing scheduler
@@ -1578,15 +1672,16 @@ async function startSquad(
1578
1672
  if (completedSched) {
1579
1673
  completedSched.updateContext();
1580
1674
  }
1675
+ const squad = store.loadSquad(squadId)!;
1581
1676
 
1582
- // followUp + triggerTurn: wake an idle main agent to review; if it's
1583
- // 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.
1584
1679
  pi.sendMessage({
1585
- customType: "squad-completed",
1586
- content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
1587
- `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` +
1588
1683
  `Total cost: $${totalCost.toFixed(4)}\n\n` +
1589
- REVIEW_INSTRUCTIONS,
1684
+ buildOrchestratorReviewGate(squad, tasks),
1590
1685
  display: true,
1591
1686
  }, { triggerTurn: true, deliverAs: "followUp" });
1592
1687
 
@@ -1605,7 +1700,7 @@ async function startSquad(
1605
1700
  customType: "squad-failed",
1606
1701
  content: `[squad] Squad "${squadId}" has stalled. ` +
1607
1702
  `${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
1608
- `Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}\n` +
1703
+ `Failed: ${buildFailureSummary(tasks)}\n` +
1609
1704
  `Use squad_status for details or squad_modify to adjust.`,
1610
1705
  display: true,
1611
1706
  }, { triggerTurn: true, deliverAs: "followUp" });
@@ -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/planner.ts CHANGED
@@ -152,7 +152,7 @@ async function runPiJson(options: PiJsonOptions): Promise<string> {
152
152
  }
153
153
 
154
154
  if (code !== 0 && messages.length === 0) {
155
- reject(new Error(`Planner failed (code ${code}): ${stderr.slice(0, 500)}`));
155
+ reject(new Error(`Planner failed (code ${code}): ${stderr}`));
156
156
  return;
157
157
  }
158
158
 
@@ -217,7 +217,7 @@ function parsePlannerOutput(text: string, validAgents?: Set<string>): PlannerOut
217
217
  return parsed as PlannerOutput;
218
218
  } catch (error) {
219
219
  if (error instanceof SyntaxError) {
220
- throw new Error(`Planner output is not valid JSON: ${text.slice(0, 200)}`);
220
+ throw new Error(`Planner output is not valid JSON: ${text}`);
221
221
  }
222
222
  throw error;
223
223
  }
package/src/protocol.ts CHANGED
@@ -102,7 +102,6 @@ function buildChainContext(task: Task, allTasks: Task[], squadId: string): strin
102
102
  const messages = loadMessages(squadId, dep.id);
103
103
  const lastText = messages
104
104
  .filter((m) => m.from === dep.agent && (m.type === "text" || m.type === "done"))
105
- .slice(-3)
106
105
  .map((m) => m.text)
107
106
  .join("\n");
108
107
  if (lastText) {
@@ -155,12 +154,9 @@ function buildSiblingAwareness(
155
154
  for (const [agent, files] of fileEntries) {
156
155
  if (files.length > 0) {
157
156
  lines.push(`**${agent}:**`);
158
- for (const f of files.slice(0, 10)) {
157
+ for (const f of files) {
159
158
  lines.push(` - ${f}`);
160
159
  }
161
- if (files.length > 10) {
162
- lines.push(` - ...and ${files.length - 10} more`);
163
- }
164
160
  }
165
161
  }
166
162
  lines.push(
@@ -187,7 +183,7 @@ function buildKnowledgeSection(squadId: string): string {
187
183
 
188
184
  if (decisions.length > 0) {
189
185
  lines.push("## Decisions");
190
- for (const d of decisions.slice(-10)) {
186
+ for (const d of decisions) {
191
187
  lines.push(`- ${d.text} (${d.from})`);
192
188
  }
193
189
  lines.push("");
@@ -195,7 +191,7 @@ function buildKnowledgeSection(squadId: string): string {
195
191
 
196
192
  if (conventions.length > 0) {
197
193
  lines.push("## Project Conventions");
198
- for (const c of conventions.slice(-10)) {
194
+ for (const c of conventions) {
199
195
  lines.push(`- ${c.text} (${c.from})`);
200
196
  }
201
197
  lines.push("");
@@ -203,7 +199,7 @@ function buildKnowledgeSection(squadId: string): string {
203
199
 
204
200
  if (findings.length > 0) {
205
201
  lines.push("## Findings");
206
- for (const f of findings.slice(-10)) {
202
+ for (const f of findings) {
207
203
  lines.push(`- ${f.text} (${f.from})`);
208
204
  }
209
205
  lines.push("");
@@ -256,7 +252,8 @@ function buildReworkContext(task: Task, squadId: string): string {
256
252
 
257
253
  if (originalTask?.output) {
258
254
  lines.push("## What Was Built (Previous Attempt)");
259
- lines.push(originalTask.output.slice(0, 2000));
255
+ // Rework agents need the complete prior handoff, not an arbitrary prefix.
256
+ lines.push(originalTask.output);
260
257
  lines.push("");
261
258
  }
262
259
 
package/src/report.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Task } from "./types.js";
2
+
3
+ /**
4
+ * Build the full task handoff included in squad completion notifications.
5
+ * This is durable report data, not a UI preview: never shorten task output.
6
+ */
7
+ export function buildCompletionSummary(tasks: Task[]): string {
8
+ return tasks
9
+ .filter((task) => task.status === "done")
10
+ .map((task) => `- ${task.id} (${task.agent}): ${task.output || "done"}`)
11
+ .join("\n");
12
+ }
13
+
14
+ /** Build the full failure handoff without shortening diagnostics. */
15
+ export function buildFailureSummary(tasks: Task[]): string {
16
+ return tasks
17
+ .filter((task) => task.status === "failed")
18
+ .map((task) => `${task.id}: ${task.error || "unknown error"}`)
19
+ .join("; ");
20
+ }
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/router.ts CHANGED
@@ -202,6 +202,6 @@ export class Router {
202
202
  return line.trim();
203
203
  }
204
204
  }
205
- return text.slice(0, 200);
205
+ return text;
206
206
  }
207
207
  }
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";
@@ -465,7 +466,7 @@ export class Scheduler {
465
466
  agentName: task.agent,
466
467
  agentRole: agentDef?.role || task.agent,
467
468
  reason,
468
- recentMessages: store.loadMessages(this.squadId, taskId).slice(-12).map((m) => ({ from: m.from, type: m.type, text: m.text })),
469
+ recentMessages: store.loadMessages(this.squadId, taskId).map((m) => ({ from: m.from, type: m.type, text: m.text })),
469
470
  recentToolCalls: activity ? [...activity.recentToolCalls] : [],
470
471
  turnCount: activity?.turnCount || 0,
471
472
  elapsedMinutes: activity ? (Date.now() - activity.startedAt) / 60000 : 0,
@@ -488,7 +489,7 @@ export class Scheduler {
488
489
  squadId: this.squadId,
489
490
  taskId,
490
491
  agentName,
491
- message: `${reason}\n\nAdvisor assessment:\n${advice.slice(0, 800)}`,
492
+ message: `${reason}\n\nAdvisor assessment:\n${advice}`,
492
493
  });
493
494
  return true; // escalation already emitted with richer context
494
495
  }
@@ -563,7 +564,9 @@ export class Scheduler {
563
564
  ts: store.now(),
564
565
  from: event.agentName,
565
566
  type: "text",
566
- text: text.slice(0, 2000),
567
+ // Persist the complete handoff. Reports can be arbitrarily long;
568
+ // presentation layers may viewport them, but source data must never truncate.
569
+ text,
567
570
  });
568
571
  }
569
572
 
@@ -636,7 +639,7 @@ export class Scheduler {
636
639
  const reason = turnCount === 0
637
640
  ? `exited with 0 turns (likely rate limit or API error)`
638
641
  : `exited with ${turnCount} turns but 0 tool calls (no work done)`;
639
- logError("squad-scheduler", `Agent ${event.agentName} ${reason}, code=${exitCode}. Retrying in 2s... stderr: ${stderr.slice(0, 200)}`);
642
+ logError("squad-scheduler", `Agent ${event.agentName} ${reason}, code=${exitCode}. Retrying in 2s... stderr: ${stderr}`);
640
643
  store.updateTaskStatus(this.squadId, event.taskId, "pending");
641
644
  store.appendMessage(this.squadId, event.taskId, {
642
645
  ts: store.now(),
@@ -650,7 +653,7 @@ export class Scheduler {
650
653
  }, 2000);
651
654
  } else {
652
655
  const stderr = event.data?.stderr || "";
653
- this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} (retry exhausted). ${stderr.slice(0, 500)}`);
656
+ this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} (retry exhausted). ${stderr}`);
654
657
  }
655
658
  this.updateContext();
656
659
  }
@@ -682,10 +685,9 @@ export class Scheduler {
682
685
 
683
686
  // Extract output from last messages
684
687
  const messages = store.loadMessages(this.squadId, taskId);
685
- const lastAgentMessages = messages
686
- .filter((m) => m.from === task.agent && (m.type === "text" || m.type === "done"))
687
- .slice(-3);
688
- const output = lastAgentMessages.map((m) => m.text).join("\n");
688
+ const agentMessages = messages
689
+ .filter((m) => m.from === task.agent && (m.type === "text" || m.type === "done"));
690
+ const output = agentMessages.map((m) => m.text).join("\n");
689
691
 
690
692
  store.updateTaskStatus(this.squadId, taskId, "done", {
691
693
  output: output || "Task completed",
@@ -869,7 +871,7 @@ export class Scheduler {
869
871
  squadId: this.squadId,
870
872
  taskId: task.id,
871
873
  agentName: task.agent,
872
- message: `QA failed ${originalId} ${retryCount} times. Retry limit reached.\nLatest feedback:\n${feedback.slice(0, 500)}`,
874
+ message: `QA failed ${originalId} ${retryCount} times. Retry limit reached.\nLatest feedback:\n${feedback}`,
873
875
  });
874
876
  continue;
875
877
  }
@@ -974,12 +976,11 @@ export class Scheduler {
974
976
 
975
977
  // Try to extract lines containing "FAIL", "Error", "✗"
976
978
  const failLines = output.split("\n")
977
- .filter((line) => /fail|error|✗|✘|broken|bug/i.test(line))
978
- .slice(0, 20);
979
+ .filter((line) => /fail|error|✗|✘|broken|bug/i.test(line));
979
980
  if (failLines.length > 0) return failLines.join("\n");
980
981
 
981
- // Fallback: last 500 chars
982
- return output.slice(-500);
982
+ // Preserve the complete QA handoff when no structured failure section exists.
983
+ return output;
983
984
  }
984
985
 
985
986
  // =========================================================================
@@ -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;
@@ -1047,7 +1051,7 @@ export class Scheduler {
1047
1051
  status: task.status,
1048
1052
  agent: task.agent,
1049
1053
  title: task.title,
1050
- ...(task.output ? { output: task.output.slice(0, 500) } : {}),
1054
+ ...(task.output ? { output: task.output } : {}),
1051
1055
  ...(task.status === "blocked"
1052
1056
  ? {
1053
1057
  blockedBy: task.depends.filter((d) => {
@@ -1089,7 +1093,7 @@ export class Scheduler {
1089
1093
  action:
1090
1094
  msg.type === "tool"
1091
1095
  ? `→ ${msg.name} ${msg.args?.path || msg.args?.command || ""}`.trim()
1092
- : msg.text.slice(0, 80),
1096
+ : msg.text,
1093
1097
  });
1094
1098
  }
1095
1099
  }
@@ -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/supervisor.ts CHANGED
@@ -70,10 +70,9 @@ export async function analyzeStuckAgent(
70
70
  ): Promise<{ action: "retry" | "reassign" | "escalate"; reason: string; suggestion?: string }> {
71
71
  const task = store.loadTask(squadId, taskId);
72
72
  const messages = store.loadMessages(squadId, taskId);
73
- const recentMessages = messages.slice(-10);
74
73
 
75
- // Simple heuristic for now
76
- const errorMessages = recentMessages.filter((m) => m.type === "error");
74
+ // Simple heuristic for now — inspect the complete task history.
75
+ const errorMessages = messages.filter((m) => m.type === "error");
77
76
  if (errorMessages.length >= 3) {
78
77
  return {
79
78
  action: "escalate",
@@ -128,7 +127,7 @@ export async function analyzeBlockRequest(
128
127
  return {
129
128
  action: "create_subtask",
130
129
  subtask: {
131
- title: `Support: ${blockReason.slice(0, 60)}`,
130
+ title: `Support: ${blockReason}`,
132
131
  agent: "fullstack",
133
132
  description: `Created from block request by ${agentName} on task ${taskId}: ${blockReason}`,
134
133
  },
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
  // ============================================================================