pi-squad 0.4.14 → 0.6.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 +12 -2
- package/src/index.ts +148 -21
- package/src/logger.ts +68 -0
- package/src/panel/message-view.ts +49 -6
- package/src/panel/squad-widget.ts +81 -24
- package/src/planner.ts +15 -0
- package/src/protocol.ts +18 -1
- package/src/scheduler.ts +128 -14
- package/src/store.ts +26 -0
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
|
@@ -11,6 +11,7 @@ import * as os from "node:os";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import type { AgentDef, AgentActivity, Task, TaskMessage } from "./types.js";
|
|
13
13
|
import { buildAgentSystemPrompt, type ProtocolBuildOptions } from "./protocol.js";
|
|
14
|
+
import { debug, logError } from "./logger.js";
|
|
14
15
|
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// Types
|
|
@@ -165,6 +166,7 @@ export class AgentPool {
|
|
|
165
166
|
|
|
166
167
|
// Spawn pi process — set env var to prevent recursive squad extension loading
|
|
167
168
|
const invocation = getPiInvocation(["--mode", "rpc", ...args]);
|
|
169
|
+
debug("squad-pool", `spawn ${agentDef.name}: ${invocation.command} ${invocation.args.slice(0, 3).join(" ")} ...`);
|
|
168
170
|
const proc = spawn(invocation.command, invocation.args, {
|
|
169
171
|
cwd,
|
|
170
172
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -199,6 +201,7 @@ export class AgentPool {
|
|
|
199
201
|
});
|
|
200
202
|
|
|
201
203
|
attachLineReader(proc.stdout!, (line) => {
|
|
204
|
+
stdoutLines++;
|
|
202
205
|
try {
|
|
203
206
|
const event = JSON.parse(line);
|
|
204
207
|
this.handleRpcEvent(agentProc, event);
|
|
@@ -208,7 +211,14 @@ export class AgentPool {
|
|
|
208
211
|
});
|
|
209
212
|
|
|
210
213
|
let agentEndEmitted = false;
|
|
211
|
-
|
|
214
|
+
let stdoutLines = 0;
|
|
215
|
+
proc.on("exit", (code, signal) => {
|
|
216
|
+
// Log diagnostic info for debugging spawn failures
|
|
217
|
+
if (code !== 0 && code !== null) {
|
|
218
|
+
logError("squad-pool", `${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr.slice(0, 300) || "(empty)"}`);
|
|
219
|
+
}
|
|
220
|
+
// Clean up agents map so getRunningAgents() doesn't count dead processes
|
|
221
|
+
this.agents.delete(taskId);
|
|
212
222
|
// Only emit if we haven't already emitted via RPC agent_end event
|
|
213
223
|
if (!agentEndEmitted) {
|
|
214
224
|
agentEndEmitted = true;
|
|
@@ -368,7 +378,7 @@ export class AgentPool {
|
|
|
368
378
|
// Pi RPC mode emits agent_end when the agent loop finishes.
|
|
369
379
|
// The RPC process stays alive waiting for more commands,
|
|
370
380
|
// so we need to explicitly kill it and emit our own agent_end.
|
|
371
|
-
|
|
381
|
+
debug("squad-pool", `agent_end from RPC: ${agent.agentName} (task: ${agent.taskId})`);
|
|
372
382
|
// Mark the guard to prevent double-emit from proc.on("exit")
|
|
373
383
|
const guardFn = (agent as any)._agentEndEmitted;
|
|
374
384
|
if (guardFn) guardFn();
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { runPlanner } from "./planner.js";
|
|
|
21
21
|
import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
|
|
22
22
|
import { setupSquadWidget, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
23
23
|
import * as store from "./store.js";
|
|
24
|
+
import { debug, logError } from "./logger.js";
|
|
24
25
|
|
|
25
26
|
// ============================================================================
|
|
26
27
|
// State
|
|
@@ -166,15 +167,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
166
167
|
),
|
|
167
168
|
}),
|
|
168
169
|
|
|
169
|
-
async execute(_toolCallId, params,
|
|
170
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
170
171
|
if (!squadEnabled) return { content: [{ type: "text" as const, text: "Squad is disabled. Use /squad enable to re-enable." }] };
|
|
171
172
|
if (!uiCtx) uiCtx = ctx;
|
|
172
173
|
|
|
174
|
+
// Check if the user cancelled before we start
|
|
175
|
+
if (signal?.aborted) return { content: [{ type: "text" as const, text: "Cancelled." }] };
|
|
176
|
+
|
|
173
177
|
// Multiple squads can run concurrently — no guard needed
|
|
174
178
|
|
|
175
179
|
const squadId = store.makeTaskId(params.goal);
|
|
176
180
|
if (store.squadExists(squadId)) {
|
|
177
|
-
// Append timestamp to make unique
|
|
178
181
|
const uniqueId = `${squadId}-${Date.now().toString(36)}`;
|
|
179
182
|
return await startSquad(uniqueId, params, ctx.cwd, squadSkillPaths, pi);
|
|
180
183
|
}
|
|
@@ -346,15 +349,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
346
349
|
switch (event.type) {
|
|
347
350
|
case "squad_completed": {
|
|
348
351
|
const tasks = store.loadAllTasks(squadId);
|
|
349
|
-
const summary = tasks
|
|
350
|
-
.filter((t) => t.status === "done")
|
|
351
|
-
.map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
|
|
352
|
-
.join("\n");
|
|
353
352
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
354
353
|
const s = schedulers.get(squadId); if (s) s.updateContext();
|
|
354
|
+
const overview = store.loadOverview(squadId);
|
|
355
355
|
pi.sendMessage({
|
|
356
356
|
customType: "squad-completed",
|
|
357
|
-
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\
|
|
357
|
+
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
|
|
358
|
+
(overview ? `## Squad Overview\n\n${overview}\n\n` : "") +
|
|
359
|
+
`Total cost: $${totalCost.toFixed(4)}`,
|
|
358
360
|
display: true,
|
|
359
361
|
});
|
|
360
362
|
schedulers.delete(squadId);
|
|
@@ -365,9 +367,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
365
367
|
const tasks = store.loadAllTasks(squadId);
|
|
366
368
|
const failed = tasks.filter((t) => t.status === "failed");
|
|
367
369
|
const done = tasks.filter((t) => t.status === "done");
|
|
370
|
+
const overview = store.loadOverview(squadId);
|
|
368
371
|
pi.sendMessage({
|
|
369
372
|
customType: "squad-failed",
|
|
370
|
-
content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\
|
|
373
|
+
content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\n` +
|
|
374
|
+
`Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}` +
|
|
375
|
+
(overview ? `\n\n## Squad Overview\n\n${overview}` : ""),
|
|
371
376
|
display: true,
|
|
372
377
|
}, { triggerTurn: true });
|
|
373
378
|
forceWidgetUpdate();
|
|
@@ -387,7 +392,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
387
392
|
|
|
388
393
|
const resumeSched = schedulers.get(squadId)!;
|
|
389
394
|
resumeSched.resume().catch((err) => {
|
|
390
|
-
|
|
395
|
+
logError("squad", `Resume error: ${(err as Error).message}`);
|
|
391
396
|
});
|
|
392
397
|
|
|
393
398
|
const tasks = store.loadAllTasks(squadId);
|
|
@@ -438,7 +443,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
438
443
|
case "resume_task": {
|
|
439
444
|
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
|
|
440
445
|
activeScheduler.resumeTask(params.taskId).catch((err) => {
|
|
441
|
-
|
|
446
|
+
logError("squad", `Resume task error: ${(err as Error).message}`);
|
|
442
447
|
});
|
|
443
448
|
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }] };
|
|
444
449
|
}
|
|
@@ -1211,6 +1216,31 @@ async function startSquad(
|
|
|
1211
1216
|
|
|
1212
1217
|
store.saveSquad(squad);
|
|
1213
1218
|
|
|
1219
|
+
// Create initial OVERVIEW.md with squad goal, task plan, and design contract
|
|
1220
|
+
const planLines = [
|
|
1221
|
+
`# Squad: ${params.goal}`,
|
|
1222
|
+
``,
|
|
1223
|
+
`**Created:** ${squad.created}`,
|
|
1224
|
+
`**Tasks:** ${plan.tasks.length}`,
|
|
1225
|
+
``,
|
|
1226
|
+
`## Task Plan`,
|
|
1227
|
+
``,
|
|
1228
|
+
...plan.tasks.map((t) => {
|
|
1229
|
+
const deps = t.depends.length > 0 ? ` (after: ${t.depends.join(", ")})` : "";
|
|
1230
|
+
return `- **${t.id}** → ${t.agent}: ${t.title}${deps}`;
|
|
1231
|
+
}),
|
|
1232
|
+
``,
|
|
1233
|
+
];
|
|
1234
|
+
|
|
1235
|
+
// Extract design contract from task descriptions — API paths, schemas,
|
|
1236
|
+
// ports, file conventions — so parallel agents share a single source of truth.
|
|
1237
|
+
const contractLines = buildDesignContract(plan.tasks, params.goal);
|
|
1238
|
+
if (contractLines.length > 0) {
|
|
1239
|
+
planLines.push(...contractLines);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
store.appendOverview(squadId, planLines.join("\n"));
|
|
1243
|
+
|
|
1214
1244
|
// Create task files
|
|
1215
1245
|
for (const taskDef of plan.tasks) {
|
|
1216
1246
|
const task: Task = {
|
|
@@ -1256,12 +1286,8 @@ async function startSquad(
|
|
|
1256
1286
|
// Update widget on every scheduler event
|
|
1257
1287
|
forceWidgetUpdate();
|
|
1258
1288
|
switch (event.type) {
|
|
1259
|
-
|
|
1289
|
+
case "squad_completed": {
|
|
1260
1290
|
const tasks = store.loadAllTasks(squadId);
|
|
1261
|
-
const summary = tasks
|
|
1262
|
-
.filter((t) => t.status === "done")
|
|
1263
|
-
.map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
|
|
1264
|
-
.join("\n");
|
|
1265
1291
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
1266
1292
|
|
|
1267
1293
|
// Final context update before clearing scheduler
|
|
@@ -1270,12 +1296,17 @@ async function startSquad(
|
|
|
1270
1296
|
completedSched.updateContext();
|
|
1271
1297
|
}
|
|
1272
1298
|
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1299
|
+
// Load the full overview document — contains the narrative of
|
|
1300
|
+
// each task's output, decisions, issues, and files modified.
|
|
1301
|
+
const overview = store.loadOverview(squadId);
|
|
1302
|
+
|
|
1303
|
+
// Send into LLM context (not display-only) so the main agent
|
|
1304
|
+
// knows exactly what happened. No triggerTurn — user decides
|
|
1305
|
+
// what to do next.
|
|
1275
1306
|
pi.sendMessage({
|
|
1276
1307
|
customType: "squad-completed",
|
|
1277
1308
|
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
|
|
1278
|
-
|
|
1309
|
+
(overview ? `## Squad Overview\n\n${overview}\n\n` : "") +
|
|
1279
1310
|
`Total cost: $${totalCost.toFixed(4)}`,
|
|
1280
1311
|
display: true,
|
|
1281
1312
|
});
|
|
@@ -1290,14 +1321,14 @@ async function startSquad(
|
|
|
1290
1321
|
const tasks = store.loadAllTasks(squadId);
|
|
1291
1322
|
const failed = tasks.filter((t) => t.status === "failed");
|
|
1292
1323
|
const done = tasks.filter((t) => t.status === "done");
|
|
1324
|
+
const overview = store.loadOverview(squadId);
|
|
1293
1325
|
|
|
1294
|
-
// Squad failed — notify user. Use triggerTurn so the agent can
|
|
1295
|
-
// suggest next steps (retry, modify, cancel).
|
|
1296
1326
|
pi.sendMessage({
|
|
1297
1327
|
customType: "squad-failed",
|
|
1298
1328
|
content: `[squad] Squad "${squadId}" has stalled. ` +
|
|
1299
1329
|
`${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
|
|
1300
1330
|
`Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}\n` +
|
|
1331
|
+
(overview ? `\n## Squad Overview\n\n${overview}\n\n` : "\n") +
|
|
1301
1332
|
`Use squad_status for details or squad_modify to adjust.`,
|
|
1302
1333
|
display: true,
|
|
1303
1334
|
}, { triggerTurn: true });
|
|
@@ -1325,7 +1356,7 @@ async function startSquad(
|
|
|
1325
1356
|
// We must return immediately so the main agent's turn completes
|
|
1326
1357
|
// and the user regains interactive control.
|
|
1327
1358
|
scheduler.start().catch((err) => {
|
|
1328
|
-
|
|
1359
|
+
logError("squad", `Scheduler start error: ${(err as Error).message}`);
|
|
1329
1360
|
});
|
|
1330
1361
|
|
|
1331
1362
|
// Build response
|
|
@@ -1358,3 +1389,99 @@ function getSquadSkillPaths(skillsDir: string): string[] {
|
|
|
1358
1389
|
.map((d) => path.join(skillsDir, d.name))
|
|
1359
1390
|
.filter((dir) => fs.existsSync(path.join(dir, "SKILL.md")));
|
|
1360
1391
|
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Extract a design contract from task descriptions.
|
|
1395
|
+
*
|
|
1396
|
+
* Scans all tasks for API paths, ports, file names, data schemas, and
|
|
1397
|
+
* shared conventions. Produces a "## Design Contract" section in the
|
|
1398
|
+
* OVERVIEW.md so that ALL agents (including parallel ones) share a
|
|
1399
|
+
* single source of truth before they start working.
|
|
1400
|
+
*/
|
|
1401
|
+
function buildDesignContract(
|
|
1402
|
+
tasks: Array<{ id: string; title: string; description: string; agent: string; depends: string[] }>,
|
|
1403
|
+
goal: string,
|
|
1404
|
+
): string[] {
|
|
1405
|
+
const lines: string[] = [];
|
|
1406
|
+
|
|
1407
|
+
// Collect API routes mentioned in any task description
|
|
1408
|
+
const apiRoutes: string[] = [];
|
|
1409
|
+
const ports: string[] = [];
|
|
1410
|
+
const files: string[] = [];
|
|
1411
|
+
const schemas: string[] = [];
|
|
1412
|
+
|
|
1413
|
+
const allText = tasks.map((t) => `${t.title}\n${t.description}`).join("\n") + "\n" + goal;
|
|
1414
|
+
|
|
1415
|
+
// Extract API paths like GET /foo, POST /bar/:id
|
|
1416
|
+
const routePattern = /\b(GET|POST|PUT|PATCH|DELETE|HEAD)\s+(\/[\w\/:]+)/gi;
|
|
1417
|
+
for (const match of allText.matchAll(routePattern)) {
|
|
1418
|
+
const route = `${match[1].toUpperCase()} ${match[2]}`;
|
|
1419
|
+
if (!apiRoutes.includes(route)) apiRoutes.push(route);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Extract port numbers
|
|
1423
|
+
const portPattern = /\b(?:port|PORT|Port)\s*[=:]?\s*(\d{4,5})\b/gi;
|
|
1424
|
+
for (const match of allText.matchAll(portPattern)) {
|
|
1425
|
+
if (!ports.includes(match[1])) ports.push(match[1]);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Extract key file names mentioned (e.g., server.js, index.html)
|
|
1429
|
+
const filePattern = /\b([\w-]+\.(js|ts|json|html|css|sh|mjs|cjs))\b/gi;
|
|
1430
|
+
for (const match of allText.matchAll(filePattern)) {
|
|
1431
|
+
const f = match[1];
|
|
1432
|
+
if (!files.includes(f) && !['package.json'].includes(f)) files.push(f);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Extract data schemas like {field, field, field}
|
|
1436
|
+
const schemaPattern = /\{([a-zA-Z_][\w,\s\[\]?]+)\}/g;
|
|
1437
|
+
for (const match of allText.matchAll(schemaPattern)) {
|
|
1438
|
+
const fields = match[1].trim();
|
|
1439
|
+
if (fields.includes(',') && fields.length > 5 && fields.length < 200) {
|
|
1440
|
+
if (!schemas.includes(fields)) schemas.push(fields);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Only emit a contract section if we found shared design elements
|
|
1445
|
+
if (apiRoutes.length === 0 && ports.length === 0 && schemas.length === 0) {
|
|
1446
|
+
return [];
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
lines.push(`## Design Contract`);
|
|
1450
|
+
lines.push(``);
|
|
1451
|
+
lines.push(`> **All agents MUST follow these specifications.** Do not invent`);
|
|
1452
|
+
lines.push(`> alternative paths, ports, or schemas. If you need to deviate,`);
|
|
1453
|
+
lines.push(`> document the reason in your output.`);
|
|
1454
|
+
lines.push(``);
|
|
1455
|
+
|
|
1456
|
+
if (ports.length > 0) {
|
|
1457
|
+
lines.push(`### Server`);
|
|
1458
|
+
lines.push(`- Port: **${ports.join(", ")}**`);
|
|
1459
|
+
lines.push(``);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (apiRoutes.length > 0) {
|
|
1463
|
+
lines.push(`### API Endpoints`);
|
|
1464
|
+
for (const r of apiRoutes) {
|
|
1465
|
+
lines.push(`- \`${r}\``);
|
|
1466
|
+
}
|
|
1467
|
+
lines.push(``);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (schemas.length > 0) {
|
|
1471
|
+
lines.push(`### Data Schemas`);
|
|
1472
|
+
for (const s of schemas) {
|
|
1473
|
+
lines.push(`- \`{ ${s} }\``);
|
|
1474
|
+
}
|
|
1475
|
+
lines.push(``);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (files.length > 0) {
|
|
1479
|
+
lines.push(`### Key Files`);
|
|
1480
|
+
for (const f of files) {
|
|
1481
|
+
lines.push(`- \`${f}\``);
|
|
1482
|
+
}
|
|
1483
|
+
lines.push(``);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
return lines;
|
|
1487
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.ts — Squad debug logging to file, NOT stderr.
|
|
3
|
+
*
|
|
4
|
+
* Writing to stderr (console.error) corrupts the TUI because it bypasses
|
|
5
|
+
* the TUI's differential renderer. All squad debug output goes to a log
|
|
6
|
+
* file instead: ~/.pi/squad/debug.log
|
|
7
|
+
*
|
|
8
|
+
* Set PI_SQUAD_DEBUG=1 to enable logging. Without it, logs are silent.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
|
|
15
|
+
const LOG_DIR = path.join(os.homedir(), ".pi", "squad");
|
|
16
|
+
const LOG_FILE = path.join(LOG_DIR, "debug.log");
|
|
17
|
+
const DEBUG = process.env.PI_SQUAD_DEBUG === "1";
|
|
18
|
+
|
|
19
|
+
/** Max log file size before rotation (2MB) */
|
|
20
|
+
const MAX_LOG_SIZE = 2 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
let rotationChecked = false;
|
|
23
|
+
|
|
24
|
+
function ensureLogDir(): void {
|
|
25
|
+
try {
|
|
26
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
27
|
+
} catch { /* ignore */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rotateIfNeeded(): void {
|
|
31
|
+
if (rotationChecked) return;
|
|
32
|
+
rotationChecked = true;
|
|
33
|
+
try {
|
|
34
|
+
const stat = fs.statSync(LOG_FILE);
|
|
35
|
+
if (stat.size > MAX_LOG_SIZE) {
|
|
36
|
+
const backup = LOG_FILE + ".old";
|
|
37
|
+
try { fs.unlinkSync(backup); } catch { /* ignore */ }
|
|
38
|
+
fs.renameSync(LOG_FILE, backup);
|
|
39
|
+
}
|
|
40
|
+
} catch { /* file doesn't exist yet, fine */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Log a debug message to ~/.pi/squad/debug.log.
|
|
45
|
+
* Only writes when PI_SQUAD_DEBUG=1 is set. Silent otherwise.
|
|
46
|
+
*/
|
|
47
|
+
export function debug(prefix: string, ...args: unknown[]): void {
|
|
48
|
+
if (!DEBUG) return;
|
|
49
|
+
write(prefix, ...args);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Always log to ~/.pi/squad/debug.log regardless of PI_SQUAD_DEBUG.
|
|
54
|
+
* Use for errors and critical events that should never be lost.
|
|
55
|
+
*/
|
|
56
|
+
export function logError(prefix: string, ...args: unknown[]): void {
|
|
57
|
+
write(prefix, ...args);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function write(prefix: string, ...args: unknown[]): void {
|
|
61
|
+
try {
|
|
62
|
+
ensureLogDir();
|
|
63
|
+
rotateIfNeeded();
|
|
64
|
+
const ts = new Date().toISOString();
|
|
65
|
+
const msg = args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
66
|
+
fs.appendFileSync(LOG_FILE, `[${ts}] [${prefix}] ${msg}\n`);
|
|
67
|
+
} catch { /* never throw from logging */ }
|
|
68
|
+
}
|
|
@@ -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);
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* squad-widget.ts —
|
|
2
|
+
* squad-widget.ts — Compact widget for squad status above the editor.
|
|
3
3
|
*
|
|
4
|
-
* Uses
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Uses a component factory for setWidget so it can access terminal width
|
|
5
|
+
* and truncate every line to prevent Text wrapping. Without truncation,
|
|
6
|
+
* long lines wrap inside the Text component, causing unpredictable
|
|
7
|
+
* widget height changes that corrupt the TUI diff renderer.
|
|
7
8
|
*
|
|
8
9
|
* Updates are pushed by calling requestUpdate() which rebuilds the
|
|
9
|
-
*
|
|
10
|
+
* lines and calls setWidget() again.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
13
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
14
|
+
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
13
15
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
14
16
|
import type { TaskStatus } from "../types.js";
|
|
15
17
|
import * as store from "../store.js";
|
|
@@ -41,7 +43,10 @@ export interface SquadWidgetState {
|
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Set up the squad widget. Returns control functions.
|
|
44
|
-
*
|
|
46
|
+
*
|
|
47
|
+
* Uses a component factory so we can access TUI.terminal.columns and
|
|
48
|
+
* truncate every line, preventing Text word-wrapping that would cause
|
|
49
|
+
* unpredictable widget height changes.
|
|
45
50
|
*/
|
|
46
51
|
export function setupSquadWidget(
|
|
47
52
|
ctx: { ui: { setWidget: Function; setStatus: Function; [key: string]: any }; hasUI?: boolean },
|
|
@@ -56,22 +61,16 @@ export function setupSquadWidget(
|
|
|
56
61
|
let renderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
57
62
|
/** Cache key to skip redundant setWidget calls */
|
|
58
63
|
let lastCacheKey = "";
|
|
64
|
+
/** Last built lines — the factory re-uses these on each TUI render */
|
|
65
|
+
let currentLines: string[] = [];
|
|
59
66
|
|
|
60
|
-
function
|
|
61
|
-
if (!state.enabled || !state.squadId)
|
|
62
|
-
ctx.ui.setWidget("squad-tasks", undefined);
|
|
63
|
-
ctx.ui.setStatus("squad", undefined);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
67
|
+
function buildLines(): { lines: string[]; cacheKey: string; statusText: string } | null {
|
|
68
|
+
if (!state.enabled || !state.squadId) return null;
|
|
66
69
|
|
|
67
70
|
const th = ctx.ui.theme;
|
|
68
71
|
const tasks = store.loadAllTasks(state.squadId);
|
|
69
72
|
const squad = store.loadSquad(state.squadId);
|
|
70
|
-
if (!squad || tasks.length === 0)
|
|
71
|
-
ctx.ui.setWidget("squad-tasks", undefined);
|
|
72
|
-
ctx.ui.setStatus("squad", undefined);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
73
|
+
if (!squad || tasks.length === 0) return null;
|
|
75
74
|
|
|
76
75
|
const lines: string[] = [];
|
|
77
76
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
@@ -136,19 +135,77 @@ export function setupSquadWidget(
|
|
|
136
135
|
lines.push(` ${th.fg("dim", ` +${tasks.length - maxVisible} more · ^q detail`)}`);
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
// Skip if nothing changed (avoid redundant setWidget calls that cause flicker)
|
|
140
138
|
const cacheKey = `${squad.status}:${tasks.map(t => `${t.id}=${t.status}:${t.usage.turns}`).join(",")}`;
|
|
141
|
-
if (cacheKey === lastCacheKey) return;
|
|
142
|
-
lastCacheKey = cacheKey;
|
|
143
139
|
|
|
144
|
-
ctx.ui.setWidget("squad-tasks", lines);
|
|
145
|
-
|
|
146
|
-
// Update status bar
|
|
147
140
|
const statusText = squad.status === "done"
|
|
148
141
|
? th.fg("success", `✓ squad ${doneCount}/${tasks.length}`)
|
|
149
142
|
: squad.status === "failed"
|
|
150
143
|
? th.fg("error", `✗ squad ${doneCount}/${tasks.length}`)
|
|
151
144
|
: th.fg("accent", `⏳ squad ${doneCount}/${tasks.length} $${totalCost.toFixed(2)}`);
|
|
145
|
+
|
|
146
|
+
return { lines, cacheKey, statusText };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Widget component that renders pre-built lines truncated to terminal width.
|
|
151
|
+
* Each line is guaranteed to fit on exactly one terminal row — no wrapping.
|
|
152
|
+
* This keeps the widget height deterministic (= lines.length) so the TUI
|
|
153
|
+
* diff renderer never sees unexpected height changes.
|
|
154
|
+
*/
|
|
155
|
+
class SquadWidgetComponent implements Component {
|
|
156
|
+
lines: string[] = [];
|
|
157
|
+
|
|
158
|
+
invalidate(): void { /* stateless — lines are rebuilt externally */ }
|
|
159
|
+
|
|
160
|
+
render(width: number): string[] {
|
|
161
|
+
// Truncate every line to terminal width so Text wrapping cannot
|
|
162
|
+
// add extra rows. One input line → exactly one output line.
|
|
163
|
+
return this.lines.map(line => {
|
|
164
|
+
const truncated = truncateToWidth(line, width);
|
|
165
|
+
// Pad to full width to prevent leftover characters from previous renders
|
|
166
|
+
const vw = visibleWidth(truncated);
|
|
167
|
+
const pad = Math.max(0, width - vw);
|
|
168
|
+
return truncated + " ".repeat(pad);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Persistent widget component instance — survives across setWidget calls */
|
|
174
|
+
let widgetComponent: SquadWidgetComponent | null = null;
|
|
175
|
+
let widgetInstalled = false;
|
|
176
|
+
|
|
177
|
+
function render(): void {
|
|
178
|
+
const result = buildLines();
|
|
179
|
+
if (!result) {
|
|
180
|
+
ctx.ui.setWidget("squad-tasks", undefined);
|
|
181
|
+
ctx.ui.setStatus("squad", undefined);
|
|
182
|
+
widgetInstalled = false;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { lines, cacheKey, statusText } = result;
|
|
187
|
+
|
|
188
|
+
// Skip if nothing changed (avoid redundant setWidget calls)
|
|
189
|
+
if (cacheKey === lastCacheKey) return;
|
|
190
|
+
lastCacheKey = cacheKey;
|
|
191
|
+
|
|
192
|
+
currentLines = lines;
|
|
193
|
+
|
|
194
|
+
if (!widgetInstalled) {
|
|
195
|
+
// Install the component factory once. On subsequent updates we just
|
|
196
|
+
// mutate widgetComponent.lines and requestRender — no setWidget churn.
|
|
197
|
+
widgetComponent = new SquadWidgetComponent();
|
|
198
|
+
widgetComponent.lines = lines;
|
|
199
|
+
|
|
200
|
+
const comp = widgetComponent;
|
|
201
|
+
ctx.ui.setWidget("squad-tasks", (_tui: TUI, _theme: Theme) => comp);
|
|
202
|
+
widgetInstalled = true;
|
|
203
|
+
} else if (widgetComponent) {
|
|
204
|
+
// Update lines in-place — the next TUI render picks them up.
|
|
205
|
+
// No setWidget call needed, avoiding component tree rebuild.
|
|
206
|
+
widgetComponent.lines = lines;
|
|
207
|
+
}
|
|
208
|
+
|
|
152
209
|
ctx.ui.setStatus("squad", statusText);
|
|
153
210
|
}
|
|
154
211
|
|
package/src/planner.ts
CHANGED
|
@@ -33,6 +33,14 @@ export async function runPlanner(options: PlannerOptions): Promise<PlannerOutput
|
|
|
33
33
|
const plannerDef = loadAgentDef("planner", cwd);
|
|
34
34
|
const allAgents = loadAllAgentDefs(cwd).filter((a) => a.name !== "planner" && !a.disabled);
|
|
35
35
|
|
|
36
|
+
if (allAgents.length === 0) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"No squad agents found. Agent definitions should be in ~/.pi/squad/agents/. " +
|
|
39
|
+
"This usually means the extension failed to load on first run (check /reload for errors). " +
|
|
40
|
+
"Try: /reload, then run the squad command again."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
const agentList = allAgents
|
|
37
45
|
.map((a) => `- **${a.name}** (${a.role}): ${a.description} [tags: ${a.tags.join(", ")}]`)
|
|
38
46
|
.join("\n");
|
|
@@ -250,6 +258,13 @@ Respond with a JSON object (and nothing else outside the JSON):
|
|
|
250
258
|
- Include a final QA/verification task if there are user-facing changes
|
|
251
259
|
- Keep descriptions actionable — agent should know exactly what to build
|
|
252
260
|
- Don't over-decompose — 3-7 tasks is usually right for most goals
|
|
261
|
+
|
|
262
|
+
## Shared Contracts
|
|
263
|
+
- When tasks share an interface (e.g., API endpoints, database schema, data formats), create a design/architecture task first that defines the contract, and make consuming tasks depend on it
|
|
264
|
+
- If tasks run in parallel, they MUST depend on a shared spec task that defines the interface between them
|
|
265
|
+
- Frontend tasks should depend on backend API tasks — the frontend agent needs a running API to test against
|
|
266
|
+
- In each task description, specify the exact API paths, data schemas, and conventions that the agent should follow or create
|
|
267
|
+
|
|
253
268
|
`;
|
|
254
269
|
}
|
|
255
270
|
|
package/src/protocol.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { AgentDef, KnowledgeEntry, Squad, Task, TaskMessage } from "./types.js";
|
|
14
|
-
import { loadAllKnowledge, loadAllTasks, loadMessages } from "./store.js";
|
|
14
|
+
import { loadAllKnowledge, loadAllTasks, loadMessages, loadOverview } from "./store.js";
|
|
15
15
|
|
|
16
16
|
// ============================================================================
|
|
17
17
|
// Squad Protocol (injected into every agent)
|
|
@@ -54,7 +54,9 @@ The squad system will detect this and route help.
|
|
|
54
54
|
## Rules
|
|
55
55
|
- Stay focused on YOUR task — don't do work assigned to other agents
|
|
56
56
|
- Read the dependency outputs below — don't redo completed work
|
|
57
|
+
- **Follow the Design Contract in the Squad Progress Document** — use the exact API paths, ports, schemas, and file names specified. Do NOT invent alternatives
|
|
57
58
|
- Check the modified files list — coordinate before editing shared files
|
|
59
|
+
- When creating APIs, clearly document all endpoints, request/response shapes, and status codes in your completion output
|
|
58
60
|
- Ask for help if stuck — don't spin for more than a few minutes
|
|
59
61
|
- Verify your work before claiming done
|
|
60
62
|
`;
|
|
@@ -114,6 +116,20 @@ ${sections.join("\n---\n\n")}
|
|
|
114
116
|
`;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Squad Progress Overview
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
export function buildOverviewSection(squadId: string): string {
|
|
124
|
+
const content = loadOverview(squadId);
|
|
125
|
+
if (!content.trim()) return "";
|
|
126
|
+
|
|
127
|
+
return `# Squad Progress Document
|
|
128
|
+
|
|
129
|
+
${content.trim()}
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
117
133
|
// ============================================================================
|
|
118
134
|
// Sibling Awareness
|
|
119
135
|
// ============================================================================
|
|
@@ -293,6 +309,7 @@ export function buildAgentSystemPrompt(options: ProtocolBuildOptions): string {
|
|
|
293
309
|
buildTaskSection(task),
|
|
294
310
|
buildReworkContext(task, squadId),
|
|
295
311
|
buildChainContext(task, allTasks, squadId),
|
|
312
|
+
buildOverviewSection(squadId),
|
|
296
313
|
buildSiblingAwareness(task, allTasks, modifiedFiles),
|
|
297
314
|
buildKnowledgeSection(squadId),
|
|
298
315
|
buildQueuedMessages(queuedMessages),
|
package/src/scheduler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { AgentPool, type AgentEvent } from "./agent-pool.js";
|
|
|
14
14
|
import { Monitor } from "./monitor.js";
|
|
15
15
|
import { Router } from "./router.js";
|
|
16
16
|
import * as store from "./store.js";
|
|
17
|
+
import { debug, logError } from "./logger.js";
|
|
17
18
|
import { buildAgentSystemPrompt } from "./protocol.js";
|
|
18
19
|
|
|
19
20
|
// ============================================================================
|
|
@@ -55,6 +56,8 @@ export class Scheduler {
|
|
|
55
56
|
private listeners: SchedulerEventListener[] = [];
|
|
56
57
|
private skillPaths: string[] = [];
|
|
57
58
|
private running = false;
|
|
59
|
+
/** Track spawn retries to allow one retry per task */
|
|
60
|
+
private spawnRetries = new Set<string>();
|
|
58
61
|
|
|
59
62
|
/** Get the project cwd for this squad (from squad.json) */
|
|
60
63
|
getProjectCwd(): string | undefined {
|
|
@@ -181,13 +184,13 @@ export class Scheduler {
|
|
|
181
184
|
/** Find and spawn ready tasks up to concurrency limit */
|
|
182
185
|
private async scheduleReadyTasks(): Promise<void> {
|
|
183
186
|
if (!this.running) {
|
|
184
|
-
|
|
187
|
+
debug("squad-scheduler", "scheduleReadyTasks: not running, skipping");
|
|
185
188
|
return;
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
const squad = store.loadSquad(this.squadId);
|
|
189
192
|
if (!squad || squad.status !== "running") {
|
|
190
|
-
|
|
193
|
+
debug("squad-scheduler", `scheduleReadyTasks: squad status=${squad?.status}, skipping`);
|
|
191
194
|
return;
|
|
192
195
|
}
|
|
193
196
|
|
|
@@ -195,22 +198,25 @@ export class Scheduler {
|
|
|
195
198
|
const runningCount = this.pool.getRunningAgents().length;
|
|
196
199
|
const available = squad.config.maxConcurrency - runningCount;
|
|
197
200
|
|
|
198
|
-
|
|
201
|
+
debug("squad-scheduler", `scheduleReadyTasks: ${tasks.length} tasks, ${runningCount} running, ${available} slots`);
|
|
199
202
|
|
|
200
203
|
if (available <= 0) {
|
|
201
|
-
|
|
204
|
+
debug("squad-scheduler", "scheduleReadyTasks: no available slots");
|
|
202
205
|
return;
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
const ready = this.getReadyTasks(tasks);
|
|
206
|
-
|
|
209
|
+
debug("squad-scheduler", `scheduleReadyTasks: ${ready.length} ready tasks: ${ready.map(t => t.id).join(", ")}`);
|
|
207
210
|
const toSpawn = ready.slice(0, available);
|
|
208
211
|
|
|
209
212
|
for (const task of toSpawn) {
|
|
210
213
|
try {
|
|
211
214
|
await this.spawnAgentForTask(task, squad);
|
|
212
215
|
} catch (error) {
|
|
213
|
-
|
|
216
|
+
logError("squad-scheduler", `Failed to spawn ${task.id}: ${(error as Error).message}`);
|
|
217
|
+
// MUST fail the task — otherwise it stays in_progress forever
|
|
218
|
+
// with no process (zombie state)
|
|
219
|
+
this.handleTaskFailed(task.id, `Spawn failed: ${(error as Error).message}`);
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
|
|
@@ -381,8 +387,29 @@ export class Scheduler {
|
|
|
381
387
|
if (exitCode === 0 || hadMeaningfulWork) {
|
|
382
388
|
this.handleTaskCompleted(event.taskId).then(() => this.updateContext());
|
|
383
389
|
} else {
|
|
384
|
-
|
|
385
|
-
|
|
390
|
+
// Agent died with no work done — retry once before failing.
|
|
391
|
+
// Transient failures (resource pressure, startup race) are common
|
|
392
|
+
// when multiple agents spawn simultaneously.
|
|
393
|
+
const retryKey = `spawn-retry:${event.taskId}`;
|
|
394
|
+
if (!this.spawnRetries.has(retryKey)) {
|
|
395
|
+
this.spawnRetries.add(retryKey);
|
|
396
|
+
const stderr = event.data?.stderr || "";
|
|
397
|
+
logError("squad-scheduler", `Agent ${event.agentName} died instantly (code ${exitCode}). Retrying in 2s... stderr: ${stderr.slice(0, 200)}`);
|
|
398
|
+
store.updateTaskStatus(this.squadId, event.taskId, "pending");
|
|
399
|
+
store.appendMessage(this.squadId, event.taskId, {
|
|
400
|
+
ts: store.now(),
|
|
401
|
+
from: "system",
|
|
402
|
+
type: "status",
|
|
403
|
+
text: `Agent crashed on startup (code ${exitCode}). Retrying...`,
|
|
404
|
+
});
|
|
405
|
+
// Delay retry to let resources settle
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
if (this.running) this.scheduleReadyTasks();
|
|
408
|
+
}, 2000);
|
|
409
|
+
} else {
|
|
410
|
+
const stderr = event.data?.stderr || "";
|
|
411
|
+
this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} (retry exhausted). ${stderr.slice(0, 500)}`);
|
|
412
|
+
}
|
|
386
413
|
this.updateContext();
|
|
387
414
|
}
|
|
388
415
|
// Skip the updateContext() below — handled in the branches above
|
|
@@ -430,6 +457,10 @@ export class Scheduler {
|
|
|
430
457
|
text: "Task completed",
|
|
431
458
|
});
|
|
432
459
|
|
|
460
|
+
// Reload task after status update so overview has output, completed time, etc.
|
|
461
|
+
const updatedTask = store.loadTask(this.squadId, taskId) || task;
|
|
462
|
+
this.appendTaskOverview(updatedTask, taskId, messages);
|
|
463
|
+
|
|
433
464
|
this.emit({
|
|
434
465
|
type: "task_completed",
|
|
435
466
|
squadId: this.squadId,
|
|
@@ -444,7 +475,7 @@ export class Scheduler {
|
|
|
444
475
|
|
|
445
476
|
if (!reworkCreated) {
|
|
446
477
|
// Normal flow: auto-unblock dependents
|
|
447
|
-
|
|
478
|
+
debug("squad-scheduler", `handleTaskCompleted: ${taskId} done, auto-unblocking dependents`);
|
|
448
479
|
this.autoUnblock(taskId);
|
|
449
480
|
|
|
450
481
|
// If this is a passing retest, also unblock dependents of the ORIGINAL
|
|
@@ -459,19 +490,19 @@ export class Scheduler {
|
|
|
459
490
|
rootId = root.retryOf;
|
|
460
491
|
root = allTasks.find((t) => t.id === rootId);
|
|
461
492
|
}
|
|
462
|
-
|
|
493
|
+
debug("squad-scheduler", `Retest passed — also unblocking dependents of original: ${rootId}`);
|
|
463
494
|
this.autoUnblock(rootId);
|
|
464
495
|
}
|
|
465
496
|
}
|
|
466
497
|
|
|
467
498
|
// Schedule next ready tasks (may spawn new agents)
|
|
468
|
-
|
|
499
|
+
debug("squad-scheduler", `handleTaskCompleted: scheduling next ready tasks`);
|
|
469
500
|
await this.scheduleReadyTasks();
|
|
470
501
|
|
|
471
502
|
// Re-check squad completion with fresh data AFTER scheduling
|
|
472
503
|
const freshTasks = store.loadAllTasks(this.squadId);
|
|
473
504
|
const freshSquad = store.loadSquad(this.squadId);
|
|
474
|
-
|
|
505
|
+
debug("squad-scheduler", `handleTaskCompleted: final check — tasks: ${freshTasks.map(t => `${t.id}:${t.status}`).join(", ")}`);
|
|
475
506
|
if (freshSquad) {
|
|
476
507
|
this.checkSquadCompletion(freshTasks, freshSquad);
|
|
477
508
|
}
|
|
@@ -594,7 +625,7 @@ export class Scheduler {
|
|
|
594
625
|
const originalId = implTask.retryOf || implTask.id;
|
|
595
626
|
|
|
596
627
|
if (retryCount >= squad.config.maxRetries) {
|
|
597
|
-
|
|
628
|
+
debug("squad-scheduler", `Retry limit reached for ${originalId} (${retryCount}/${squad.config.maxRetries})`);
|
|
598
629
|
this.emit({
|
|
599
630
|
type: "escalation",
|
|
600
631
|
squadId: this.squadId,
|
|
@@ -663,7 +694,7 @@ export class Scheduler {
|
|
|
663
694
|
message: `QA found issues in ${implTask.id}. Rework attempt ${fixN}.`,
|
|
664
695
|
});
|
|
665
696
|
|
|
666
|
-
|
|
697
|
+
debug("squad-scheduler", `Rework: ${reworkId} (${implTask.agent}) + retest ${retestId} (${task.agent})`);
|
|
667
698
|
createdAny = true;
|
|
668
699
|
}
|
|
669
700
|
|
|
@@ -905,6 +936,89 @@ export class Scheduler {
|
|
|
905
936
|
.map((p: any) => p.text);
|
|
906
937
|
return textParts.length > 0 ? textParts.join("\n") : null;
|
|
907
938
|
}
|
|
939
|
+
|
|
940
|
+
// =========================================================================
|
|
941
|
+
// Overview Document — append task summary after completion
|
|
942
|
+
// =========================================================================
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Build a concise markdown summary of a completed task and append it
|
|
946
|
+
* to the squad's OVERVIEW.md. This gives subsequent agents a narrative
|
|
947
|
+
* understanding of what was accomplished, issues hit, and decisions made.
|
|
948
|
+
*/
|
|
949
|
+
private appendTaskOverview(
|
|
950
|
+
task: Task,
|
|
951
|
+
taskId: string,
|
|
952
|
+
messages: import("./types.js").TaskMessage[],
|
|
953
|
+
): void {
|
|
954
|
+
try {
|
|
955
|
+
const lines: string[] = [];
|
|
956
|
+
|
|
957
|
+
// Header
|
|
958
|
+
lines.push(`## ${task.id}: ${task.title}`);
|
|
959
|
+
lines.push(``);
|
|
960
|
+
lines.push(`**Agent:** ${task.agent} | **Status:** ${task.status}`);
|
|
961
|
+
if (task.started && task.completed) {
|
|
962
|
+
const dur = new Date(task.completed).getTime() - new Date(task.started).getTime();
|
|
963
|
+
lines.push(`**Duration:** ${formatElapsed(dur)}`);
|
|
964
|
+
}
|
|
965
|
+
lines.push(``);
|
|
966
|
+
|
|
967
|
+
// Output
|
|
968
|
+
if (task.output) {
|
|
969
|
+
lines.push(`### Output`);
|
|
970
|
+
lines.push(task.output.slice(0, 800));
|
|
971
|
+
if (task.output.length > 800) lines.push(`... (truncated)`);
|
|
972
|
+
lines.push(``);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Issues / errors encountered
|
|
976
|
+
const errors = messages.filter((m) => m.type === "error" && m.from !== "system");
|
|
977
|
+
if (errors.length > 0) {
|
|
978
|
+
lines.push(`### Issues Encountered`);
|
|
979
|
+
for (const err of errors.slice(-5)) {
|
|
980
|
+
lines.push(`- ${err.text.split("\n")[0].slice(0, 200)}`);
|
|
981
|
+
}
|
|
982
|
+
lines.push(``);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Decisions — scan agent text for decision-like language
|
|
986
|
+
const decisionPatterns = /\b(decided|chose|approach|instead of|trade-?off|opted|going with|switched to|using .+ because)\b/i;
|
|
987
|
+
const agentTexts = messages.filter((m) => m.from === task.agent && m.type === "text");
|
|
988
|
+
const decisions: string[] = [];
|
|
989
|
+
for (const msg of agentTexts) {
|
|
990
|
+
for (const line of msg.text.split("\n")) {
|
|
991
|
+
if (decisionPatterns.test(line) && line.trim().length > 10) {
|
|
992
|
+
decisions.push(line.trim().slice(0, 200));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (decisions.length > 0) {
|
|
997
|
+
lines.push(`### Decisions`);
|
|
998
|
+
for (const d of decisions.slice(-5)) {
|
|
999
|
+
lines.push(`- ${d}`);
|
|
1000
|
+
}
|
|
1001
|
+
lines.push(``);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Files modified
|
|
1005
|
+
const activity = this.pool.getActivity(taskId);
|
|
1006
|
+
if (activity && activity.modifiedFiles.size > 0) {
|
|
1007
|
+
lines.push(`### Files Modified`);
|
|
1008
|
+
for (const f of Array.from(activity.modifiedFiles).slice(0, 15)) {
|
|
1009
|
+
lines.push(`- ${f}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (activity.modifiedFiles.size > 15) {
|
|
1012
|
+
lines.push(`- ... and ${activity.modifiedFiles.size - 15} more`);
|
|
1013
|
+
}
|
|
1014
|
+
lines.push(``);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
store.appendOverview(this.squadId, lines.join("\n"));
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
logError("squad-scheduler", `Failed to append overview for ${taskId}: ${(err as Error).message}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
908
1022
|
}
|
|
909
1023
|
|
|
910
1024
|
function formatElapsed(ms: number): string {
|
package/src/store.ts
CHANGED
|
@@ -405,6 +405,32 @@ export function loadAllKnowledge(squadId: string): KnowledgeEntry[] {
|
|
|
405
405
|
].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// Overview Document (shared squad summary)
|
|
410
|
+
// ============================================================================
|
|
411
|
+
|
|
412
|
+
export function getOverviewPath(squadId: string): string {
|
|
413
|
+
return path.join(getSquadDir(squadId), "OVERVIEW.md");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function loadOverview(squadId: string): string {
|
|
417
|
+
try {
|
|
418
|
+
return fs.readFileSync(getOverviewPath(squadId), "utf-8");
|
|
419
|
+
} catch {
|
|
420
|
+
return "";
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function appendOverview(squadId: string, section: string): void {
|
|
425
|
+
const filePath = getOverviewPath(squadId);
|
|
426
|
+
ensureDir(path.dirname(filePath));
|
|
427
|
+
const existing = loadOverview(squadId);
|
|
428
|
+
const content = existing
|
|
429
|
+
? existing.trimEnd() + "\n\n---\n\n" + section.trim() + "\n"
|
|
430
|
+
: section.trim() + "\n";
|
|
431
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
432
|
+
}
|
|
433
|
+
|
|
408
434
|
// ============================================================================
|
|
409
435
|
// Rework Helpers
|
|
410
436
|
// ============================================================================
|