@tmustier/pi-agent-teams 0.1.2 → 0.3.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.
Files changed (39) hide show
  1. package/README.md +50 -9
  2. package/docs/claude-parity.md +22 -18
  3. package/docs/field-notes-teams-setup.md +6 -4
  4. package/docs/smoke-test-plan.md +139 -0
  5. package/eslint.config.js +74 -0
  6. package/extensions/teams/activity-tracker.ts +234 -0
  7. package/extensions/teams/fs-lock.ts +21 -5
  8. package/extensions/teams/leader-inbox.ts +175 -0
  9. package/extensions/teams/leader-info-commands.ts +139 -0
  10. package/extensions/teams/leader-lifecycle-commands.ts +343 -0
  11. package/extensions/teams/leader-messaging-commands.ts +148 -0
  12. package/extensions/teams/leader-plan-commands.ts +96 -0
  13. package/extensions/teams/leader-spawn-command.ts +57 -0
  14. package/extensions/teams/leader-task-commands.ts +421 -0
  15. package/extensions/teams/leader-team-command.ts +312 -0
  16. package/extensions/teams/leader-teams-tool.ts +227 -0
  17. package/extensions/teams/leader.ts +260 -1562
  18. package/extensions/teams/mailbox.ts +54 -29
  19. package/extensions/teams/names.ts +87 -0
  20. package/extensions/teams/protocol.ts +241 -0
  21. package/extensions/teams/spawn-types.ts +21 -0
  22. package/extensions/teams/task-store.ts +36 -21
  23. package/extensions/teams/team-config.ts +71 -25
  24. package/extensions/teams/teammate-rpc.ts +81 -23
  25. package/extensions/teams/teams-panel.ts +644 -0
  26. package/extensions/teams/teams-style.ts +62 -0
  27. package/extensions/teams/teams-ui-shared.ts +89 -0
  28. package/extensions/teams/teams-widget.ts +182 -0
  29. package/extensions/teams/worker.ts +100 -138
  30. package/extensions/teams/worktree.ts +4 -7
  31. package/package.json +32 -5
  32. package/scripts/integration-claim-test.mts +157 -0
  33. package/scripts/integration-todo-test.mts +532 -0
  34. package/scripts/lib/pi-workers.ts +105 -0
  35. package/scripts/smoke-test.mts +424 -0
  36. package/skills/agent-teams/SKILL.md +139 -0
  37. package/tsconfig.strict.json +22 -0
  38. package/extensions/teams/tasks.ts +0 -95
  39. package/scripts/smoke-test.mjs +0 -199
@@ -0,0 +1,532 @@
1
+ /**
2
+ * Integration test: spawn 3 real Pi worker processes and create 15 dependent tasks
3
+ * that collaboratively build a minimal (vanilla JS) todo app in a dedicated team
4
+ * artifacts workspace.
5
+ *
6
+ * Requirements covered:
7
+ * - Uses task-store createTask + addTaskDependency; relies on worker auto-claim.
8
+ * - 3 agents, 15 tasks, realistic dependency order.
9
+ * - Workspace is under ~/.pi/agent/teams/<teamId>/artifacts/todo-app (teamDir/artifacts/todo-app).
10
+ * - Periodically prints status counts; prints per-task summary on completion.
11
+ * - After completion, tails the session .jsonl files for each agent.
12
+ *
13
+ * Usage:
14
+ * npx tsx scripts/integration-todo-test.mts
15
+ * npx tsx scripts/integration-todo-test.mts --timeoutSec 900
16
+ */
17
+
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { spawnSync, type ChildProcess } from "node:child_process";
21
+ import { randomUUID } from "node:crypto";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ import { ensureTeamConfig } from "../extensions/teams/team-config.js";
25
+ import { getTeamDir } from "../extensions/teams/paths.js";
26
+ import {
27
+ addTaskDependency,
28
+ isTaskBlocked,
29
+ listTasks,
30
+ createTask,
31
+ type TeamTask,
32
+ } from "../extensions/teams/task-store.js";
33
+
34
+ import { sleep, spawnTeamsWorkerRpc, terminateAll } from "./lib/pi-workers.js";
35
+
36
+ function parseArgs(argv: readonly string[]): { timeoutSec: number; pollMs: number } {
37
+ let timeoutSec = 15 * 60;
38
+ let pollMs = 1500;
39
+
40
+ for (let i = 0; i < argv.length; i += 1) {
41
+ const a = argv[i];
42
+ if (a === "--timeoutSec") {
43
+ const v = argv[i + 1];
44
+ if (v) timeoutSec = Number.parseInt(v, 10);
45
+ i += 1;
46
+ continue;
47
+ }
48
+ if (a === "--pollMs") {
49
+ const v = argv[i + 1];
50
+ if (v) pollMs = Number.parseInt(v, 10);
51
+ i += 1;
52
+ continue;
53
+ }
54
+ }
55
+
56
+ if (!Number.isFinite(timeoutSec) || timeoutSec < 60) timeoutSec = 15 * 60;
57
+ if (!Number.isFinite(pollMs) || pollMs < 250) pollMs = 1500;
58
+ return { timeoutSec, pollMs };
59
+ }
60
+
61
+ function spawnWorker(opts: {
62
+ cwd: string;
63
+ repoRoot: string;
64
+ entryPath: string;
65
+ sessionsDir: string;
66
+ teamId: string;
67
+ agentName: string;
68
+ logDir: string;
69
+ }): ChildProcess {
70
+ const { cwd, entryPath, sessionsDir, teamId, agentName, logDir } = opts;
71
+
72
+ const systemAppend = [
73
+ "You are a teammate in an automated integration test.",
74
+ "Work ONLY inside the current working directory.",
75
+ "Keep replies short (<= 8 lines).",
76
+ "Always end with: ACCEPTED: <one-line acceptance confirmation>.",
77
+ ].join(" ");
78
+
79
+ return spawnTeamsWorkerRpc({
80
+ cwd,
81
+ entryPath,
82
+ sessionsDir,
83
+ teamId,
84
+ taskListId: teamId,
85
+ agentName,
86
+ leadName: "team-lead",
87
+ style: "normal",
88
+ autoClaim: true,
89
+ planRequired: false,
90
+ systemAppend,
91
+ logDir,
92
+ });
93
+ }
94
+
95
+ function allCompleted(tasks: TeamTask[]): boolean {
96
+ return tasks.length === 15 && tasks.every((t) => t.status === "completed");
97
+ }
98
+
99
+ type TaskKey =
100
+ | "scaffold"
101
+ | "html"
102
+ | "css"
103
+ | "model"
104
+ | "storage"
105
+ | "main_add"
106
+ | "main_toggle_remove"
107
+ | "filters"
108
+ | "clear_completed"
109
+ | "persistence"
110
+ | "test_model"
111
+ | "test_storage"
112
+ | "verify_script"
113
+ | "readme"
114
+ | "qa";
115
+
116
+ type PlannedTask = {
117
+ key: TaskKey;
118
+ subject: string;
119
+ description: string;
120
+ dependsOn: TaskKey[];
121
+ };
122
+
123
+ function plannedTasks(): PlannedTask[] {
124
+ return [
125
+ {
126
+ key: "scaffold",
127
+ subject: "Todo app: scaffold workspace (dirs + package.json)",
128
+ description: [
129
+ "Create a minimal vanilla-JS todo app workspace.",
130
+ "- Create directories: src/, test/, scripts/",
131
+ "- Create .gitignore (ignore node_modules, .DS_Store)",
132
+ "- Create package.json (type: module, private: true) with scripts:",
133
+ " - test: node --test",
134
+ " - verify: node scripts/verify.mjs",
135
+ " - start: python3 -m http.server 5173",
136
+ "Acceptance: ls shows src/ test/ scripts/ and package.json has those scripts.",
137
+ ].join("\n"),
138
+ dependsOn: [],
139
+ },
140
+ {
141
+ key: "html",
142
+ subject: "Todo app: add index.html skeleton",
143
+ description: [
144
+ "Create index.html in repo root (workspace root).",
145
+ "Requirements:",
146
+ "- Link styles.css",
147
+ "- Load <script type=\"module\" src=\"./src/main.js\"></script>",
148
+ "- Layout includes: h1, input#new-todo, button#add-todo, ul#todo-list",
149
+ "- Include filter buttons with data-filter=all|active|completed inside #filters",
150
+ "- Include button#clear-completed",
151
+ "Acceptance: index.html contains the required ids and module script tag.",
152
+ ].join("\n"),
153
+ dependsOn: ["scaffold"],
154
+ },
155
+ {
156
+ key: "css",
157
+ subject: "Todo app: add styles.css",
158
+ description: [
159
+ "Create styles.css with basic readable styling (centered container, list rows, buttons).",
160
+ "Keep it minimal (no frameworks).",
161
+ "Acceptance: styles.css exists and includes rules for #app and #todo-list li.",
162
+ ].join("\n"),
163
+ dependsOn: ["scaffold"],
164
+ },
165
+ {
166
+ key: "model",
167
+ subject: "Todo app: implement src/model.js (pure state helpers)",
168
+ description: [
169
+ "Create src/model.js exporting pure functions (no DOM, no localStorage).",
170
+ "State shape suggestion: { todos: Array<{id:string,text:string,completed:boolean}>, filter: 'all'|'active'|'completed' }",
171
+ "Export these named functions:",
172
+ "- createInitialState()",
173
+ "- addTodo(state, text)",
174
+ "- toggleTodo(state, id)",
175
+ "- removeTodo(state, id)",
176
+ "- clearCompleted(state)",
177
+ "- setFilter(state, filter)",
178
+ "- getVisibleTodos(state)",
179
+ "Implementation notes: do not mutate the input state; return new objects.",
180
+ "Acceptance: src/model.js exists and exports all functions listed above.",
181
+ ].join("\n"),
182
+ dependsOn: ["scaffold"],
183
+ },
184
+ {
185
+ key: "storage",
186
+ subject: "Todo app: implement src/storage.js (localStorage + serialization)",
187
+ description: [
188
+ "Create src/storage.js.",
189
+ "Export:",
190
+ "- const STORAGE_KEY = 'todo-app-state-v1'",
191
+ "- serializeState(state) => string",
192
+ "- deserializeState(raw) => state|null (return null on invalid)",
193
+ "- loadState() => state|null (returns null if localStorage unavailable)",
194
+ "- saveState(state) => void (no-op if localStorage unavailable)",
195
+ "Acceptance: src/storage.js exists and loadState/saveState do not throw in Node.",
196
+ ].join("\n"),
197
+ dependsOn: ["scaffold"],
198
+ },
199
+ {
200
+ key: "main_add",
201
+ subject: "Todo app: implement src/main.js (render + add)",
202
+ description: [
203
+ "Create src/main.js to wire up the UI.",
204
+ "- Import createInitialState/addTodo/getVisibleTodos from src/model.js",
205
+ "- On load, create state and render ul#todo-list",
206
+ "- Support adding todos via button#add-todo and Enter in input#new-todo",
207
+ "- Basic render: each todo is an <li> with its text",
208
+ "Acceptance: src/main.js exists, imports model.js, and add works via DOM event listeners.",
209
+ ].join("\n"),
210
+ dependsOn: ["html", "model"],
211
+ },
212
+ {
213
+ key: "main_toggle_remove",
214
+ subject: "Todo app: main.js toggle + remove controls",
215
+ description: [
216
+ "Update src/main.js UI to support:",
217
+ "- toggle completed via checkbox per item (uses toggleTodo)",
218
+ "- remove via a delete button per item (uses removeTodo)",
219
+ "- visually indicate completed (e.g. line-through)",
220
+ "Acceptance: rendered list items include a checkbox and delete button and handlers update state.",
221
+ ].join("\n"),
222
+ dependsOn: ["main_add"],
223
+ },
224
+ {
225
+ key: "filters",
226
+ subject: "Todo app: wire filter buttons (all/active/completed)",
227
+ description: [
228
+ "Update src/main.js to wire #filters buttons (data-filter=all|active|completed).",
229
+ "- Use setFilter/getVisibleTodos from model.js",
230
+ "- Add an 'active' CSS class on the selected filter button",
231
+ "Acceptance: clicking filter buttons changes rendered list length appropriately.",
232
+ ].join("\n"),
233
+ dependsOn: ["main_toggle_remove"],
234
+ },
235
+ {
236
+ key: "clear_completed",
237
+ subject: "Todo app: implement clear completed",
238
+ description: [
239
+ "Update model.js and main.js to support clearing completed todos.",
240
+ "- Use clearCompleted(state)",
241
+ "- Wire button#clear-completed",
242
+ "Acceptance: clicking clear completed removes completed todos from state + UI.",
243
+ ].join("\n"),
244
+ dependsOn: ["filters"],
245
+ },
246
+ {
247
+ key: "persistence",
248
+ subject: "Todo app: persistence (load on start, save on changes)",
249
+ description: [
250
+ "Update src/main.js to persist state.",
251
+ "- Import loadState/saveState from storage.js",
252
+ "- On startup, initialize state from loadState() if non-null",
253
+ "- After any state change, call saveState(state) (a small debounce is ok)",
254
+ "Acceptance: main.js imports storage.js and calls saveState after add/toggle/remove/filter/clear.",
255
+ ].join("\n"),
256
+ dependsOn: ["clear_completed", "storage"],
257
+ },
258
+ {
259
+ key: "test_model",
260
+ subject: "Todo app: add node:test unit tests for model.js",
261
+ description: [
262
+ "Create test/model.test.js using node:test + node:assert/strict.",
263
+ "Cover: addTodo, toggleTodo, removeTodo, clearCompleted, filters (getVisibleTodos).",
264
+ "Acceptance: `node --test test/model.test.js` passes.",
265
+ ].join("\n"),
266
+ dependsOn: ["model", "scaffold"],
267
+ },
268
+ {
269
+ key: "test_storage",
270
+ subject: "Todo app: add tests for storage serialization",
271
+ description: [
272
+ "Create test/storage.test.js testing serializeState/deserializeState.",
273
+ "- roundtrip returns equivalent state",
274
+ "- invalid input returns null",
275
+ "Acceptance: `node --test test/storage.test.js` passes.",
276
+ ].join("\n"),
277
+ dependsOn: ["storage", "scaffold"],
278
+ },
279
+ {
280
+ key: "verify_script",
281
+ subject: "Todo app: scripts/verify.mjs (fast local verification)",
282
+ description: [
283
+ "Create scripts/verify.mjs that:",
284
+ "- checks required files exist (index.html, styles.css, src/main.js, src/model.js, src/storage.js)",
285
+ "- imports model.js and does a tiny sanity check (add -> toggle)",
286
+ "- runs `node --test` as a subprocess (or via spawnSync) and fails if tests fail",
287
+ "- prints exactly: verify: ok",
288
+ "Acceptance: `node scripts/verify.mjs` prints 'verify: ok' and exits 0.",
289
+ ].join("\n"),
290
+ dependsOn: ["test_model", "test_storage", "persistence", "html", "css"],
291
+ },
292
+ {
293
+ key: "readme",
294
+ subject: "Todo app: write README.md",
295
+ description: [
296
+ "Create README.md describing:",
297
+ "- what the app does",
298
+ "- how to run locally (python http.server)",
299
+ "- how to run tests + verify",
300
+ "Acceptance: README.md exists and mentions `npm test` and `npm run verify`.",
301
+ ].join("\n"),
302
+ dependsOn: ["verify_script"],
303
+ },
304
+ {
305
+ key: "qa",
306
+ subject: "Todo app: final QA run (tests + verify)",
307
+ description: [
308
+ "Run the final checks and fix any issues:",
309
+ "- npm test",
310
+ "- npm run verify",
311
+ "If anything fails, fix files until both pass.",
312
+ "Acceptance: paste the final two command outputs (or at least their last lines) showing success.",
313
+ ].join("\n"),
314
+ dependsOn: ["readme"],
315
+ },
316
+ ];
317
+ }
318
+
319
+ function tailLines(raw: string, n: number): string[] {
320
+ const lines = raw.split(/\r?\n/).filter((l) => l.length > 0);
321
+ return lines.slice(Math.max(0, lines.length - n));
322
+ }
323
+
324
+ function tailFile(filePath: string, n: number): string {
325
+ try {
326
+ const st = fs.statSync(filePath);
327
+ if (!st.isFile()) return "";
328
+ // Read at most last 256 KiB.
329
+ const maxBytes = 256 * 1024;
330
+ const start = Math.max(0, st.size - maxBytes);
331
+ const fd = fs.openSync(filePath, "r");
332
+ try {
333
+ const buf = Buffer.alloc(st.size - start);
334
+ fs.readSync(fd, buf, 0, buf.length, start);
335
+ return tailLines(buf.toString("utf8"), n).join("\n");
336
+ } finally {
337
+ fs.closeSync(fd);
338
+ }
339
+ } catch {
340
+ return "";
341
+ }
342
+ }
343
+
344
+ function printTaskSummary(tasks: TeamTask[]): void {
345
+ const sorted = [...tasks].sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
346
+ console.log("\nPer-task summary (id owner subject):");
347
+ for (const t of sorted) {
348
+ console.log(`- #${t.id} ${(t.owner ?? "-").padEnd(7)} ${t.subject}`);
349
+ }
350
+
351
+ const agents = ["agent1", "agent2", "agent3"];
352
+ const knownAgents = new Set<string>(agents);
353
+
354
+ const dist = new Map<string, number>();
355
+ for (const a of agents) dist.set(a, 0);
356
+ for (const t of tasks) {
357
+ const owner = t.owner;
358
+ if (!owner) continue;
359
+ dist.set(owner, (dist.get(owner) ?? 0) + 1);
360
+ }
361
+
362
+ const parts = agents.map((a) => `${a}=${dist.get(a) ?? 0}`);
363
+ const otherOwners: string[] = [];
364
+ for (const k of dist.keys()) {
365
+ if (!knownAgents.has(k)) otherOwners.push(k);
366
+ }
367
+ for (const o of otherOwners) {
368
+ parts.push(`${o}=${dist.get(o) ?? 0}`);
369
+ }
370
+
371
+ console.log(`\nOwner distribution: ${parts.join(" ")}`);
372
+ }
373
+
374
+ function runWorkspaceVerify(workspaceDir: string): { ok: boolean; output: string } {
375
+ const res = spawnSync("npm", ["run", "-s", "verify"], {
376
+ cwd: workspaceDir,
377
+ encoding: "utf8",
378
+ timeout: 120_000,
379
+ });
380
+ const out = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
381
+ return { ok: res.status === 0, output: out };
382
+ }
383
+
384
+ const { timeoutSec, pollMs } = parseArgs(process.argv.slice(2));
385
+
386
+ const teamId = randomUUID();
387
+ const teamDir = getTeamDir(teamId);
388
+ const workspaceDir = path.join(teamDir, "artifacts", "todo-app");
389
+ const sessionsDir = path.join(teamDir, "sessions");
390
+ const logsDir = path.join(teamDir, "logs");
391
+
392
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
393
+ const repoRoot = path.resolve(scriptDir, "..");
394
+ const entryPath = path.join(repoRoot, "extensions", "teams", "index.ts");
395
+
396
+ console.log(`TeamId: ${teamId}`);
397
+ console.log(`TeamDir: ${teamDir}`);
398
+ console.log(`Workspace: ${workspaceDir}`);
399
+ console.log(`SessionsDir: ${sessionsDir}`);
400
+ for (let i = 1; i <= 3; i += 1) {
401
+ console.log(`SessionFile agent${i}: ${path.join(sessionsDir, `agent${i}.jsonl`)}`);
402
+ }
403
+ console.log("Spawning 3 workers, creating 15 tasks (todo app)");
404
+
405
+ fs.mkdirSync(workspaceDir, { recursive: true });
406
+
407
+ await ensureTeamConfig(teamDir, { teamId, taskListId: teamId, leadName: "team-lead", style: "normal" });
408
+
409
+ // Create tasks first (unowned) then add dependencies.
410
+ const plan = plannedTasks();
411
+ if (plan.length !== 15) {
412
+ throw new Error(`Expected 15 planned tasks, got ${plan.length}`);
413
+ }
414
+
415
+ const created = new Map<TaskKey, TeamTask>();
416
+ for (const p of plan) {
417
+ // IMPORTANT: create tasks with owner unset so workers must auto-claim.
418
+ const t = await createTask(teamDir, teamId, { subject: p.subject, description: p.description });
419
+ created.set(p.key, t);
420
+ }
421
+
422
+ // Ensure tasks are truly unassigned (owner unset)
423
+ {
424
+ const ts = await listTasks(teamDir, teamId);
425
+ if (ts.length !== 15) {
426
+ throw new Error(`Expected 15 tasks after creation, got ${ts.length}`);
427
+ }
428
+ const owned = ts.filter((t) => t.owner !== undefined);
429
+ if (owned.length) {
430
+ throw new Error(`Expected all tasks to be unowned; found owned task ids: ${owned.map((t) => t.id).join(", ")}`);
431
+ }
432
+ console.log("Created 15 tasks with owner unset (unassigned). Workers will auto-claim.");
433
+ }
434
+
435
+ for (const p of plan) {
436
+ const task = created.get(p.key);
437
+ if (!task) throw new Error(`Missing task: ${p.key}`);
438
+ for (const depKey of p.dependsOn) {
439
+ const dep = created.get(depKey);
440
+ if (!dep) throw new Error(`Missing dependency: ${p.key} -> ${depKey}`);
441
+ const res = await addTaskDependency(teamDir, teamId, task.id, dep.id);
442
+ if (!res.ok) throw new Error(`addTaskDependency failed: ${res.error}`);
443
+ }
444
+ }
445
+
446
+ const children: ChildProcess[] = [];
447
+ let cleaningUp = false;
448
+ const cleanup = async (): Promise<void> => {
449
+ if (cleaningUp) return;
450
+ cleaningUp = true;
451
+ await terminateAll(children);
452
+ };
453
+
454
+ process.on("SIGINT", () => {
455
+ void cleanup().finally(() => process.exit(130));
456
+ });
457
+ process.on("SIGTERM", () => {
458
+ void cleanup().finally(() => process.exit(143));
459
+ });
460
+
461
+ try {
462
+ for (let i = 1; i <= 3; i += 1) {
463
+ children.push(
464
+ spawnWorker({
465
+ cwd: workspaceDir,
466
+ repoRoot,
467
+ entryPath,
468
+ sessionsDir,
469
+ teamId,
470
+ agentName: `agent${i}`,
471
+ logDir: logsDir,
472
+ }),
473
+ );
474
+ }
475
+
476
+ const deadline = Date.now() + timeoutSec * 1000;
477
+ while (Date.now() < deadline) {
478
+ const ts = await listTasks(teamDir, teamId);
479
+ const completed = ts.filter((t) => t.status === "completed").length;
480
+ const inProgress = ts.filter((t) => t.status === "in_progress").length;
481
+ const pending = ts.filter((t) => t.status === "pending").length;
482
+
483
+ let blocked = 0;
484
+ for (const t of ts) {
485
+ if (t.status !== "pending") continue;
486
+ if (await isTaskBlocked(teamDir, teamId, t)) blocked += 1;
487
+ }
488
+
489
+ console.log(
490
+ `tasks: completed=${completed} in_progress=${inProgress} pending=${pending} blocked=${blocked}`,
491
+ );
492
+
493
+ if (allCompleted(ts)) {
494
+ console.log("PASS: all tasks completed");
495
+ printTaskSummary(ts);
496
+
497
+ const verify = runWorkspaceVerify(workspaceDir);
498
+ if (!verify.ok) {
499
+ console.error("FAIL: workspace verification failed (npm run verify)");
500
+ console.error(verify.output);
501
+ process.exitCode = 1;
502
+ } else {
503
+ console.log("Workspace verification: ok");
504
+ process.exitCode = 0;
505
+ }
506
+ break;
507
+ }
508
+
509
+ await sleep(pollMs);
510
+ }
511
+
512
+ if (process.exitCode !== 0 && process.exitCode !== 1) {
513
+ console.error(`FAIL: timeout after ${timeoutSec}s (inspect logs under ${logsDir})`);
514
+ const ts = await listTasks(teamDir, teamId);
515
+ printTaskSummary(ts);
516
+ process.exitCode = 1;
517
+ }
518
+ } finally {
519
+ await cleanup();
520
+
521
+ // Transcript inspection (tail sessions)
522
+ console.log("\nSession tails (last ~30 lines each):");
523
+ for (let i = 1; i <= 3; i += 1) {
524
+ const agentName = `agent${i}`;
525
+ const sessionFile = path.join(sessionsDir, `${agentName}.jsonl`);
526
+ console.log(`\n--- ${agentName}: ${sessionFile} ---`);
527
+ const tail = tailFile(sessionFile, 30);
528
+ console.log(tail || "(no session output)");
529
+ }
530
+
531
+ console.log(`\nWorkspace remains at: ${workspaceDir}`);
532
+ }
@@ -0,0 +1,105 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { spawn, type ChildProcess } from "node:child_process";
4
+
5
+ export function sleep(ms: number): Promise<void> {
6
+ return new Promise((r) => setTimeout(r, ms));
7
+ }
8
+
9
+ export async function terminateAll(children: readonly ChildProcess[]): Promise<void> {
10
+ for (const c of children) {
11
+ try {
12
+ c.kill("SIGTERM");
13
+ } catch {
14
+ // ignore
15
+ }
16
+ }
17
+
18
+ // Give them a moment to flush + exit.
19
+ const deadline = Date.now() + 10_000;
20
+ for (const c of children) {
21
+ while (c.exitCode === null && Date.now() < deadline) {
22
+ await sleep(100);
23
+ }
24
+ if (c.exitCode === null) {
25
+ try {
26
+ c.kill("SIGKILL");
27
+ } catch {
28
+ // ignore
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ export function spawnTeamsWorkerRpc(opts: {
35
+ cwd: string;
36
+ entryPath: string;
37
+ sessionsDir: string;
38
+ teamId: string;
39
+ taskListId: string;
40
+ agentName: string;
41
+ leadName: string;
42
+ style: "normal" | "soviet";
43
+ autoClaim: boolean;
44
+ planRequired: boolean;
45
+ systemAppend: string;
46
+ logDir: string;
47
+ extraEnv?: Record<string, string>;
48
+ }): ChildProcess {
49
+ const {
50
+ cwd,
51
+ entryPath,
52
+ sessionsDir,
53
+ teamId,
54
+ taskListId,
55
+ agentName,
56
+ leadName,
57
+ style,
58
+ autoClaim,
59
+ planRequired,
60
+ systemAppend,
61
+ logDir,
62
+ extraEnv,
63
+ } = opts;
64
+
65
+ fs.mkdirSync(logDir, { recursive: true });
66
+ fs.mkdirSync(sessionsDir, { recursive: true });
67
+
68
+ const sessionFile = path.join(sessionsDir, `${agentName}.jsonl`);
69
+ fs.closeSync(fs.openSync(sessionFile, "a"));
70
+
71
+ const logPath = path.join(logDir, `${agentName}.log`);
72
+ const out = fs.openSync(logPath, "a");
73
+ const err = fs.openSync(logPath, "a");
74
+
75
+ const args = [
76
+ "--mode",
77
+ "rpc",
78
+ "--session",
79
+ sessionFile,
80
+ "--session-dir",
81
+ sessionsDir,
82
+ "--no-extensions",
83
+ "-e",
84
+ entryPath,
85
+ "--append-system-prompt",
86
+ systemAppend,
87
+ ];
88
+
89
+ return spawn("pi", args, {
90
+ cwd,
91
+ env: {
92
+ ...process.env,
93
+ PI_TEAMS_WORKER: "1",
94
+ PI_TEAMS_TEAM_ID: teamId,
95
+ PI_TEAMS_TASK_LIST_ID: taskListId,
96
+ PI_TEAMS_AGENT_NAME: agentName,
97
+ PI_TEAMS_LEAD_NAME: leadName,
98
+ PI_TEAMS_STYLE: style,
99
+ PI_TEAMS_AUTO_CLAIM: autoClaim ? "1" : "0",
100
+ PI_TEAMS_PLAN_REQUIRED: planRequired ? "1" : "0",
101
+ ...(extraEnv ?? {}),
102
+ },
103
+ stdio: ["ignore", out, err],
104
+ });
105
+ }