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.
@@ -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
- // Render task list
84
- const taskLines = this.renderTaskTree(tasks, selectedIndex, width);
85
-
86
- // Scroll window
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
- lines.push(...this.renderLiveActivity(runningTask, width, scheduler));
91
+ bottomLines.push(...this.renderLiveActivity(runningTask, width, scheduler));
99
92
  }
100
93
 
101
- // Summary line
102
- lines.push("");
103
- lines.push(this.renderSummary(tasks, width));
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
- return this.padToHeight(lines, maxLines);
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
- const errorPreview = task.error.slice(0, width - 10);
172
- lines.push(` ${th.fg("error", "└ " + errorPreview)}`);
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(th.fg("border", ` ── ${task.id} (live) `) + th.fg("border", "─".repeat(Math.max(0, width - task.id.length - 12))));
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 argsStr = msg.args?.path || msg.args?.command || "";
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.slice(0, width - 2))}`);
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].slice(0, width - 4);
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
- const stderr = event.data?.stderr || "";
379
- this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode}. ${stderr.slice(0, 500)}`);
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
- // Auto-unblock dependents
436
- console.error(`[squad-scheduler] handleTaskCompleted: ${taskId} done, auto-unblocking dependents`);
437
- this.autoUnblock(taskId);
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
- return content.split("\n").map((line) => JSON.parse(line) as T);
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
  // ============================================================================