pi-squad 0.1.0 → 0.5.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 +288 -75
- package/package.json +7 -3
- package/src/agent-pool.ts +10 -1
- package/src/index.ts +563 -322
- package/src/panel/message-view.ts +140 -122
- package/src/panel/squad-panel.ts +94 -148
- package/src/panel/squad-widget.ts +192 -0
- package/src/panel/task-list.ts +38 -28
- package/src/planner.ts +1 -1
- package/src/protocol.ts +37 -0
- package/src/scheduler.ts +216 -5
- package/src/skills/supervisor/SKILL.md +111 -0
- package/src/skills/verification/SKILL.md +32 -0
- package/src/store.ts +29 -1
- package/src/types.ts +11 -0
package/src/panel/task-list.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
6
7
|
import type { Task, TaskStatus } from "../types.js";
|
|
7
8
|
import type { Scheduler } from "../scheduler.js";
|
|
8
9
|
import * as store from "../store.js";
|
|
@@ -80,29 +81,33 @@ export class TaskListView {
|
|
|
80
81
|
return this.padToHeight(lines, maxLines);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const scrollStart = Math.max(0, Math.min(selectedIndex - Math.floor(maxLines / 2), taskLines.length - maxLines + 4));
|
|
88
|
-
const visibleTasks = taskLines.slice(scrollStart, scrollStart + maxLines - 4);
|
|
89
|
-
lines.push(...visibleTasks);
|
|
84
|
+
// Build bottom sections first so we know how much space they need
|
|
85
|
+
const bottomLines: string[] = [];
|
|
86
|
+
bottomLines.push("");
|
|
87
|
+
bottomLines.push(truncateToWidth(th.fg("border", " " + "─".repeat(width - 2)), width, ""));
|
|
90
88
|
|
|
91
|
-
// Separator
|
|
92
|
-
lines.push("");
|
|
93
|
-
lines.push(th.fg("border", " " + "─".repeat(width - 2)));
|
|
94
|
-
|
|
95
|
-
// Live activity for selected or first running task
|
|
96
89
|
const runningTask = tasks.find((t) => t.status === "in_progress");
|
|
97
90
|
if (runningTask) {
|
|
98
|
-
|
|
91
|
+
bottomLines.push(...this.renderLiveActivity(runningTask, width, scheduler));
|
|
99
92
|
}
|
|
100
93
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
bottomLines.push("");
|
|
95
|
+
bottomLines.push(this.renderSummary(tasks, width));
|
|
96
|
+
|
|
97
|
+
// Task list gets remaining space
|
|
98
|
+
const taskLines = this.renderTaskTree(tasks, selectedIndex, width);
|
|
99
|
+
const taskSpace = Math.max(3, maxLines - bottomLines.length);
|
|
100
|
+
const scrollStart = Math.max(0, Math.min(selectedIndex - Math.floor(taskSpace / 2), taskLines.length - taskSpace));
|
|
101
|
+
const visibleTasks = taskLines.slice(scrollStart, scrollStart + taskSpace);
|
|
102
|
+
|
|
103
|
+
// Pad task area to exact size so bottom sticks
|
|
104
|
+
while (visibleTasks.length < taskSpace) visibleTasks.push("");
|
|
105
|
+
lines.push(...visibleTasks);
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
// Append bottom (always at the same position)
|
|
108
|
+
lines.push(...bottomLines);
|
|
109
|
+
|
|
110
|
+
return lines.slice(0, maxLines);
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
// =========================================================================
|
|
@@ -150,7 +155,7 @@ export class TaskListView {
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
const line = ` ${cursor} ${icon} ${taskName} ${agentTag}${labelStr}${timeStr}`;
|
|
153
|
-
lines.push(line);
|
|
158
|
+
lines.push(truncateToWidth(line, width, "…"));
|
|
154
159
|
|
|
155
160
|
// Show blocked-by info
|
|
156
161
|
if (task.status === "blocked" && task.depends.length > 0) {
|
|
@@ -160,16 +165,17 @@ export class TaskListView {
|
|
|
160
165
|
return dep && dep.status !== "done";
|
|
161
166
|
});
|
|
162
167
|
if (blockers.length > 0) {
|
|
163
|
-
lines.push(
|
|
164
|
-
` ${th.fg("dim", "└ waiting on: " + blockers.join(", "))}`,
|
|
165
|
-
);
|
|
168
|
+
lines.push(truncateToWidth(
|
|
169
|
+
` ${th.fg("dim", "└ waiting on: " + blockers.join(", "))}`, width, "…",
|
|
170
|
+
));
|
|
166
171
|
}
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
// Show error for failed tasks
|
|
170
175
|
if (task.status === "failed" && task.error) {
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
lines.push(truncateToWidth(
|
|
177
|
+
` ${th.fg("error", "└ " + task.error)}`, width, "…",
|
|
178
|
+
));
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
181
|
|
|
@@ -184,7 +190,10 @@ export class TaskListView {
|
|
|
184
190
|
const th = this.theme;
|
|
185
191
|
const lines: string[] = [];
|
|
186
192
|
|
|
187
|
-
lines.push(
|
|
193
|
+
lines.push(truncateToWidth(
|
|
194
|
+
th.fg("border", ` ── ${task.id} (live) `) + th.fg("border", "─".repeat(Math.max(0, width - task.id.length - 12))),
|
|
195
|
+
width, "",
|
|
196
|
+
));
|
|
188
197
|
|
|
189
198
|
// Get recent messages for this task
|
|
190
199
|
const messages = store.loadMessages(this.squadId, task.id);
|
|
@@ -193,12 +202,13 @@ export class TaskListView {
|
|
|
193
202
|
for (const msg of recent) {
|
|
194
203
|
if (msg.type === "tool") {
|
|
195
204
|
const toolStr = `→ ${msg.name || msg.text}`;
|
|
196
|
-
const
|
|
205
|
+
const rawArgs = (msg.args?.path || msg.args?.command || "").toString();
|
|
206
|
+
const argsStr = rawArgs.split("\n")[0]; // first line only
|
|
197
207
|
const preview = argsStr ? `${toolStr} ${argsStr}` : toolStr;
|
|
198
|
-
lines.push(` ${th.fg("muted", preview
|
|
208
|
+
lines.push(truncateToWidth(` ${th.fg("muted", preview)}`, width, "…"));
|
|
199
209
|
} else if (msg.type === "text" && msg.from !== "system") {
|
|
200
|
-
const preview = msg.text.split("\n")[0]
|
|
201
|
-
lines.push(` ${th.fg("dim", `"${preview}"`)}
|
|
210
|
+
const preview = msg.text.split("\n")[0];
|
|
211
|
+
lines.push(truncateToWidth(` ${th.fg("dim", `"${preview}"`)}`, width, "…"));
|
|
202
212
|
}
|
|
203
213
|
}
|
|
204
214
|
|
package/src/planner.ts
CHANGED
|
@@ -31,7 +31,7 @@ export async function runPlanner(options: PlannerOptions): Promise<PlannerOutput
|
|
|
31
31
|
const { goal, cwd, model } = options;
|
|
32
32
|
|
|
33
33
|
const plannerDef = loadAgentDef("planner", cwd);
|
|
34
|
-
const allAgents = loadAllAgentDefs(cwd).filter((a) => a.name !== "planner");
|
|
34
|
+
const allAgents = loadAllAgentDefs(cwd).filter((a) => a.name !== "planner" && !a.disabled);
|
|
35
35
|
|
|
36
36
|
const agentList = allAgents
|
|
37
37
|
.map((a) => `- **${a.name}** (${a.role}): ${a.description} [tags: ${a.tags.join(", ")}]`)
|
package/src/protocol.ts
CHANGED
|
@@ -234,6 +234,42 @@ ${task.description || "(no additional description)"}
|
|
|
234
234
|
`;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// Rework Context
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
function buildReworkContext(task: Task, squadId: string): string {
|
|
242
|
+
if (!task.retryOf) return "";
|
|
243
|
+
|
|
244
|
+
const originalTask = loadAllTasks(squadId).find((t) => t.id === task.retryOf);
|
|
245
|
+
|
|
246
|
+
const lines: string[] = [
|
|
247
|
+
"# ⚠️ REWORK — Fix Issues From Previous Attempt\n",
|
|
248
|
+
`This is attempt #${task.retryCount || 1} to fix issues in **${task.retryOf}**.\n`,
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
if (originalTask?.output) {
|
|
252
|
+
lines.push("## What Was Built (Previous Attempt)");
|
|
253
|
+
lines.push(originalTask.output.slice(0, 2000));
|
|
254
|
+
lines.push("");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (task.qaFeedback) {
|
|
258
|
+
lines.push("## QA Feedback — What Needs Fixing");
|
|
259
|
+
lines.push(task.qaFeedback);
|
|
260
|
+
lines.push("");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
lines.push("## Instructions");
|
|
264
|
+
lines.push("- Read the QA feedback carefully — fix ONLY the reported issues");
|
|
265
|
+
lines.push("- Do NOT rewrite everything from scratch");
|
|
266
|
+
lines.push("- Make targeted, minimal fixes");
|
|
267
|
+
lines.push("- Re-run the failing tests to verify your fixes");
|
|
268
|
+
lines.push("- Include test output as evidence in your completion message\n");
|
|
269
|
+
|
|
270
|
+
return lines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
|
|
237
273
|
// ============================================================================
|
|
238
274
|
// Full Prompt Assembly
|
|
239
275
|
// ============================================================================
|
|
@@ -255,6 +291,7 @@ export function buildAgentSystemPrompt(options: ProtocolBuildOptions): string {
|
|
|
255
291
|
buildSquadProtocol(task.agent, agentDef, squad),
|
|
256
292
|
buildAgentIdentity(agentDef),
|
|
257
293
|
buildTaskSection(task),
|
|
294
|
+
buildReworkContext(task, squadId),
|
|
258
295
|
buildChainContext(task, allTasks, squadId),
|
|
259
296
|
buildSiblingAwareness(task, allTasks, modifiedFiles),
|
|
260
297
|
buildKnowledgeSection(squadId),
|
package/src/scheduler.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type SchedulerEventType =
|
|
|
26
26
|
| "task_failed"
|
|
27
27
|
| "task_blocked"
|
|
28
28
|
| "task_unblocked"
|
|
29
|
+
| "task_rework"
|
|
29
30
|
| "squad_completed"
|
|
30
31
|
| "squad_failed"
|
|
31
32
|
| "escalation"
|
|
@@ -54,6 +55,8 @@ export class Scheduler {
|
|
|
54
55
|
private listeners: SchedulerEventListener[] = [];
|
|
55
56
|
private skillPaths: string[] = [];
|
|
56
57
|
private running = false;
|
|
58
|
+
/** Track spawn retries to allow one retry per task */
|
|
59
|
+
private spawnRetries = new Set<string>();
|
|
57
60
|
|
|
58
61
|
/** Get the project cwd for this squad (from squad.json) */
|
|
59
62
|
getProjectCwd(): string | undefined {
|
|
@@ -210,6 +213,9 @@ export class Scheduler {
|
|
|
210
213
|
await this.spawnAgentForTask(task, squad);
|
|
211
214
|
} catch (error) {
|
|
212
215
|
console.error(`[squad-scheduler] Failed to spawn ${task.id}: ${(error as Error).message}`);
|
|
216
|
+
// MUST fail the task — otherwise it stays in_progress forever
|
|
217
|
+
// with no process (zombie state)
|
|
218
|
+
this.handleTaskFailed(task.id, `Spawn failed: ${(error as Error).message}`);
|
|
213
219
|
}
|
|
214
220
|
}
|
|
215
221
|
|
|
@@ -236,6 +242,11 @@ export class Scheduler {
|
|
|
236
242
|
return;
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
if (agentDef.disabled) {
|
|
246
|
+
this.handleTaskFailed(task.id, `Agent '${task.agent}' is disabled. Enable it with /squad agents or edit ${task.agent}.json`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
239
250
|
// Apply squad-level model override
|
|
240
251
|
const squadAgentEntry = squad.agents[task.agent];
|
|
241
252
|
if (squadAgentEntry?.model) {
|
|
@@ -375,8 +386,29 @@ export class Scheduler {
|
|
|
375
386
|
if (exitCode === 0 || hadMeaningfulWork) {
|
|
376
387
|
this.handleTaskCompleted(event.taskId).then(() => this.updateContext());
|
|
377
388
|
} else {
|
|
378
|
-
|
|
379
|
-
|
|
389
|
+
// Agent died with no work done — retry once before failing.
|
|
390
|
+
// Transient failures (resource pressure, startup race) are common
|
|
391
|
+
// when multiple agents spawn simultaneously.
|
|
392
|
+
const retryKey = `spawn-retry:${event.taskId}`;
|
|
393
|
+
if (!this.spawnRetries.has(retryKey)) {
|
|
394
|
+
this.spawnRetries.add(retryKey);
|
|
395
|
+
const stderr = event.data?.stderr || "";
|
|
396
|
+
console.error(`[squad-scheduler] Agent ${event.agentName} died instantly (code ${exitCode}). Retrying in 2s... stderr: ${stderr.slice(0, 200)}`);
|
|
397
|
+
store.updateTaskStatus(this.squadId, event.taskId, "pending");
|
|
398
|
+
store.appendMessage(this.squadId, event.taskId, {
|
|
399
|
+
ts: store.now(),
|
|
400
|
+
from: "system",
|
|
401
|
+
type: "status",
|
|
402
|
+
text: `Agent crashed on startup (code ${exitCode}). Retrying...`,
|
|
403
|
+
});
|
|
404
|
+
// Delay retry to let resources settle
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
if (this.running) this.scheduleReadyTasks();
|
|
407
|
+
}, 2000);
|
|
408
|
+
} else {
|
|
409
|
+
const stderr = event.data?.stderr || "";
|
|
410
|
+
this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} (retry exhausted). ${stderr.slice(0, 500)}`);
|
|
411
|
+
}
|
|
380
412
|
this.updateContext();
|
|
381
413
|
}
|
|
382
414
|
// Skip the updateContext() below — handled in the branches above
|
|
@@ -432,9 +464,31 @@ export class Scheduler {
|
|
|
432
464
|
message: output,
|
|
433
465
|
});
|
|
434
466
|
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
this.
|
|
467
|
+
// Check for QA rework: if this is a QA/test task and it found failures,
|
|
468
|
+
// create a rework task for the original agent instead of proceeding
|
|
469
|
+
const reworkCreated = this.checkForRework(task, output);
|
|
470
|
+
|
|
471
|
+
if (!reworkCreated) {
|
|
472
|
+
// Normal flow: auto-unblock dependents
|
|
473
|
+
console.error(`[squad-scheduler] handleTaskCompleted: ${taskId} done, auto-unblocking dependents`);
|
|
474
|
+
this.autoUnblock(taskId);
|
|
475
|
+
|
|
476
|
+
// If this is a passing retest, also unblock dependents of the ORIGINAL
|
|
477
|
+
// QA task. When qa-auth failed, its dependents weren't unblocked.
|
|
478
|
+
// Now that the retest passes, those dependents should proceed.
|
|
479
|
+
if (task.retryOf) {
|
|
480
|
+
// Walk up the retry chain to find the root task
|
|
481
|
+
let rootId = task.retryOf;
|
|
482
|
+
const allTasks = store.loadAllTasks(this.squadId);
|
|
483
|
+
let root = allTasks.find((t) => t.id === rootId);
|
|
484
|
+
while (root?.retryOf) {
|
|
485
|
+
rootId = root.retryOf;
|
|
486
|
+
root = allTasks.find((t) => t.id === rootId);
|
|
487
|
+
}
|
|
488
|
+
console.error(`[squad-scheduler] Retest passed — also unblocking dependents of original: ${rootId}`);
|
|
489
|
+
this.autoUnblock(rootId);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
438
492
|
|
|
439
493
|
// Schedule next ready tasks (may spawn new agents)
|
|
440
494
|
console.error(`[squad-scheduler] handleTaskCompleted: scheduling next ready tasks`);
|
|
@@ -527,6 +581,163 @@ export class Scheduler {
|
|
|
527
581
|
}
|
|
528
582
|
}
|
|
529
583
|
|
|
584
|
+
// =========================================================================
|
|
585
|
+
// QA Rework Loop
|
|
586
|
+
// =========================================================================
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Check if a completed task is a QA task that found failures.
|
|
590
|
+
* If so, create a rework task for the original agent and a retest task for QA.
|
|
591
|
+
* Returns true if rework was created (caller should NOT auto-unblock dependents).
|
|
592
|
+
*/
|
|
593
|
+
private checkForRework(task: Task, output: string): boolean {
|
|
594
|
+
// Only trigger rework for QA/test agent tasks
|
|
595
|
+
const qaAgents = ["qa", "tester", "security"];
|
|
596
|
+
if (!qaAgents.includes(task.agent)) return false;
|
|
597
|
+
|
|
598
|
+
// Parse verdict from output
|
|
599
|
+
const verdict = this.parseQaVerdict(output);
|
|
600
|
+
if (verdict === "pass") return false;
|
|
601
|
+
|
|
602
|
+
// Find the implementation task(s) this QA task was testing
|
|
603
|
+
const allTasks = store.loadAllTasks(this.squadId);
|
|
604
|
+
const implDeps = task.depends
|
|
605
|
+
.map((depId) => allTasks.find((t) => t.id === depId))
|
|
606
|
+
.filter((t): t is Task => t !== null && !qaAgents.includes(t.agent));
|
|
607
|
+
|
|
608
|
+
if (implDeps.length === 0) return false;
|
|
609
|
+
|
|
610
|
+
const squad = store.loadSquad(this.squadId);
|
|
611
|
+
if (!squad) return false;
|
|
612
|
+
|
|
613
|
+
// Extract the failure details for feedback
|
|
614
|
+
const feedback = this.extractQaFeedback(output);
|
|
615
|
+
|
|
616
|
+
let createdAny = false;
|
|
617
|
+
for (const implTask of implDeps) {
|
|
618
|
+
// Check retry limit
|
|
619
|
+
const retryCount = store.getRetryCount(this.squadId, implTask.retryOf || implTask.id);
|
|
620
|
+
const originalId = implTask.retryOf || implTask.id;
|
|
621
|
+
|
|
622
|
+
if (retryCount >= squad.config.maxRetries) {
|
|
623
|
+
console.error(`[squad-scheduler] Retry limit reached for ${originalId} (${retryCount}/${squad.config.maxRetries})`);
|
|
624
|
+
this.emit({
|
|
625
|
+
type: "escalation",
|
|
626
|
+
squadId: this.squadId,
|
|
627
|
+
taskId: task.id,
|
|
628
|
+
agentName: task.agent,
|
|
629
|
+
message: `QA failed ${originalId} ${retryCount} times. Retry limit reached.\nLatest feedback:\n${feedback.slice(0, 500)}`,
|
|
630
|
+
});
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const fixN = retryCount + 1;
|
|
635
|
+
|
|
636
|
+
// Create rework task for the original agent
|
|
637
|
+
const reworkId = `${originalId}-fix-${fixN}`;
|
|
638
|
+
const reworkTask: Task = {
|
|
639
|
+
id: reworkId,
|
|
640
|
+
title: `Fix: ${implTask.title} (attempt ${fixN})`,
|
|
641
|
+
description: `QA found issues in ${implTask.id}. Fix the problems described below.\n\n## QA Feedback\n${feedback}`,
|
|
642
|
+
agent: implTask.agent,
|
|
643
|
+
status: "pending",
|
|
644
|
+
depends: [],
|
|
645
|
+
created: store.now(),
|
|
646
|
+
started: null,
|
|
647
|
+
completed: null,
|
|
648
|
+
output: null,
|
|
649
|
+
error: null,
|
|
650
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0, turns: 0 },
|
|
651
|
+
retryOf: originalId,
|
|
652
|
+
retryCount: fixN,
|
|
653
|
+
qaFeedback: feedback,
|
|
654
|
+
};
|
|
655
|
+
store.createTask(this.squadId, reworkTask);
|
|
656
|
+
|
|
657
|
+
// Create retest task for QA
|
|
658
|
+
const retestId = `${task.id}-retest-${fixN}`;
|
|
659
|
+
const retestTask: Task = {
|
|
660
|
+
id: retestId,
|
|
661
|
+
title: `Re-test: ${implTask.title} (after fix ${fixN})`,
|
|
662
|
+
description: `Re-test ${implTask.id} after rework. Verify the issues from the previous QA round are fixed.\n\nPrevious issues:\n${feedback}`,
|
|
663
|
+
agent: task.agent,
|
|
664
|
+
status: "blocked",
|
|
665
|
+
depends: [reworkId],
|
|
666
|
+
created: store.now(),
|
|
667
|
+
started: null,
|
|
668
|
+
completed: null,
|
|
669
|
+
output: null,
|
|
670
|
+
error: null,
|
|
671
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0, turns: 0 },
|
|
672
|
+
retryOf: task.id,
|
|
673
|
+
retryCount: fixN,
|
|
674
|
+
};
|
|
675
|
+
store.createTask(this.squadId, retestTask);
|
|
676
|
+
|
|
677
|
+
store.appendMessage(this.squadId, task.id, {
|
|
678
|
+
ts: store.now(),
|
|
679
|
+
from: "system",
|
|
680
|
+
type: "status",
|
|
681
|
+
text: `QA failed — creating rework task ${reworkId} for ${implTask.agent} and retest ${retestId}`,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
this.emit({
|
|
685
|
+
type: "task_rework",
|
|
686
|
+
squadId: this.squadId,
|
|
687
|
+
taskId: reworkId,
|
|
688
|
+
agentName: implTask.agent,
|
|
689
|
+
message: `QA found issues in ${implTask.id}. Rework attempt ${fixN}.`,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
console.error(`[squad-scheduler] Rework: ${reworkId} (${implTask.agent}) + retest ${retestId} (${task.agent})`);
|
|
693
|
+
createdAny = true;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return createdAny;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Parse QA verdict from task output */
|
|
700
|
+
private parseQaVerdict(output: string): "pass" | "fail" | "pass_with_issues" {
|
|
701
|
+
const lower = output.toLowerCase();
|
|
702
|
+
|
|
703
|
+
// Look for structured verdict line: "## Verdict: FAIL" or "Verdict: PASS"
|
|
704
|
+
const verdictMatch = output.match(/##?\s*Verdict:\s*(PASS WITH ISSUES|PASS|FAIL)/i);
|
|
705
|
+
if (verdictMatch) {
|
|
706
|
+
const v = verdictMatch[1].toUpperCase();
|
|
707
|
+
if (v === "FAIL") return "fail";
|
|
708
|
+
if (v === "PASS WITH ISSUES") return "pass_with_issues";
|
|
709
|
+
return "pass";
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Fallback: look for common failure patterns
|
|
713
|
+
if (
|
|
714
|
+
lower.includes("verdict: fail") ||
|
|
715
|
+
lower.includes("status: fail") ||
|
|
716
|
+
/\d+\s+(?:tests?\s+)?fail(?:ed|ing|ure)/i.test(output) ||
|
|
717
|
+
(lower.includes("fail") && lower.includes("test") && !lower.includes("0 fail"))
|
|
718
|
+
) {
|
|
719
|
+
return "fail";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return "pass";
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Extract actionable feedback from QA output */
|
|
726
|
+
private extractQaFeedback(output: string): string {
|
|
727
|
+
// Try to extract "## Issues" or "## Failures" section
|
|
728
|
+
const issuesMatch = output.match(/##\s*(?:Issues|Failures|Bugs|Problems|Failed Tests)[\s\S]*?(?=\n##\s|$)/i);
|
|
729
|
+
if (issuesMatch) return issuesMatch[0].trim();
|
|
730
|
+
|
|
731
|
+
// Try to extract lines containing "FAIL", "Error", "✗"
|
|
732
|
+
const failLines = output.split("\n")
|
|
733
|
+
.filter((line) => /fail|error|✗|✘|broken|bug/i.test(line))
|
|
734
|
+
.slice(0, 20);
|
|
735
|
+
if (failLines.length > 0) return failLines.join("\n");
|
|
736
|
+
|
|
737
|
+
// Fallback: last 500 chars
|
|
738
|
+
return output.slice(-500);
|
|
739
|
+
}
|
|
740
|
+
|
|
530
741
|
// =========================================================================
|
|
531
742
|
// Squad Completion
|
|
532
743
|
// =========================================================================
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: squad-supervisor
|
|
3
|
+
description: Manage multi-agent squads — monitor progress, respond to escalations, relay human instructions, and summarize results. Use when a squad is running or when the user asks about squad status, agents, or task progress.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Squad Supervisor
|
|
7
|
+
|
|
8
|
+
You are the supervisor of multi-agent squads. Agents work on decomposed tasks in the background while you coordinate with the user.
|
|
9
|
+
|
|
10
|
+
## Your Role
|
|
11
|
+
|
|
12
|
+
You are the bridge between the human and the squad. You:
|
|
13
|
+
- Start squads for complex tasks (use the `squad` tool)
|
|
14
|
+
- Monitor progress and relay status to the user
|
|
15
|
+
- Handle escalations when agents get stuck
|
|
16
|
+
- Send instructions to agents on behalf of the user
|
|
17
|
+
- Summarize results when the squad completes
|
|
18
|
+
|
|
19
|
+
## When to Use Squad
|
|
20
|
+
|
|
21
|
+
**Use squad** when the user's request involves:
|
|
22
|
+
- 2+ concerns (backend + frontend, code + tests, implementation + docs)
|
|
23
|
+
- Work that benefits from parallel execution
|
|
24
|
+
- Tasks that would overflow a single agent's context
|
|
25
|
+
- Projects needing specialist knowledge (security audit + implementation)
|
|
26
|
+
|
|
27
|
+
**Don't use squad** for:
|
|
28
|
+
- Quick single-file edits
|
|
29
|
+
- Simple questions or explanations
|
|
30
|
+
- Tasks a single agent can finish in a few minutes
|
|
31
|
+
|
|
32
|
+
## Monitoring a Running Squad
|
|
33
|
+
|
|
34
|
+
### Passive monitoring (automatic)
|
|
35
|
+
The squad status is injected into your context via `<squad_status>` before each response.
|
|
36
|
+
Read it to stay aware of progress without needing to call tools.
|
|
37
|
+
|
|
38
|
+
### Active monitoring (on-demand)
|
|
39
|
+
Use `squad_status` when:
|
|
40
|
+
- The user asks "how's the squad doing?"
|
|
41
|
+
- You need detailed info not in the status block
|
|
42
|
+
- You want to check a specific squad by ID
|
|
43
|
+
|
|
44
|
+
### What to tell the user
|
|
45
|
+
- Summarize in plain language: "2 of 4 tasks done, tests are running, docs waiting on API"
|
|
46
|
+
- Highlight blockers: "The QA agent is stuck — it needs the API endpoint list"
|
|
47
|
+
- Report costs when relevant: "Squad is at $0.45 so far"
|
|
48
|
+
|
|
49
|
+
## Handling Escalations
|
|
50
|
+
|
|
51
|
+
When you receive `[squad] Agent needs attention`:
|
|
52
|
+
1. **Read the message** — understand what the agent needs
|
|
53
|
+
2. **Check if you can answer** — often it's a decision only the human can make
|
|
54
|
+
3. **If you can answer**: use `squad_message` to reply directly
|
|
55
|
+
4. **If you can't**: ask the user, then relay their answer via `squad_message`
|
|
56
|
+
|
|
57
|
+
Common escalation patterns:
|
|
58
|
+
- **"Which approach should I use?"** → Ask the user for preference, relay via `squad_message`
|
|
59
|
+
- **"I need info from another agent"** → Check if that agent is done, relay their output
|
|
60
|
+
- **"I'm blocked by a failing test"** → Check the error, suggest a fix via `squad_message`
|
|
61
|
+
- **"The dependency output is unclear"** → Read the dep's messages, clarify for the agent
|
|
62
|
+
|
|
63
|
+
## Sending Messages to Agents
|
|
64
|
+
|
|
65
|
+
Use `squad_message` with:
|
|
66
|
+
- `taskId` — target a specific task
|
|
67
|
+
- `agent` — target whichever task an agent is working on
|
|
68
|
+
- `message` — your instruction or answer
|
|
69
|
+
|
|
70
|
+
Keep messages **specific and actionable**:
|
|
71
|
+
- Good: "Use RS256 for JWT signing. The secret is in env var JWT_SECRET."
|
|
72
|
+
- Bad: "Figure out the auth approach."
|
|
73
|
+
|
|
74
|
+
## Modifying a Running Squad
|
|
75
|
+
|
|
76
|
+
Use `squad_modify` when:
|
|
77
|
+
- **`add_task`**: User requests something not in the original plan
|
|
78
|
+
- **`cancel_task`**: A task is no longer needed
|
|
79
|
+
- **`pause_task`** / **`resume_task`**: Temporarily halt an agent
|
|
80
|
+
- **`pause`** / **`resume`**: Stop/restart the entire squad
|
|
81
|
+
- **`cancel`**: Abort everything (user changed their mind)
|
|
82
|
+
|
|
83
|
+
## After Squad Completes
|
|
84
|
+
|
|
85
|
+
When you receive `[squad] Squad completed`:
|
|
86
|
+
1. Read the summary of what each agent produced
|
|
87
|
+
2. Summarize for the user in plain language
|
|
88
|
+
3. Highlight any issues or partial results
|
|
89
|
+
4. Suggest next steps if applicable
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
> Squad finished all 4 tasks:
|
|
93
|
+
> - API endpoints created at `/api/auth` and `/api/users`
|
|
94
|
+
> - JWT middleware with RS256 validation
|
|
95
|
+
> - 12 tests passing
|
|
96
|
+
> - README updated with API docs
|
|
97
|
+
>
|
|
98
|
+
> Total cost: $1.23. Want me to run the full test suite or deploy?
|
|
99
|
+
|
|
100
|
+
## Decision Framework
|
|
101
|
+
|
|
102
|
+
| Situation | Action |
|
|
103
|
+
|---|---|
|
|
104
|
+
| User asks complex task | Start squad with `squad` tool |
|
|
105
|
+
| User asks "what's happening?" | Read `<squad_status>`, summarize |
|
|
106
|
+
| Agent escalates | Triage → answer or ask user |
|
|
107
|
+
| User says "tell the backend agent to..." | `squad_message` to that agent |
|
|
108
|
+
| User says "add a task for..." | `squad_modify` with `add_task` |
|
|
109
|
+
| User says "cancel/stop" | `squad_modify` with `cancel` |
|
|
110
|
+
| Squad completes | Summarize results, suggest next steps |
|
|
111
|
+
| Squad fails | Report what failed, offer options (retry, modify, cancel) |
|
|
@@ -62,3 +62,35 @@ $ npm test -- test/auth.test.ts
|
|
|
62
62
|
- Include relevant code snippets as evidence
|
|
63
63
|
- State confidence level on uncertain findings
|
|
64
64
|
- List what you checked AND what you didn't check
|
|
65
|
+
|
|
66
|
+
## QA Verdict Format (REQUIRED for test/QA tasks)
|
|
67
|
+
|
|
68
|
+
Your final message MUST end with a structured verdict that automation can parse:
|
|
69
|
+
|
|
70
|
+
### If everything passes:
|
|
71
|
+
```
|
|
72
|
+
## Verdict: PASS
|
|
73
|
+
All N tests passing. No issues found.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### If tests fail:
|
|
77
|
+
```
|
|
78
|
+
## Verdict: FAIL
|
|
79
|
+
|
|
80
|
+
## Issues
|
|
81
|
+
1. **[file:line]** Description of the failure
|
|
82
|
+
- Expected: X
|
|
83
|
+
- Got: Y
|
|
84
|
+
2. **[file:line]** Another issue
|
|
85
|
+
- Steps to reproduce: ...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### If tests pass but with concerns:
|
|
89
|
+
```
|
|
90
|
+
## Verdict: PASS WITH ISSUES
|
|
91
|
+
|
|
92
|
+
## Minor Issues
|
|
93
|
+
1. Description of concern (non-blocking)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The verdict line (`## Verdict: PASS/FAIL/PASS WITH ISSUES`) is **machine-parsed** by the squad system. When you output `FAIL`, the squad automatically creates a rework task for the original agent with your feedback, then re-tests after the fix. Make your Issues section **specific and actionable** — the fixing agent only sees your feedback.
|
package/src/store.ts
CHANGED
|
@@ -136,7 +136,18 @@ function readJsonl<T>(filePath: string): T[] {
|
|
|
136
136
|
try {
|
|
137
137
|
const content = fs.readFileSync(filePath, "utf-8").trim();
|
|
138
138
|
if (!content) return [];
|
|
139
|
-
|
|
139
|
+
// Parse each line individually — skip partial/corrupt lines
|
|
140
|
+
// (can happen when reading while an agent is mid-write)
|
|
141
|
+
const results: T[] = [];
|
|
142
|
+
for (const line of content.split("\n")) {
|
|
143
|
+
if (!line.trim()) continue;
|
|
144
|
+
try {
|
|
145
|
+
results.push(JSON.parse(line) as T);
|
|
146
|
+
} catch {
|
|
147
|
+
// Skip corrupt/partial line — likely mid-write
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
140
151
|
} catch {
|
|
141
152
|
return [];
|
|
142
153
|
}
|
|
@@ -394,6 +405,23 @@ export function loadAllKnowledge(squadId: string): KnowledgeEntry[] {
|
|
|
394
405
|
].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
395
406
|
}
|
|
396
407
|
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// Rework Helpers
|
|
410
|
+
// ============================================================================
|
|
411
|
+
|
|
412
|
+
/** Find all retry tasks for a given original task ID */
|
|
413
|
+
export function findRetries(squadId: string, originalTaskId: string): Task[] {
|
|
414
|
+
return loadAllTasks(squadId).filter((t) => t.retryOf === originalTaskId);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Get the retry count for a task chain (original + all retries) */
|
|
418
|
+
export function getRetryCount(squadId: string, taskId: string): number {
|
|
419
|
+
const task = loadTask(squadId, taskId);
|
|
420
|
+
if (!task) return 0;
|
|
421
|
+
if (task.retryCount !== undefined) return task.retryCount;
|
|
422
|
+
return findRetries(squadId, taskId).length;
|
|
423
|
+
}
|
|
424
|
+
|
|
397
425
|
// ============================================================================
|
|
398
426
|
// Memory (cross-squad)
|
|
399
427
|
// ============================================================================
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface AgentDef {
|
|
|
17
17
|
tags: string[];
|
|
18
18
|
/** System prompt injected via --append-system-prompt */
|
|
19
19
|
prompt: string;
|
|
20
|
+
/** When true, agent is hidden from the planner and cannot be assigned tasks */
|
|
21
|
+
disabled?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Agent entry in squad.json — just overrides, references an AgentDef by key */
|
|
@@ -34,12 +36,15 @@ export interface SquadConfig {
|
|
|
34
36
|
maxConcurrency: number;
|
|
35
37
|
autoUnblock: boolean;
|
|
36
38
|
reviewOnComplete: boolean;
|
|
39
|
+
/** Max rework attempts when QA fails a task (0 = no rework, just fail) */
|
|
40
|
+
maxRetries: number;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export const DEFAULT_SQUAD_CONFIG: SquadConfig = {
|
|
40
44
|
maxConcurrency: 2,
|
|
41
45
|
autoUnblock: true,
|
|
42
46
|
reviewOnComplete: false,
|
|
47
|
+
maxRetries: 2,
|
|
43
48
|
};
|
|
44
49
|
|
|
45
50
|
export interface Squad {
|
|
@@ -79,6 +84,12 @@ export interface Task {
|
|
|
79
84
|
output: string | null;
|
|
80
85
|
error: string | null;
|
|
81
86
|
usage: TaskUsage;
|
|
87
|
+
/** If this is a rework task, the original task ID it's fixing */
|
|
88
|
+
retryOf?: string;
|
|
89
|
+
/** How many times this task chain has been retried */
|
|
90
|
+
retryCount?: number;
|
|
91
|
+
/** QA feedback that triggered this rework */
|
|
92
|
+
qaFeedback?: string;
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
// ============================================================================
|