pi-squad 0.5.0 → 0.6.1
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/package.json +2 -2
- package/src/agent-pool.ts +4 -3
- package/src/index.ts +144 -19
- package/src/logger.ts +68 -0
- package/src/panel/squad-widget.ts +81 -24
- package/src/planner.ts +15 -0
- package/src/protocol.ts +18 -1
- package/src/scheduler.ts +101 -13
- package/src/store.ts +26 -0
- /package/src/skills/{supervisor → squad-supervisor}/SKILL.md +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"pi": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"src/index.ts"
|
|
9
9
|
],
|
|
10
10
|
"skills": [
|
|
11
|
-
"src/skills/supervisor"
|
|
11
|
+
"src/skills/squad-supervisor"
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
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,7 +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]);
|
|
168
|
-
|
|
169
|
+
debug("squad-pool", `spawn ${agentDef.name}: ${invocation.command} ${invocation.args.slice(0, 3).join(" ")} ...`);
|
|
169
170
|
const proc = spawn(invocation.command, invocation.args, {
|
|
170
171
|
cwd,
|
|
171
172
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -214,7 +215,7 @@ export class AgentPool {
|
|
|
214
215
|
proc.on("exit", (code, signal) => {
|
|
215
216
|
// Log diagnostic info for debugging spawn failures
|
|
216
217
|
if (code !== 0 && code !== null) {
|
|
217
|
-
|
|
218
|
+
logError("squad-pool", `${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr.slice(0, 300) || "(empty)"}`);
|
|
218
219
|
}
|
|
219
220
|
// Clean up agents map so getRunningAgents() doesn't count dead processes
|
|
220
221
|
this.agents.delete(taskId);
|
|
@@ -377,7 +378,7 @@ export class AgentPool {
|
|
|
377
378
|
// Pi RPC mode emits agent_end when the agent loop finishes.
|
|
378
379
|
// The RPC process stays alive waiting for more commands,
|
|
379
380
|
// so we need to explicitly kill it and emit our own agent_end.
|
|
380
|
-
|
|
381
|
+
debug("squad-pool", `agent_end from RPC: ${agent.agentName} (task: ${agent.taskId})`);
|
|
381
382
|
// Mark the guard to prevent double-emit from proc.on("exit")
|
|
382
383
|
const guardFn = (agent as any)._agentEndEmitted;
|
|
383
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
|
|
@@ -348,15 +349,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
348
349
|
switch (event.type) {
|
|
349
350
|
case "squad_completed": {
|
|
350
351
|
const tasks = store.loadAllTasks(squadId);
|
|
351
|
-
const summary = tasks
|
|
352
|
-
.filter((t) => t.status === "done")
|
|
353
|
-
.map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
|
|
354
|
-
.join("\n");
|
|
355
352
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
356
353
|
const s = schedulers.get(squadId); if (s) s.updateContext();
|
|
354
|
+
const overview = store.loadOverview(squadId);
|
|
357
355
|
pi.sendMessage({
|
|
358
356
|
customType: "squad-completed",
|
|
359
|
-
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)}`,
|
|
360
360
|
display: true,
|
|
361
361
|
});
|
|
362
362
|
schedulers.delete(squadId);
|
|
@@ -367,9 +367,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
367
367
|
const tasks = store.loadAllTasks(squadId);
|
|
368
368
|
const failed = tasks.filter((t) => t.status === "failed");
|
|
369
369
|
const done = tasks.filter((t) => t.status === "done");
|
|
370
|
+
const overview = store.loadOverview(squadId);
|
|
370
371
|
pi.sendMessage({
|
|
371
372
|
customType: "squad-failed",
|
|
372
|
-
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}` : ""),
|
|
373
376
|
display: true,
|
|
374
377
|
}, { triggerTurn: true });
|
|
375
378
|
forceWidgetUpdate();
|
|
@@ -389,7 +392,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
389
392
|
|
|
390
393
|
const resumeSched = schedulers.get(squadId)!;
|
|
391
394
|
resumeSched.resume().catch((err) => {
|
|
392
|
-
|
|
395
|
+
logError("squad", `Resume error: ${(err as Error).message}`);
|
|
393
396
|
});
|
|
394
397
|
|
|
395
398
|
const tasks = store.loadAllTasks(squadId);
|
|
@@ -440,7 +443,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
440
443
|
case "resume_task": {
|
|
441
444
|
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
|
|
442
445
|
activeScheduler.resumeTask(params.taskId).catch((err) => {
|
|
443
|
-
|
|
446
|
+
logError("squad", `Resume task error: ${(err as Error).message}`);
|
|
444
447
|
});
|
|
445
448
|
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }] };
|
|
446
449
|
}
|
|
@@ -1213,6 +1216,31 @@ async function startSquad(
|
|
|
1213
1216
|
|
|
1214
1217
|
store.saveSquad(squad);
|
|
1215
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
|
+
|
|
1216
1244
|
// Create task files
|
|
1217
1245
|
for (const taskDef of plan.tasks) {
|
|
1218
1246
|
const task: Task = {
|
|
@@ -1258,12 +1286,8 @@ async function startSquad(
|
|
|
1258
1286
|
// Update widget on every scheduler event
|
|
1259
1287
|
forceWidgetUpdate();
|
|
1260
1288
|
switch (event.type) {
|
|
1261
|
-
|
|
1289
|
+
case "squad_completed": {
|
|
1262
1290
|
const tasks = store.loadAllTasks(squadId);
|
|
1263
|
-
const summary = tasks
|
|
1264
|
-
.filter((t) => t.status === "done")
|
|
1265
|
-
.map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
|
|
1266
|
-
.join("\n");
|
|
1267
1291
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
1268
1292
|
|
|
1269
1293
|
// Final context update before clearing scheduler
|
|
@@ -1272,12 +1296,17 @@ async function startSquad(
|
|
|
1272
1296
|
completedSched.updateContext();
|
|
1273
1297
|
}
|
|
1274
1298
|
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
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.
|
|
1277
1306
|
pi.sendMessage({
|
|
1278
1307
|
customType: "squad-completed",
|
|
1279
1308
|
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
|
|
1280
|
-
|
|
1309
|
+
(overview ? `## Squad Overview\n\n${overview}\n\n` : "") +
|
|
1281
1310
|
`Total cost: $${totalCost.toFixed(4)}`,
|
|
1282
1311
|
display: true,
|
|
1283
1312
|
});
|
|
@@ -1292,14 +1321,14 @@ async function startSquad(
|
|
|
1292
1321
|
const tasks = store.loadAllTasks(squadId);
|
|
1293
1322
|
const failed = tasks.filter((t) => t.status === "failed");
|
|
1294
1323
|
const done = tasks.filter((t) => t.status === "done");
|
|
1324
|
+
const overview = store.loadOverview(squadId);
|
|
1295
1325
|
|
|
1296
|
-
// Squad failed — notify user. Use triggerTurn so the agent can
|
|
1297
|
-
// suggest next steps (retry, modify, cancel).
|
|
1298
1326
|
pi.sendMessage({
|
|
1299
1327
|
customType: "squad-failed",
|
|
1300
1328
|
content: `[squad] Squad "${squadId}" has stalled. ` +
|
|
1301
1329
|
`${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
|
|
1302
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") +
|
|
1303
1332
|
`Use squad_status for details or squad_modify to adjust.`,
|
|
1304
1333
|
display: true,
|
|
1305
1334
|
}, { triggerTurn: true });
|
|
@@ -1327,7 +1356,7 @@ async function startSquad(
|
|
|
1327
1356
|
// We must return immediately so the main agent's turn completes
|
|
1328
1357
|
// and the user regains interactive control.
|
|
1329
1358
|
scheduler.start().catch((err) => {
|
|
1330
|
-
|
|
1359
|
+
logError("squad", `Scheduler start error: ${(err as Error).message}`);
|
|
1331
1360
|
});
|
|
1332
1361
|
|
|
1333
1362
|
// Build response
|
|
@@ -1360,3 +1389,99 @@ function getSquadSkillPaths(skillsDir: string): string[] {
|
|
|
1360
1389
|
.map((d) => path.join(skillsDir, d.name))
|
|
1361
1390
|
.filter((dir) => fs.existsSync(path.join(dir, "SKILL.md")));
|
|
1362
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
|
+
}
|
|
@@ -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
|
// ============================================================================
|
|
@@ -183,13 +184,13 @@ export class Scheduler {
|
|
|
183
184
|
/** Find and spawn ready tasks up to concurrency limit */
|
|
184
185
|
private async scheduleReadyTasks(): Promise<void> {
|
|
185
186
|
if (!this.running) {
|
|
186
|
-
|
|
187
|
+
debug("squad-scheduler", "scheduleReadyTasks: not running, skipping");
|
|
187
188
|
return;
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
const squad = store.loadSquad(this.squadId);
|
|
191
192
|
if (!squad || squad.status !== "running") {
|
|
192
|
-
|
|
193
|
+
debug("squad-scheduler", `scheduleReadyTasks: squad status=${squad?.status}, skipping`);
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
196
|
|
|
@@ -197,22 +198,22 @@ export class Scheduler {
|
|
|
197
198
|
const runningCount = this.pool.getRunningAgents().length;
|
|
198
199
|
const available = squad.config.maxConcurrency - runningCount;
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
debug("squad-scheduler", `scheduleReadyTasks: ${tasks.length} tasks, ${runningCount} running, ${available} slots`);
|
|
201
202
|
|
|
202
203
|
if (available <= 0) {
|
|
203
|
-
|
|
204
|
+
debug("squad-scheduler", "scheduleReadyTasks: no available slots");
|
|
204
205
|
return;
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
const ready = this.getReadyTasks(tasks);
|
|
208
|
-
|
|
209
|
+
debug("squad-scheduler", `scheduleReadyTasks: ${ready.length} ready tasks: ${ready.map(t => t.id).join(", ")}`);
|
|
209
210
|
const toSpawn = ready.slice(0, available);
|
|
210
211
|
|
|
211
212
|
for (const task of toSpawn) {
|
|
212
213
|
try {
|
|
213
214
|
await this.spawnAgentForTask(task, squad);
|
|
214
215
|
} catch (error) {
|
|
215
|
-
|
|
216
|
+
logError("squad-scheduler", `Failed to spawn ${task.id}: ${(error as Error).message}`);
|
|
216
217
|
// MUST fail the task — otherwise it stays in_progress forever
|
|
217
218
|
// with no process (zombie state)
|
|
218
219
|
this.handleTaskFailed(task.id, `Spawn failed: ${(error as Error).message}`);
|
|
@@ -393,7 +394,7 @@ export class Scheduler {
|
|
|
393
394
|
if (!this.spawnRetries.has(retryKey)) {
|
|
394
395
|
this.spawnRetries.add(retryKey);
|
|
395
396
|
const stderr = event.data?.stderr || "";
|
|
396
|
-
|
|
397
|
+
logError("squad-scheduler", `Agent ${event.agentName} died instantly (code ${exitCode}). Retrying in 2s... stderr: ${stderr.slice(0, 200)}`);
|
|
397
398
|
store.updateTaskStatus(this.squadId, event.taskId, "pending");
|
|
398
399
|
store.appendMessage(this.squadId, event.taskId, {
|
|
399
400
|
ts: store.now(),
|
|
@@ -456,6 +457,10 @@ export class Scheduler {
|
|
|
456
457
|
text: "Task completed",
|
|
457
458
|
});
|
|
458
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
|
+
|
|
459
464
|
this.emit({
|
|
460
465
|
type: "task_completed",
|
|
461
466
|
squadId: this.squadId,
|
|
@@ -470,7 +475,7 @@ export class Scheduler {
|
|
|
470
475
|
|
|
471
476
|
if (!reworkCreated) {
|
|
472
477
|
// Normal flow: auto-unblock dependents
|
|
473
|
-
|
|
478
|
+
debug("squad-scheduler", `handleTaskCompleted: ${taskId} done, auto-unblocking dependents`);
|
|
474
479
|
this.autoUnblock(taskId);
|
|
475
480
|
|
|
476
481
|
// If this is a passing retest, also unblock dependents of the ORIGINAL
|
|
@@ -485,19 +490,19 @@ export class Scheduler {
|
|
|
485
490
|
rootId = root.retryOf;
|
|
486
491
|
root = allTasks.find((t) => t.id === rootId);
|
|
487
492
|
}
|
|
488
|
-
|
|
493
|
+
debug("squad-scheduler", `Retest passed — also unblocking dependents of original: ${rootId}`);
|
|
489
494
|
this.autoUnblock(rootId);
|
|
490
495
|
}
|
|
491
496
|
}
|
|
492
497
|
|
|
493
498
|
// Schedule next ready tasks (may spawn new agents)
|
|
494
|
-
|
|
499
|
+
debug("squad-scheduler", `handleTaskCompleted: scheduling next ready tasks`);
|
|
495
500
|
await this.scheduleReadyTasks();
|
|
496
501
|
|
|
497
502
|
// Re-check squad completion with fresh data AFTER scheduling
|
|
498
503
|
const freshTasks = store.loadAllTasks(this.squadId);
|
|
499
504
|
const freshSquad = store.loadSquad(this.squadId);
|
|
500
|
-
|
|
505
|
+
debug("squad-scheduler", `handleTaskCompleted: final check — tasks: ${freshTasks.map(t => `${t.id}:${t.status}`).join(", ")}`);
|
|
501
506
|
if (freshSquad) {
|
|
502
507
|
this.checkSquadCompletion(freshTasks, freshSquad);
|
|
503
508
|
}
|
|
@@ -620,7 +625,7 @@ export class Scheduler {
|
|
|
620
625
|
const originalId = implTask.retryOf || implTask.id;
|
|
621
626
|
|
|
622
627
|
if (retryCount >= squad.config.maxRetries) {
|
|
623
|
-
|
|
628
|
+
debug("squad-scheduler", `Retry limit reached for ${originalId} (${retryCount}/${squad.config.maxRetries})`);
|
|
624
629
|
this.emit({
|
|
625
630
|
type: "escalation",
|
|
626
631
|
squadId: this.squadId,
|
|
@@ -689,7 +694,7 @@ export class Scheduler {
|
|
|
689
694
|
message: `QA found issues in ${implTask.id}. Rework attempt ${fixN}.`,
|
|
690
695
|
});
|
|
691
696
|
|
|
692
|
-
|
|
697
|
+
debug("squad-scheduler", `Rework: ${reworkId} (${implTask.agent}) + retest ${retestId} (${task.agent})`);
|
|
693
698
|
createdAny = true;
|
|
694
699
|
}
|
|
695
700
|
|
|
@@ -931,6 +936,89 @@ export class Scheduler {
|
|
|
931
936
|
.map((p: any) => p.text);
|
|
932
937
|
return textParts.length > 0 ? textParts.join("\n") : null;
|
|
933
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
|
+
}
|
|
934
1022
|
}
|
|
935
1023
|
|
|
936
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
|
// ============================================================================
|
|
File without changes
|