pi-squad 0.4.14 → 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 CHANGED
@@ -22,7 +22,7 @@ Ask pi to do something complex. It calls the `squad` tool automatically:
22
22
  > Build a REST API with authentication, tests, and documentation
23
23
  ```
24
24
 
25
- Or be explicit with predefined tasks:
25
+ The planner agent reads your codebase and creates a task breakdown automatically. Or be explicit:
26
26
 
27
27
  ```
28
28
  > Use squad: goal="Build task API", tasks=[
@@ -34,17 +34,18 @@ Or be explicit with predefined tasks:
34
34
 
35
35
  ### What Happens
36
36
 
37
- 1. Extension creates tasks and starts the scheduler
37
+ 1. Planner analyzes the codebase and creates tasks (or uses your predefined tasks)
38
38
  2. A **live widget** appears above the editor showing task status
39
39
  3. Agents spawn as separate pi processes, work in parallel where dependencies allow
40
40
  4. QA agents can trigger **automatic rework loops** when they find bugs
41
- 5. When complete, pi reports the summary with costs
41
+ 5. When complete, pi reports the summary notifications don't pollute message history
42
+ 6. Multiple squads can run concurrently across different projects
42
43
 
43
44
  ## User Interface
44
45
 
45
46
  ### Widget (above editor)
46
47
 
47
- Always visible when a squad is active. Component-based, event-driven rendering — updates on every scheduler event with no polling.
48
+ Always visible when a squad is active. Debounced updates with cache key to avoid flicker.
48
49
 
49
50
  ```
50
51
  ⏳ squad Build task API 2/3 $0.58 3m12s ^q detail · /squad msg
@@ -57,7 +58,7 @@ Features:
57
58
  - Per-task elapsed time (warning color after 3 minutes)
58
59
  - Live tool call preview for running tasks
59
60
  - Stale detection — shows `⏳ idle` when agent has no output for 2+ minutes
60
- - Responsive — caps visible tasks based on terminal height
61
+ - Responsive — caps visible tasks on small terminals, shows `+N more` overflow
61
62
  - Completion duration for finished tasks
62
63
 
63
64
  ### Status Bar (footer)
@@ -68,9 +69,9 @@ Features:
68
69
 
69
70
  ### Panel (Ctrl+Q)
70
71
 
71
- Centered overlay panel. Press `Ctrl+Q` to open/close.
72
+ Centered overlay panel with proper lifecycle management. Press `Ctrl+Q` to open/close.
72
73
 
73
- **Task list:**
74
+ **Task list** with sticky summary at bottom:
74
75
  ```
75
76
  ╭─ squad: Build task API ─────────────────────╮
76
77
  │ ▸ ✓ api (backend) 2m12s │
@@ -86,15 +87,18 @@ Centered overlay panel. Press `Ctrl+Q` to open/close.
86
87
  ╰──────────────────────────────────────────────╯
87
88
  ```
88
89
 
89
- **Message view** (Enter on a task):
90
+ **Message view** (Enter on a task) — long text wraps instead of truncating:
90
91
  ```
91
92
  ╭─ tests · qa ⏳ ─────────────────────────────╮
92
93
  │ 10:03 qa │
93
- │ Starting test implementation
94
+ │ Starting test implementation. Will cover
95
+ │ all CRUD endpoints, validation, and │
96
+ │ edge cases for empty input. │
94
97
  │ 10:04 qa │
95
98
  │ → write src/tasks.test.js │
96
99
  │ 10:05 YOU │
97
100
  │ Also test edge cases for empty input │
101
+ │ ─ 65% ─ 24 msgs ─ ↑↓ scroll │
98
102
  ├──────────────────────────────────────────────┤
99
103
  │ ↑↓ scroll m send esc back ^q close │
100
104
  ╰──────────────────────────────────────────────╯
@@ -110,10 +114,14 @@ Centered overlay panel. Press `Ctrl+Q` to open/close.
110
114
  | `/squad agents` | List, view, edit, enable/disable agents |
111
115
  | `/squad agents <name>` | Quick view of a specific agent |
112
116
  | `/squad msg [agent] text` | Send message to a running agent |
113
- | `/squad widget` | Toggle live widget |
117
+ | `/squad widget` | Toggle live widget on/off |
114
118
  | `/squad panel` | Toggle overlay panel |
115
119
  | `/squad cancel` | Cancel running squad |
116
120
  | `/squad clear` | Dismiss widget, deactivate view |
121
+ | `/squad cleanup` | Delete squad data (select one or all) |
122
+ | `/squad cleanup all` | Delete all squad data |
123
+ | `/squad enable` | Enable pi-squad (tools, widget, system prompt) |
124
+ | `/squad disable` | Disable pi-squad completely |
117
125
 
118
126
  ## Keyboard Shortcuts
119
127
 
@@ -137,7 +145,21 @@ Centered overlay panel. Press `Ctrl+Q` to open/close.
137
145
  | `squad_message` | Send message to a running agent via `steer()` |
138
146
  | `squad_modify` | Add/cancel/pause/resume tasks, or cancel/resume entire squad |
139
147
 
140
- The main pi agent sees squad state in its system prompt automatically (`<squad_status>` block) and can relay messages, add tasks, or check status on your behalf.
148
+ The main pi agent sees squad state in its system prompt automatically (`<squad_status>` block) and can relay messages, add tasks, or check status on your behalf. Squad notifications use `sendMessage({ display: true })` so they don't pollute the LLM's message history.
149
+
150
+ ## Multiple Concurrent Squads
151
+
152
+ Multiple squads can run in parallel from the same pi session. Each squad has its own scheduler, agent pool, and disk state.
153
+
154
+ ```
155
+ > Build the auth API → starts squad-1
156
+ > Build the dashboard UI → starts squad-2 (runs in parallel)
157
+
158
+ /squad list → see both, select which to view
159
+ /squad select → switch widget/panel focus
160
+ ```
161
+
162
+ All squads run independently in the background. The widget/panel shows whichever squad you're focused on.
141
163
 
142
164
  ## QA Rework Loop
143
165
 
@@ -190,7 +212,7 @@ All 42 tests passing. No issues found.
190
212
 
191
213
  ```
192
214
  config: {
193
- maxConcurrency: 3, // parallel agents
215
+ maxConcurrency: 3, // parallel agents (default: 2)
194
216
  maxRetries: 2, // rework attempts before escalation (default: 2)
195
217
  }
196
218
  ```
@@ -269,7 +291,7 @@ Create a JSON file in `~/.pi/squad/agents/` (global) or `{project}/.pi/squad/age
269
291
 
270
292
  - `model`: `null` = pi default. Override with any model ID your provider supports.
271
293
  - `tools`: `null` = all tools. Array like `["bash", "read", "write", "edit"]` to restrict.
272
- - `tags`: Used by the planner to match agents to tasks.
294
+ - `tags`: Used by the planner to match agents to tasks automatically.
273
295
  - `disabled`: `true` = hidden from planner, tasks assigned to this agent will fail.
274
296
  - Project-local agents override global agents with the same name.
275
297
 
@@ -277,7 +299,7 @@ Create a JSON file in `~/.pi/squad/agents/` (global) or `{project}/.pi/squad/age
277
299
 
278
300
  ### Chain Context (primary)
279
301
 
280
- When task A completes, its **full output** is injected into task B's system prompt as a `# Completed Dependencies` section. Downstream agents read what was built and extend it.
302
+ When task A completes, its **full output** is injected into task B's system prompt as a `# Completed Dependencies` section. Downstream agents read what was built and extend it — no need for explicit communication.
281
303
 
282
304
  ### Shared Filesystem
283
305
 
@@ -290,7 +312,7 @@ When agents run in parallel, each sees the other's status and modified files in
290
312
  ```
291
313
  # Sibling Tasks
292
314
  - template-engine [in_progress] — fullstack — Create HTML templates
293
-
315
+
294
316
  ## Files Modified by Other Agents
295
317
  **fullstack:** src/templateEngine.js, templates/post.html
296
318
  ⚠️ Coordinate with the owning agent before editing files listed above.
@@ -319,6 +341,7 @@ When a pi session crashes (tmux dies, SIGKILL, etc.):
319
341
  - In-progress tasks are automatically **suspended** on next startup
320
342
  - Squad is marked as **paused**
321
343
  - Only squads with completed tasks trigger a notification
344
+ - Squads with zero progress are silently paused (no noise)
322
345
 
323
346
  ### Resume
324
347
 
@@ -331,9 +354,14 @@ Resume a paused squad across sessions:
331
354
  The agent calls `squad_modify({ action: "resume" })`, which:
332
355
  1. Finds the latest paused squad for the project
333
356
  2. Creates a fresh scheduler from disk state
334
- 3. Restarts suspended tasks
357
+ 3. Wires up event handlers for completion/failure/escalation
358
+ 4. Restarts suspended tasks
359
+
360
+ All state lives on disk — squads are fully reconstructable from JSON files.
335
361
 
336
- All state lives on disk — squads are fully reconstructable.
362
+ ### Spawn Resilience
363
+
364
+ Child agent processes occasionally fail during startup (transient resource pressure). The scheduler automatically retries once after a 2-second delay. If the retry also fails, the task is marked failed with a diagnostic log including PID, exit code, stderr, and stdout line count.
337
365
 
338
366
  ## Governance
339
367
 
@@ -343,13 +371,15 @@ All state lives on disk — squads are fully reconstructable.
343
371
  | **Concurrency control** | `maxConcurrency` limits parallel agents (default: 2) |
344
372
  | **Health monitoring** | Idle warning (3m), stuck intervention (5m), loop detection (same tool 5x), 30m hard ceiling |
345
373
  | **QA rework loop** | Auto-creates fix + retest tasks when QA fails (up to `maxRetries`) |
374
+ | **Spawn retry** | Retries once on transient startup failures, then fails with diagnostics |
346
375
  | **File tracking** | Warns agents about files modified by other agents |
347
376
  | **@mention routing** | Parsed from output, delivered via RPC `steer()` |
348
- | **Escalation** | Blocked agents, stuck agents, and retry limit → notify main agent |
377
+ | **Escalation** | Blocked agents, stuck agents, retry limit, spawn failure → notify main agent |
378
+ | **Clean notifications** | Uses `sendMessage({ display: true })` — visible to user, not in LLM history |
349
379
 
350
380
  ## Data Layout
351
381
 
352
- All state lives in `~/.pi/squad/`. No database, no daemon, no external services. Writes are atomic (temp file + rename).
382
+ All state lives in `~/.pi/squad/`. No database, no daemon, no external services. Writes are atomic (temp file + rename). JSONL reads are fault-tolerant (skip corrupt lines from concurrent writes).
353
383
 
354
384
  ```
355
385
  ~/.pi/squad/
@@ -364,7 +394,7 @@ All state lives in `~/.pi/squad/`. No database, no daemon, no external services.
364
394
  │ ├── decisions.jsonl
365
395
  │ └── findings.jsonl
366
396
  └── {task-id}/ — one directory per task
367
- ├── task.json — status, agent, output, usage
397
+ ├── task.json — status, agent, output, usage, retryOf, qaFeedback
368
398
  └── messages.jsonl — full conversation log
369
399
  ```
370
400
 
@@ -374,46 +404,51 @@ All state lives in `~/.pi/squad/`. No database, no daemon, no external services.
374
404
  src/
375
405
  ├── index.ts — extension entry: tools, commands, widget, panel, lifecycle
376
406
  ├── types.ts — Squad, Task, Agent, Message types
377
- ├── store.ts — JSON/JSONL file I/O with atomic writes
378
- ├── scheduler.ts — dependency DAG, concurrency, rework loop, task lifecycle
379
- ├── agent-pool.ts — pi RPC process spawn/steer/kill
380
- ├── protocol.ts — system prompt builder (7 sections)
407
+ ├── store.ts — JSON/JSONL file I/O with atomic writes, fault-tolerant reads
408
+ ├── scheduler.ts — dependency DAG, concurrency, rework loop, spawn retry, task lifecycle
409
+ ├── agent-pool.ts — pi RPC process spawn/steer/kill, diagnostic logging
410
+ ├── protocol.ts — system prompt builder (7 sections + rework context)
381
411
  ├── router.ts — @mention parsing, cross-agent messaging
382
412
  ├── monitor.ts — health checks: idle/stuck/loop/ceiling
383
413
  ├── supervisor.ts — quality review, block analysis
384
414
  ├── planner.ts — one-shot goal decomposition via LLM
385
415
  ├── panel/
386
416
  │ ├── squad-panel.ts — overlay component (ctx.ui.custom with done())
387
- │ ├── squad-widget.ts — component-based widget (setWidget factory)
388
- │ ├── task-list.ts — task tree with status icons, elapsed time
389
- │ └── message-view.ts — scrollable message log per task
417
+ │ ├── squad-widget.ts — string[] widget with debounce and cache
418
+ │ ├── task-list.ts — task tree with status icons, elapsed time, sticky summary
419
+ │ └── message-view.ts — scrollable message log with word wrap, fixed-height layout
390
420
  ├── skills/
391
421
  │ ├── supervisor/ — teaches main agent to supervise squads
392
422
  │ ├── squad-protocol/ — communication rules for child agents
393
423
  │ ├── collaboration/ — team interaction patterns
394
- │ └── verification/ — verify-before-done + structured verdicts
424
+ │ └── verification/ — verify-before-done + structured QA verdicts
395
425
  └── agents/_defaults/ — 11 bundled agent definitions
396
426
  ```
397
427
 
398
428
  ## Test Results
399
429
 
400
430
  ### Simple (3 tasks, linear chain)
401
- - URL shortener API: 3 tasks, $0.83, 4.5 minutes
431
+ - URL shortener API: 3 tasks, 30 turns, $0.83, 4.5 minutes
402
432
  - Backend → QA → Docs, all passing
403
433
 
404
- ### Medium (6 tasks, diamond dependency)
405
- - Static blog engine: 6 tasks, $4.56, 24 minutes
406
- - Markdown parser HTML converter site generator Express server → QA + Docs
407
- - 87+ tests passing
434
+ ### Medium (5 tasks, planner-decomposed)
435
+ - Quiz API: 5 tasks, 82 turns, $3.11, 14 minutes
436
+ - Planner auto-created: architectbackendfullstack → QA + docs
437
+ - 111 vitest tests passing, 3,637 lines of code
408
438
 
409
- ### Complex (5 original + 4 auto-rework = 9 tasks)
410
- - Chat API with auth + rooms: 9 tasks, $4.60, 26 minutes
439
+ ### Complex with rework (5 original + 4 auto-rework = 9 tasks)
440
+ - Chat API with auth + rooms: 9 tasks, 176 turns, $4.60, 26 minutes
411
441
  - QA found 2 bugs → 2 automatic rework cycles → both fixed and verified
412
- - 176 turns across 5 specialist agents
442
+ - Dependency chain with parallel QA and rework loop
443
+
444
+ ### Parallel squads (2 squads running simultaneously)
445
+ - Bookmarks API (2/2, $0.83) + Notes CLI (6/6 with 2 rework cycles, $1.17)
446
+ - Both ran concurrently from the same pi session
447
+ - Combined: 12 tasks, 101 turns, $2.00
413
448
 
414
449
  ## Requirements
415
450
 
416
- - [pi](https://github.com/badlogic/pi-mono) coding agent
451
+ - [pi](https://github.com/badlogic/pi-mono) coding agent (v0.63.0+, recommended v0.64.0+)
417
452
  - An API key configured in pi (Anthropic, OpenRouter, etc.)
418
453
  - Node.js 18+
419
454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.4.14",
3
+ "version": "0.5.0",
4
4
  "description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
5
5
  "type": "module",
6
6
  "pi": {
package/src/agent-pool.ts CHANGED
@@ -165,6 +165,7 @@ export class AgentPool {
165
165
 
166
166
  // Spawn pi process — set env var to prevent recursive squad extension loading
167
167
  const invocation = getPiInvocation(["--mode", "rpc", ...args]);
168
+ console.error(`[squad-pool] spawn ${agentDef.name}: ${invocation.command} ${invocation.args.slice(0, 3).join(" ")} ...`);
168
169
  const proc = spawn(invocation.command, invocation.args, {
169
170
  cwd,
170
171
  stdio: ["pipe", "pipe", "pipe"],
@@ -199,6 +200,7 @@ export class AgentPool {
199
200
  });
200
201
 
201
202
  attachLineReader(proc.stdout!, (line) => {
203
+ stdoutLines++;
202
204
  try {
203
205
  const event = JSON.parse(line);
204
206
  this.handleRpcEvent(agentProc, event);
@@ -208,7 +210,14 @@ export class AgentPool {
208
210
  });
209
211
 
210
212
  let agentEndEmitted = false;
211
- proc.on("exit", (code) => {
213
+ let stdoutLines = 0;
214
+ proc.on("exit", (code, signal) => {
215
+ // Log diagnostic info for debugging spawn failures
216
+ if (code !== 0 && code !== null) {
217
+ console.error(`[squad-pool] ${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr.slice(0, 300) || "(empty)"}`);
218
+ }
219
+ // Clean up agents map so getRunningAgents() doesn't count dead processes
220
+ this.agents.delete(taskId);
212
221
  // Only emit if we haven't already emitted via RPC agent_end event
213
222
  if (!agentEndEmitted) {
214
223
  agentEndEmitted = true;
package/src/index.ts CHANGED
@@ -166,15 +166,17 @@ export default function (pi: ExtensionAPI) {
166
166
  ),
167
167
  }),
168
168
 
169
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
169
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
170
170
  if (!squadEnabled) return { content: [{ type: "text" as const, text: "Squad is disabled. Use /squad enable to re-enable." }] };
171
171
  if (!uiCtx) uiCtx = ctx;
172
172
 
173
+ // Check if the user cancelled before we start
174
+ if (signal?.aborted) return { content: [{ type: "text" as const, text: "Cancelled." }] };
175
+
173
176
  // Multiple squads can run concurrently — no guard needed
174
177
 
175
178
  const squadId = store.makeTaskId(params.goal);
176
179
  if (store.squadExists(squadId)) {
177
- // Append timestamp to make unique
178
180
  const uniqueId = `${squadId}-${Date.now().toString(36)}`;
179
181
  return await startSquad(uniqueId, params, ctx.cwd, squadSkillPaths, pi);
180
182
  }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Theme } from "@mariozechner/pi-coding-agent";
8
- import { truncateToWidth } from "@mariozechner/pi-tui";
8
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
9
  import type { TaskMessage } from "../types.js";
10
10
  import * as store from "../store.js";
11
11
 
@@ -159,11 +159,12 @@ export class MessageView {
159
159
  case "text":
160
160
  case "message":
161
161
  case "reply": {
162
- // Split text, strip any \r, ensure no embedded newlines survive
163
162
  const textLines = msg.text.replace(/\r/g, "").split("\n");
164
163
  const show = textLines.slice(0, MAX_TEXT_LINES);
165
164
  for (const tl of show) {
166
- lines.push(fit(` ${tl}`, width));
165
+ // Wrap long lines instead of truncating
166
+ const wrapped = wrap(` ${tl}`, width, " ");
167
+ lines.push(...wrapped);
167
168
  }
168
169
  if (textLines.length > MAX_TEXT_LINES) {
169
170
  lines.push(fit(` ${th.fg("dim", `... +${textLines.length - MAX_TEXT_LINES} lines`)}`, width));
@@ -171,10 +172,10 @@ export class MessageView {
171
172
  break;
172
173
  }
173
174
  case "done":
174
- lines.push(fit(` ${th.fg("success", "✓ " + msg.text)}`, width));
175
+ lines.push(...wrap(` ${msg.text}`, width, " "));
175
176
  break;
176
177
  case "error":
177
- lines.push(fit(` ${th.fg("error", "✗ " + msg.text)}`, width));
178
+ lines.push(...wrap(` ${msg.text}`, width, " "));
178
179
  break;
179
180
  case "status":
180
181
  lines.push(fit(` ${th.fg("dim", msg.text)}`, width));
@@ -186,12 +187,54 @@ export class MessageView {
186
187
  }
187
188
  }
188
189
 
190
+ /** Truncate a single line (for headers, tool calls, status — not text content) */
189
191
  function fit(line: string, width: number): string {
190
- // Strip any newlines that would create extra terminal lines and break layout math
191
192
  const clean = line.replace(/[\n\r]/g, " ");
192
193
  return truncateToWidth(clean, width, "…");
193
194
  }
194
195
 
196
+ /** Wrap a text line into multiple lines that fit within width.
197
+ * Uses ANSI-aware visibleWidth for correct wrapping with styled text.
198
+ * Returns array of lines, each guaranteed to fit within width. */
199
+ function wrap(line: string, width: number, indent: string = " "): string[] {
200
+ const clean = line.replace(/[\n\r]/g, " ");
201
+ // Fast path: already fits
202
+ if (visibleWidth(clean) <= width) return [clean];
203
+
204
+ // For styled text, we can't word-wrap by chars (ANSI codes break).
205
+ // Instead, strip to plain text, wrap that, then truncate styled lines.
206
+ const plain = stripAnsi(clean);
207
+ const indentW = visibleWidth(indent);
208
+ const firstW = width;
209
+ const contW = width - indentW;
210
+
211
+ const results: string[] = [];
212
+ let remaining = plain;
213
+ let isFirst = true;
214
+
215
+ while (remaining.length > 0) {
216
+ const maxW = isFirst ? firstW : contW;
217
+ if (remaining.length <= maxW) {
218
+ results.push(isFirst ? remaining : indent + remaining);
219
+ break;
220
+ }
221
+ // Find word break point
222
+ let breakAt = remaining.lastIndexOf(" ", maxW);
223
+ if (breakAt <= maxW * 0.3) breakAt = maxW; // No good break, hard cut
224
+ const chunk = remaining.slice(0, breakAt);
225
+ results.push(isFirst ? chunk : indent + chunk);
226
+ remaining = remaining.slice(breakAt).trimStart();
227
+ isFirst = false;
228
+ }
229
+
230
+ // Truncate each to be safe (handles edge cases)
231
+ return results.map(r => truncateToWidth(r, width, ""));
232
+ }
233
+
234
+ function stripAnsi(str: string): string {
235
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
236
+ }
237
+
195
238
  function pad(lines: string[], max: number): string[] {
196
239
  while (lines.length < max) lines.push("");
197
240
  return lines.slice(0, max);
package/src/scheduler.ts CHANGED
@@ -55,6 +55,8 @@ export class Scheduler {
55
55
  private listeners: SchedulerEventListener[] = [];
56
56
  private skillPaths: string[] = [];
57
57
  private running = false;
58
+ /** Track spawn retries to allow one retry per task */
59
+ private spawnRetries = new Set<string>();
58
60
 
59
61
  /** Get the project cwd for this squad (from squad.json) */
60
62
  getProjectCwd(): string | undefined {
@@ -211,6 +213,9 @@ export class Scheduler {
211
213
  await this.spawnAgentForTask(task, squad);
212
214
  } catch (error) {
213
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}`);
214
219
  }
215
220
  }
216
221
 
@@ -381,8 +386,29 @@ export class Scheduler {
381
386
  if (exitCode === 0 || hadMeaningfulWork) {
382
387
  this.handleTaskCompleted(event.taskId).then(() => this.updateContext());
383
388
  } else {
384
- const stderr = event.data?.stderr || "";
385
- 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
+ }
386
412
  this.updateContext();
387
413
  }
388
414
  // Skip the updateContext() below — handled in the branches above