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 +72 -37
- package/package.json +1 -1
- package/src/agent-pool.ts +10 -1
- package/src/index.ts +4 -2
- package/src/panel/message-view.ts +49 -6
- package/src/scheduler.ts +28 -2
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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 —
|
|
388
|
-
│ ├── task-list.ts — task tree with status icons, elapsed time
|
|
389
|
-
│ └── message-view.ts — scrollable message log
|
|
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 (
|
|
405
|
-
-
|
|
406
|
-
-
|
|
407
|
-
-
|
|
434
|
+
### Medium (5 tasks, planner-decomposed)
|
|
435
|
+
- Quiz API: 5 tasks, 82 turns, $3.11, 14 minutes
|
|
436
|
+
- Planner auto-created: architect → backend → fullstack → 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
|
-
-
|
|
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
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
|
-
|
|
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,
|
|
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
|
|
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(
|
|
175
|
+
lines.push(...wrap(` ✓ ${msg.text}`, width, " "));
|
|
175
176
|
break;
|
|
176
177
|
case "error":
|
|
177
|
-
lines.push(
|
|
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
|
-
|
|
385
|
-
|
|
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
|