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 +19 -2
- package/package.json +1 -1
- package/src/advisor.ts +9 -18
- package/src/agent-pool.ts +3 -2
- package/src/index.ts +138 -43
- package/src/panel/squad-widget.ts +4 -1
- package/src/planner.ts +2 -2
- package/src/protocol.ts +6 -9
- package/src/report.ts +20 -0
- package/src/review.ts +107 -0
- package/src/router.ts +1 -1
- package/src/scheduler.ts +23 -19
- package/src/skills/squad-supervisor/SKILL.md +24 -17
- package/src/store.ts +3 -3
- package/src/supervisor.ts +3 -4
- package/src/types.ts +17 -2
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.
|
|
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.
|
|
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
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
|
-
*
|
|
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(
|
|
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
|
|
111
|
-
lines.push(`- ${
|
|
102
|
+
for (const call of input.recentToolCalls) {
|
|
103
|
+
lines.push(`- ${call.trim()}`);
|
|
112
104
|
}
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
|
|
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
|
|
120
|
-
lines.push(`[${msg.from}/${msg.type}] ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
|
233
|
-
if (t.error) line += ` ERROR: ${t.error
|
|
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
|
|
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: "
|
|
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 "
|
|
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
|
-
|
|
529
|
-
//
|
|
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-
|
|
532
|
-
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)}`,
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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
|
|
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 "
|
|
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
|
-
//
|
|
1583
|
-
//
|
|
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-
|
|
1586
|
-
content: `[squad]
|
|
1587
|
-
`
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
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";
|
|
@@ -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).
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
686
|
-
.filter((m) => m.from === task.agent && (m.type === "text" || m.type === "done"))
|
|
687
|
-
|
|
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
|
|
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
|
-
//
|
|
982
|
-
return output
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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/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 =
|
|
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
|
|
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:
|
|
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
|
// ============================================================================
|