@schilderlabs/pitown 0.1.2 → 0.2.6

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/dist/index.mjs CHANGED
@@ -1,42 +1,1299 @@
1
1
  #!/usr/bin/env node
2
- import { m as isDirectExecution } from "./config-Bw-mNdF5.mjs";
2
+ import { a as appendAgentMessage, c as getAgentDir, d as listAgentStates, f as readAgentMessages, i as writeTaskRecord, l as getAgentSessionsDir, m as writeAgentState, n as listTaskRecords, o as createAgentSessionRecord, p as readAgentState, r as updateTaskRecordStatus, s as createAgentState, t as createTaskRecord, u as getLatestAgentSession } from "./tasks-De4IAy3x.mjs";
3
+ import { t as runLoop } from "./loop-CocC9qO1.mjs";
4
+ import { a as runCommandSync, i as runCommandInteractive, n as assertCommandAvailable, t as isDirectExecution } from "./entrypoint-WBAQmFbT.mjs";
5
+ import { a as getRecommendedPlanDir, c as getReposRootDir, d as createRepoSlug, f as getCurrentBranch, l as getTownHomeDir, m as getRepoRoot, n as parseOptionalRepoFlag, o as getRepoArtifactsDir, p as getRepoIdentity, u as getUserConfigPath } from "./config-BG1v4iIi.mjs";
6
+ import { t as resolveRepoContext } from "./repo-context-BuA2JqPm.mjs";
7
+ import { runDoctor } from "./doctor.mjs";
3
8
  import { runTown } from "./run.mjs";
4
9
  import { showTownStatus } from "./status.mjs";
5
10
  import { watchTown } from "./watch.mjs";
11
+ import { createRequire } from "node:module";
12
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
13
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { spawn } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
17
+ import { readPiTownMayorPrompt, resolvePiTownExtensionPath } from "@schilderlabs/pitown-package";
6
18
 
19
+ //#region ../core/src/orchestration.ts
20
+ function createPiInvocationArgs(input) {
21
+ const args = [];
22
+ if (input.extensionPath) args.push("--extension", input.extensionPath);
23
+ if (input.appendedSystemPrompt) args.push("--append-system-prompt", input.appendedSystemPrompt);
24
+ if (input.sessionPath) args.push("--session", input.sessionPath);
25
+ else if (input.sessionDir) args.push("--session-dir", input.sessionDir);
26
+ else throw new Error("Pi invocation requires a session path or session directory");
27
+ if (input.prompt) args.push("-p", input.prompt);
28
+ return args;
29
+ }
30
+ function createDetachedRunnerInvocation(encodedPayload) {
31
+ if (fileURLToPath(import.meta.url).endsWith(".ts")) {
32
+ const require = createRequire(import.meta.url);
33
+ return {
34
+ command: process.execPath,
35
+ args: [
36
+ "--import",
37
+ require.resolve("tsx"),
38
+ fileURLToPath(new URL("./agent-runner.ts", import.meta.url)),
39
+ encodedPayload
40
+ ]
41
+ };
42
+ }
43
+ return {
44
+ command: process.execPath,
45
+ args: [fileURLToPath(new URL("./agent-runner.mjs", import.meta.url)), encodedPayload]
46
+ };
47
+ }
48
+ function createRolePrompt(input) {
49
+ const task = input.task ?? "pick the next bounded task from the current repo context";
50
+ switch (input.role) {
51
+ case "mayor": return [
52
+ "You are the Pi Town mayor.",
53
+ "You coordinate work for this repository and act as the primary human-facing agent.",
54
+ "",
55
+ `Repository: ${input.repoRoot}`,
56
+ `Task: ${task}`,
57
+ "Keep updates concise, choose bounded next steps, and leave a durable artifact trail."
58
+ ].join("\n");
59
+ case "reviewer": return [
60
+ "You are the Pi Town reviewer.",
61
+ "You review work for correctness, safety, and completeness.",
62
+ "",
63
+ `Repository: ${input.repoRoot}`,
64
+ `Task: ${task}`,
65
+ "Focus on validation confidence, regressions, and whether the output is ready for a human handoff."
66
+ ].join("\n");
67
+ case "docs-keeper": return [
68
+ "You are the Pi Town docs keeper.",
69
+ "You summarize outcomes, blockers, and continuity in compact factual language.",
70
+ "",
71
+ `Repository: ${input.repoRoot}`,
72
+ `Task: ${task}`,
73
+ "Keep the output concise and useful for the next run or human review."
74
+ ].join("\n");
75
+ default: return [
76
+ "You are the Pi Town worker.",
77
+ "You implement one bounded task at a time.",
78
+ "",
79
+ `Repository: ${input.repoRoot}`,
80
+ `Task: ${task}`,
81
+ "Keep scope tight, prefer explicit validations, and summarize what changed and what still needs follow-up."
82
+ ].join("\n");
83
+ }
84
+ }
85
+ function resolveAgentSession(agentId, artifactsDir) {
86
+ const state = readAgentState(artifactsDir, agentId);
87
+ if (state === null) throw new Error(`Unknown agent: ${agentId}`);
88
+ const latestSession = getLatestAgentSession(artifactsDir, agentId);
89
+ const sessionPath = state.session.sessionPath ?? latestSession.sessionPath;
90
+ const sessionId = state.session.sessionId ?? latestSession.sessionId;
91
+ const sessionDir = state.session.sessionDir ?? latestSession.sessionDir ?? getAgentSessionsDir(artifactsDir, agentId);
92
+ if (sessionPath === null) throw new Error(`Agent ${agentId} does not have a persisted Pi session yet.`);
93
+ return {
94
+ state,
95
+ session: createAgentSessionRecord({
96
+ sessionDir,
97
+ sessionId,
98
+ sessionPath,
99
+ processId: state.session.processId,
100
+ lastAttachedAt: (/* @__PURE__ */ new Date()).toISOString()
101
+ })
102
+ };
103
+ }
104
+ function queueAgentMessage(input) {
105
+ const state = readAgentState(input.artifactsDir, input.agentId);
106
+ if (state === null) throw new Error(`Unknown agent: ${input.agentId}`);
107
+ appendAgentMessage({
108
+ artifactsDir: input.artifactsDir,
109
+ agentId: input.agentId,
110
+ box: "inbox",
111
+ from: input.from,
112
+ body: input.body
113
+ });
114
+ writeAgentState(input.artifactsDir, createAgentState({
115
+ ...state,
116
+ status: state.status === "idle" ? "queued" : state.status,
117
+ lastMessage: input.body,
118
+ waitingOn: null,
119
+ blocked: false,
120
+ session: createAgentSessionRecord({
121
+ sessionDir: state.session.sessionDir ?? getAgentSessionsDir(input.artifactsDir, input.agentId),
122
+ sessionId: state.session.sessionId,
123
+ sessionPath: state.session.sessionPath,
124
+ processId: state.session.processId,
125
+ lastAttachedAt: state.session.lastAttachedAt
126
+ })
127
+ }));
128
+ }
129
+ function spawnAgentRun(options) {
130
+ const sessionDir = getAgentSessionsDir(options.artifactsDir, options.agentId);
131
+ if (readAgentState(options.artifactsDir, options.agentId) !== null) throw new Error(`Agent already exists: ${options.agentId}`);
132
+ assertCommandAvailable("pi");
133
+ const state = createAgentState({
134
+ agentId: options.agentId,
135
+ role: options.role,
136
+ status: "queued",
137
+ taskId: options.taskId ?? null,
138
+ task: options.task,
139
+ lastMessage: options.task ? `Spawned with task: ${options.task}` : `Spawned ${options.role} agent`,
140
+ session: createAgentSessionRecord({ sessionDir })
141
+ });
142
+ writeAgentState(options.artifactsDir, state);
143
+ if (options.task) appendAgentMessage({
144
+ artifactsDir: options.artifactsDir,
145
+ agentId: options.agentId,
146
+ box: "inbox",
147
+ from: "system",
148
+ body: options.task
149
+ });
150
+ writeAgentState(options.artifactsDir, createAgentState({
151
+ ...state,
152
+ status: "running",
153
+ lastMessage: options.task ? `Running ${options.role} task: ${options.task}` : `Running ${options.role} agent`
154
+ }));
155
+ const piArgs = createPiInvocationArgs({
156
+ sessionDir,
157
+ prompt: createRolePrompt({
158
+ role: options.role,
159
+ task: options.task,
160
+ repoRoot: options.repoRoot
161
+ }),
162
+ appendedSystemPrompt: options.appendedSystemPrompt,
163
+ extensionPath: options.extensionPath
164
+ });
165
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
166
+ const runner = createDetachedRunnerInvocation(Buffer.from(JSON.stringify({
167
+ repoRoot: options.repoRoot,
168
+ artifactsDir: options.artifactsDir,
169
+ agentId: options.agentId,
170
+ role: options.role,
171
+ task: options.task,
172
+ taskId: options.taskId ?? null,
173
+ sessionDir,
174
+ piArgs
175
+ }), "utf-8").toString("base64url"));
176
+ const child = spawn(runner.command, runner.args, {
177
+ cwd: options.repoRoot,
178
+ detached: true,
179
+ env: process.env,
180
+ stdio: "ignore"
181
+ });
182
+ child.unref();
183
+ if (!child.pid) throw new Error(`Failed to launch detached ${options.role} run for ${options.agentId}`);
184
+ writeFileSync(`${getAgentDir(options.artifactsDir, options.agentId)}/latest-invocation.json`, `${JSON.stringify({
185
+ command: "pi",
186
+ args: piArgs,
187
+ exitCode: null,
188
+ sessionDir,
189
+ sessionPath: null,
190
+ sessionId: null,
191
+ processId: child.pid,
192
+ startedAt
193
+ }, null, 2)}\n`, "utf-8");
194
+ return {
195
+ launch: {
196
+ processId: child.pid,
197
+ startedAt
198
+ },
199
+ latestSession: createAgentSessionRecord({ sessionDir })
200
+ };
201
+ }
202
+ function runAgentTurn(options) {
203
+ assertCommandAvailable("pi");
204
+ const resolved = resolveAgentSession(options.agentId, options.artifactsDir);
205
+ const messageSource = options.from ?? "human";
206
+ writeAgentState(options.artifactsDir, createAgentState({
207
+ ...resolved.state,
208
+ status: "running",
209
+ lastMessage: `Responding to ${messageSource}: ${options.message}`,
210
+ waitingOn: null,
211
+ blocked: false,
212
+ session: resolved.session
213
+ }));
214
+ const piArgs = options.runtimeArgs && options.runtimeArgs.length > 0 ? options.runtimeArgs : createPiInvocationArgs({
215
+ sessionPath: resolved.session.sessionPath,
216
+ prompt: options.message
217
+ });
218
+ const piResult = runCommandSync("pi", piArgs, {
219
+ cwd: options.repoRoot,
220
+ env: process.env
221
+ });
222
+ const latestSession = getLatestAgentSession(options.artifactsDir, options.agentId);
223
+ const agentArtifactsDir = getAgentDir(options.artifactsDir, options.agentId);
224
+ writeFileSync(`${agentArtifactsDir}/latest-stdout.txt`, piResult.stdout, "utf-8");
225
+ writeFileSync(`${agentArtifactsDir}/latest-stderr.txt`, piResult.stderr, "utf-8");
226
+ writeFileSync(`${agentArtifactsDir}/latest-invocation.json`, `${JSON.stringify({
227
+ command: "pi",
228
+ args: piArgs,
229
+ exitCode: piResult.exitCode,
230
+ sessionDir: latestSession.sessionDir,
231
+ sessionPath: latestSession.sessionPath,
232
+ sessionId: latestSession.sessionId
233
+ }, null, 2)}\n`, "utf-8");
234
+ const completionMessage = piResult.stdout.trim() || (piResult.exitCode === 0 ? `${resolved.state.role} turn completed` : `${resolved.state.role} turn exited with code ${piResult.exitCode}`);
235
+ appendAgentMessage({
236
+ artifactsDir: options.artifactsDir,
237
+ agentId: options.agentId,
238
+ box: "outbox",
239
+ from: options.agentId,
240
+ body: completionMessage
241
+ });
242
+ writeAgentState(options.artifactsDir, createAgentState({
243
+ ...resolved.state,
244
+ status: piResult.exitCode === 0 ? "idle" : "blocked",
245
+ lastMessage: completionMessage,
246
+ waitingOn: piResult.exitCode === 0 ? null : "human-or-follow-up-run",
247
+ blocked: piResult.exitCode !== 0,
248
+ session: createAgentSessionRecord({
249
+ sessionDir: latestSession.sessionDir,
250
+ sessionId: latestSession.sessionId,
251
+ sessionPath: latestSession.sessionPath,
252
+ processId: null
253
+ })
254
+ }));
255
+ return {
256
+ piResult,
257
+ latestSession,
258
+ completionMessage
259
+ };
260
+ }
261
+ function delegateTask(options) {
262
+ if (readAgentState(options.artifactsDir, options.fromAgentId) === null) throw new Error(`Unknown delegating agent: ${options.fromAgentId}`);
263
+ const agentId = options.agentId ?? `${options.role}-${Date.now()}`;
264
+ const task = createTaskRecord({
265
+ taskId: `task-${Date.now()}`,
266
+ title: options.task,
267
+ status: "queued",
268
+ role: options.role,
269
+ assignedAgentId: agentId,
270
+ createdBy: options.fromAgentId
271
+ });
272
+ writeTaskRecord(options.artifactsDir, task);
273
+ appendAgentMessage({
274
+ artifactsDir: options.artifactsDir,
275
+ agentId: options.fromAgentId,
276
+ box: "outbox",
277
+ from: options.fromAgentId,
278
+ body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
279
+ });
280
+ const { launch, latestSession } = spawnAgentRun({
281
+ repoRoot: options.repoRoot,
282
+ artifactsDir: options.artifactsDir,
283
+ role: options.role,
284
+ agentId,
285
+ appendedSystemPrompt: options.appendedSystemPrompt,
286
+ extensionPath: options.extensionPath,
287
+ task: options.task,
288
+ taskId: task.taskId
289
+ });
290
+ appendAgentMessage({
291
+ artifactsDir: options.artifactsDir,
292
+ agentId,
293
+ box: "inbox",
294
+ from: options.fromAgentId,
295
+ body: `Delegated by ${options.fromAgentId} as ${task.taskId}: ${options.task}`
296
+ });
297
+ writeTaskRecord(options.artifactsDir, {
298
+ ...task,
299
+ status: "running",
300
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
301
+ });
302
+ return {
303
+ task: {
304
+ ...task,
305
+ status: "running",
306
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
307
+ },
308
+ agentId,
309
+ launch,
310
+ latestSession
311
+ };
312
+ }
313
+
314
+ //#endregion
315
+ //#region ../core/src/stop.ts
316
+ const DEFAULT_GRACE_MS = 750;
317
+ function sleepMs(ms) {
318
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
319
+ }
320
+ function getLocksDir() {
321
+ return join(homedir(), ".pi-town", "locks");
322
+ }
323
+ function processAlive(pid) {
324
+ if (!Number.isFinite(pid) || pid <= 0) return false;
325
+ try {
326
+ process.kill(pid, 0);
327
+ return true;
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+ function terminateProcess(pid, options) {
333
+ if (!processAlive(pid)) return {
334
+ signal: null,
335
+ exited: true
336
+ };
337
+ try {
338
+ process.kill(pid, "SIGTERM");
339
+ } catch {
340
+ return {
341
+ signal: null,
342
+ exited: !processAlive(pid)
343
+ };
344
+ }
345
+ const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
346
+ const deadline = Date.now() + graceMs;
347
+ while (Date.now() < deadline) {
348
+ if (!processAlive(pid)) return {
349
+ signal: "SIGTERM",
350
+ exited: true
351
+ };
352
+ sleepMs(25);
353
+ }
354
+ if (!options.force) return {
355
+ signal: "SIGTERM",
356
+ exited: !processAlive(pid)
357
+ };
358
+ try {
359
+ process.kill(pid, "SIGKILL");
360
+ } catch {
361
+ return {
362
+ signal: "SIGTERM",
363
+ exited: !processAlive(pid)
364
+ };
365
+ }
366
+ return {
367
+ signal: "SIGKILL",
368
+ exited: !processAlive(pid)
369
+ };
370
+ }
371
+ function readLease(path) {
372
+ try {
373
+ return {
374
+ ...JSON.parse(readFileSync(path, "utf-8")),
375
+ path
376
+ };
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+ function createStopMessage(options) {
382
+ if (options.reason) return options.reason;
383
+ if (options.actorId) return `Stopped by ${options.actorId}`;
384
+ return "Stopped by operator";
385
+ }
386
+ function createStopMessageInput(options) {
387
+ return {
388
+ ...options.actorId === void 0 ? {} : { actorId: options.actorId },
389
+ ...options.reason === void 0 ? {} : { reason: options.reason }
390
+ };
391
+ }
392
+ function createTerminateOptions(options) {
393
+ return {
394
+ ...options.force === void 0 ? {} : { force: options.force },
395
+ ...options.graceMs === void 0 ? {} : { graceMs: options.graceMs }
396
+ };
397
+ }
398
+ function listRepoLeases(repoId) {
399
+ let entries;
400
+ try {
401
+ entries = readdirSync(getLocksDir());
402
+ } catch {
403
+ return [];
404
+ }
405
+ return entries.filter((entry) => entry.endsWith(".json")).map((entry) => readLease(join(getLocksDir(), entry))).filter((record) => record !== null).filter((record) => repoId === void 0 || repoId === null || record.repoId === repoId);
406
+ }
407
+ function stopRepoLeases(options) {
408
+ const results = listRepoLeases(options.repoId).map((lease) => {
409
+ const termination = terminateProcess(lease.pid, options);
410
+ if (termination.exited) rmSync(lease.path, { force: true });
411
+ return {
412
+ path: lease.path,
413
+ runId: lease.runId,
414
+ repoId: lease.repoId,
415
+ branch: lease.branch,
416
+ processId: lease.pid,
417
+ signal: termination.signal,
418
+ exited: termination.exited
419
+ };
420
+ });
421
+ return {
422
+ results,
423
+ signaledProcesses: results.filter((result) => result.signal !== null).length
424
+ };
425
+ }
426
+ function stopManagedAgents(options) {
427
+ const reason = createStopMessage(createStopMessageInput(options));
428
+ const excluded = new Set(options.excludeAgentIds ?? []);
429
+ const results = listAgentStates(options.artifactsDir).filter((agent) => {
430
+ if (excluded.has(agent.agentId)) return false;
431
+ if (options.agentId && agent.agentId !== options.agentId) return false;
432
+ return ![
433
+ "completed",
434
+ "failed",
435
+ "stopped"
436
+ ].includes(agent.status);
437
+ }).map((state) => {
438
+ const processId = state.session.processId;
439
+ const termination = processId === null ? {
440
+ signal: null,
441
+ exited: true
442
+ } : terminateProcess(processId, createTerminateOptions(options));
443
+ if (state.taskId) updateTaskRecordStatus(options.artifactsDir, state.taskId, "aborted");
444
+ appendAgentMessage({
445
+ artifactsDir: options.artifactsDir,
446
+ agentId: state.agentId,
447
+ box: "outbox",
448
+ from: options.actorId ?? "system",
449
+ body: reason
450
+ });
451
+ writeAgentState(options.artifactsDir, createAgentState({
452
+ ...state,
453
+ status: "stopped",
454
+ lastMessage: reason,
455
+ waitingOn: "stopped",
456
+ blocked: true,
457
+ session: createAgentSessionRecord({
458
+ sessionDir: state.session.sessionDir,
459
+ sessionId: state.session.sessionId,
460
+ sessionPath: state.session.sessionPath,
461
+ processId: null,
462
+ lastAttachedAt: state.session.lastAttachedAt
463
+ })
464
+ }));
465
+ return {
466
+ agentId: state.agentId,
467
+ previousStatus: state.status,
468
+ nextStatus: "stopped",
469
+ processId,
470
+ signal: termination.signal,
471
+ exited: termination.exited
472
+ };
473
+ });
474
+ return {
475
+ results,
476
+ stoppedAgents: results.length,
477
+ signaledProcesses: results.filter((result) => result.signal !== null).length
478
+ };
479
+ }
480
+
481
+ //#endregion
482
+ //#region src/agent-id.ts
483
+ function normalizeAgentId(agentId) {
484
+ return agentId;
485
+ }
486
+
487
+ //#endregion
488
+ //#region src/pi-runtime.ts
489
+ function isMayorAgent(agentId) {
490
+ return agentId === "mayor";
491
+ }
492
+ function createPiTownRuntimeArgs(options) {
493
+ const args = ["--extension", resolvePiTownExtensionPath()];
494
+ if (isMayorAgent(options.agentId)) args.push("--append-system-prompt", readPiTownMayorPrompt());
495
+ if (options.sessionPath) args.push("--session", options.sessionPath);
496
+ else if (options.sessionDir) args.push("--session-dir", options.sessionDir);
497
+ else throw new Error("Pi Town runtime requires either a session path or a session directory");
498
+ if (options.prompt) args.push("-p", options.prompt);
499
+ if (options.message) args.push(options.message);
500
+ return args;
501
+ }
502
+
503
+ //#endregion
504
+ //#region src/attach.ts
505
+ function attachTownAgent(argv = process.argv.slice(2)) {
506
+ const repo = resolveRepoContext(argv);
507
+ const [agentArg] = repo.rest;
508
+ if (!agentArg) throw new Error("Usage: pitown attach [--repo <path>] <agent>");
509
+ const agentId = normalizeAgentId(agentArg);
510
+ assertCommandAvailable("pi");
511
+ const resolved = resolveAgentSession(agentId, repo.artifactsDir);
512
+ writeAgentState(repo.artifactsDir, {
513
+ ...resolved.state,
514
+ session: resolved.session
515
+ });
516
+ console.log("[pitown] attach");
517
+ console.log(`- repo root: ${repo.repoRoot}`);
518
+ console.log(`- agent: ${agentId}`);
519
+ console.log(`- session: ${resolved.session.sessionPath}`);
520
+ const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
521
+ agentId,
522
+ sessionPath: resolved.session.sessionPath
523
+ }), {
524
+ cwd: repo.repoRoot,
525
+ env: process.env
526
+ });
527
+ if (exitCode !== 0) process.exitCode = exitCode;
528
+ }
529
+
530
+ //#endregion
531
+ //#region src/board.ts
532
+ function truncate(text, max) {
533
+ if (!text) return "—";
534
+ const single = text.replace(/\n/g, " ").trim();
535
+ if (single.length <= max) return single;
536
+ return `${single.slice(0, max - 1)}…`;
537
+ }
538
+ function showTownBoard(argv = process.argv.slice(2)) {
539
+ const repo = resolveRepoContext(argv);
540
+ const agents = listAgentStates(repo.artifactsDir);
541
+ const tasks = listTaskRecords(repo.artifactsDir);
542
+ const repoName = basename(repo.repoRoot);
543
+ const branch = getCurrentBranch(repo.repoRoot);
544
+ const branchLabel = branch ? ` (${branch})` : "";
545
+ const mayor = agents.find((a) => a.agentId === "mayor");
546
+ const workers = agents.filter((a) => a.agentId !== "mayor");
547
+ const workersByStatus = (status) => workers.filter((a) => a.status === status).length;
548
+ console.log(`[pitown] board — ${repoName}${branchLabel}`);
549
+ if (mayor) {
550
+ const spawned = workers.length;
551
+ const running = workersByStatus("running") + workersByStatus("starting");
552
+ const completed = workersByStatus("completed") + workersByStatus("idle");
553
+ const blocked = workersByStatus("blocked") + workersByStatus("failed");
554
+ const parts = [`${spawned} spawned`];
555
+ if (running > 0) parts.push(`${running} running`);
556
+ if (completed > 0) parts.push(`${completed} done`);
557
+ if (blocked > 0) parts.push(`${blocked} blocked`);
558
+ console.log(`Mayor workers: ${parts.join(", ")}`);
559
+ }
560
+ console.log("");
561
+ console.log("Agents:");
562
+ if (agents.length === 0) console.log(" (no agents)");
563
+ else for (const agent of agents) {
564
+ const id = agent.agentId.padEnd(14);
565
+ const role = agent.role.padEnd(10);
566
+ const status = agent.status.padEnd(10);
567
+ const task = truncate(agent.task, 60);
568
+ const msg = agent.lastMessage ? ` | ${truncate(agent.lastMessage, 40)}` : "";
569
+ const waiting = agent.waitingOn ? ` | waiting on: ${agent.waitingOn}` : "";
570
+ console.log(` ${id}${role}${status}${task}${msg}${waiting}`);
571
+ }
572
+ console.log("");
573
+ console.log("Tasks:");
574
+ if (tasks.length === 0) console.log(" (no tasks)");
575
+ else for (const task of tasks) {
576
+ const id = task.taskId.padEnd(14);
577
+ const status = task.status.padEnd(12);
578
+ const assignee = (task.assignedAgentId ?? "—").padEnd(14);
579
+ const title = truncate(task.title, 60);
580
+ console.log(` ${id}${status}${assignee}${title}`);
581
+ }
582
+ const metricsPath = join(repo.artifactsDir, "latest", "metrics.json");
583
+ if (existsSync(metricsPath)) try {
584
+ const metrics = JSON.parse(readFileSync(metricsPath, "utf-8"));
585
+ console.log("");
586
+ console.log("Metrics (latest run):");
587
+ console.log(` Interrupt Rate: ${fmt(metrics.interruptRate)}`);
588
+ console.log(` Autonomous Completion Rate: ${fmt(metrics.autonomousCompletionRate)}`);
589
+ console.log(` Context Coverage Score: ${fmt(metrics.contextCoverageScore)}`);
590
+ console.log(` MTTC: ${metrics.meanTimeToCorrectHours != null ? `${metrics.meanTimeToCorrectHours}h` : "—"}`);
591
+ console.log(` Feedback-to-Demo: ${metrics.feedbackToDemoCycleTimeHours != null ? `${metrics.feedbackToDemoCycleTimeHours}h` : "—"}`);
592
+ } catch {}
593
+ }
594
+ function fmt(value) {
595
+ if (value == null) return "—";
596
+ return String(value);
597
+ }
598
+
599
+ //#endregion
600
+ //#region src/continue.ts
601
+ function continueTownAgent(argv = process.argv.slice(2)) {
602
+ const repo = resolveRepoContext(argv);
603
+ const [agentArg, ...messageParts] = repo.rest;
604
+ if (!agentArg) throw new Error("Usage: pitown continue [--repo <path>] <agent> [\"message\"]");
605
+ const agentId = normalizeAgentId(agentArg);
606
+ assertCommandAvailable("pi");
607
+ const resolved = resolveAgentSession(agentId, repo.artifactsDir);
608
+ writeAgentState(repo.artifactsDir, {
609
+ ...resolved.state,
610
+ session: resolved.session
611
+ });
612
+ const message = messageParts.join(" ").trim();
613
+ const args = createPiTownRuntimeArgs({
614
+ agentId,
615
+ sessionPath: resolved.session.sessionPath,
616
+ message: message || null
617
+ });
618
+ console.log("[pitown] continue");
619
+ console.log(`- repo root: ${repo.repoRoot}`);
620
+ console.log(`- agent: ${agentId}`);
621
+ console.log(`- session: ${resolved.session.sessionPath}`);
622
+ if (message) console.log(`- message: ${message}`);
623
+ const exitCode = runCommandInteractive("pi", args, {
624
+ cwd: repo.repoRoot,
625
+ env: process.env
626
+ });
627
+ if (exitCode !== 0) process.exitCode = exitCode;
628
+ }
629
+
630
+ //#endregion
631
+ //#region src/delegate.ts
632
+ function parseDelegateFlags(argv) {
633
+ let from = "mayor";
634
+ let role = "worker";
635
+ let agentId = null;
636
+ let task = null;
637
+ for (let index = 0; index < argv.length; index += 1) {
638
+ const arg = argv[index];
639
+ if (arg === void 0) continue;
640
+ if (arg.startsWith("--from=")) {
641
+ from = arg.slice(7);
642
+ continue;
643
+ }
644
+ if (arg === "--from") {
645
+ from = argv[index + 1] ?? from;
646
+ index += 1;
647
+ continue;
648
+ }
649
+ if (arg.startsWith("--role=")) {
650
+ role = arg.slice(7);
651
+ continue;
652
+ }
653
+ if (arg === "--role") {
654
+ role = argv[index + 1] ?? role;
655
+ index += 1;
656
+ continue;
657
+ }
658
+ if (arg.startsWith("--agent=")) {
659
+ agentId = arg.slice(8);
660
+ continue;
661
+ }
662
+ if (arg === "--agent") {
663
+ agentId = argv[index + 1] ?? null;
664
+ index += 1;
665
+ continue;
666
+ }
667
+ if (arg.startsWith("--task=")) {
668
+ task = arg.slice(7);
669
+ continue;
670
+ }
671
+ if (arg === "--task") {
672
+ task = argv[index + 1] ?? null;
673
+ index += 1;
674
+ continue;
675
+ }
676
+ throw new Error(`Unknown argument: ${arg}`);
677
+ }
678
+ if (!task) throw new Error("Usage: pitown delegate [--repo <path>] [--from <agent>] [--role <role>] [--agent <id>] --task <text>");
679
+ return {
680
+ from,
681
+ role,
682
+ agentId,
683
+ task
684
+ };
685
+ }
686
+ function delegateTownTask(argv = process.argv.slice(2)) {
687
+ const repo = resolveRepoContext(argv);
688
+ const flags = parseDelegateFlags(repo.rest);
689
+ const fromAgentId = normalizeAgentId(flags.from);
690
+ if (readAgentState(repo.artifactsDir, fromAgentId) === null) throw new Error(`Unknown delegating agent: ${fromAgentId}`);
691
+ const { agentId, latestSession, launch, task } = delegateTask({
692
+ repoRoot: repo.repoRoot,
693
+ artifactsDir: repo.artifactsDir,
694
+ fromAgentId,
695
+ role: flags.role,
696
+ agentId: flags.agentId,
697
+ task: flags.task,
698
+ extensionPath: resolvePiTownExtensionPath()
699
+ });
700
+ console.log("[pitown] delegate");
701
+ console.log(`- repo root: ${repo.repoRoot}`);
702
+ console.log(`- from: ${fromAgentId}`);
703
+ console.log(`- task id: ${task.taskId}`);
704
+ console.log(`- agent: ${agentId}`);
705
+ console.log(`- role: ${flags.role}`);
706
+ console.log(`- status: ${task.status}`);
707
+ console.log(`- launch pid: ${launch.processId}`);
708
+ if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
709
+ else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
710
+ }
711
+
712
+ //#endregion
713
+ //#region src/loop-config.ts
714
+ const DEFAULT_GOAL = "continue from current scaffold state";
715
+ const DEFAULT_MAX_ITERATIONS = 10;
716
+ const DEFAULT_MAX_TIME_MINUTES = 60;
717
+ function expandHome(value) {
718
+ if (value === "~") return homedir();
719
+ if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
720
+ return value;
721
+ }
722
+ function resolvePathValue(value, baseDir) {
723
+ if (!value) return void 0;
724
+ const expanded = expandHome(value);
725
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded);
726
+ }
727
+ function parseLoopCliFlags(argv) {
728
+ const flags = { noStopOnFailure: false };
729
+ for (let index = 0; index < argv.length; index += 1) {
730
+ const arg = argv[index];
731
+ if (arg === void 0) continue;
732
+ if (arg.startsWith("--repo=")) {
733
+ flags.repo = arg.slice(7);
734
+ continue;
735
+ }
736
+ if (arg === "--repo") {
737
+ const value = argv[index + 1];
738
+ if (!value) throw new Error("Missing value for --repo");
739
+ flags.repo = value;
740
+ index += 1;
741
+ continue;
742
+ }
743
+ if (arg.startsWith("--plan=")) {
744
+ flags.plan = arg.slice(7);
745
+ continue;
746
+ }
747
+ if (arg === "--plan") {
748
+ const value = argv[index + 1];
749
+ if (!value) throw new Error("Missing value for --plan");
750
+ flags.plan = value;
751
+ index += 1;
752
+ continue;
753
+ }
754
+ if (arg.startsWith("--goal=")) {
755
+ flags.goal = arg.slice(7);
756
+ continue;
757
+ }
758
+ if (arg === "--goal") {
759
+ const value = argv[index + 1];
760
+ if (!value) throw new Error("Missing value for --goal");
761
+ flags.goal = value;
762
+ index += 1;
763
+ continue;
764
+ }
765
+ if (arg.startsWith("--max-iterations=")) {
766
+ flags.maxIterations = Number.parseInt(arg.slice(17), 10);
767
+ continue;
768
+ }
769
+ if (arg === "--max-iterations") {
770
+ const value = argv[index + 1];
771
+ if (!value) throw new Error("Missing value for --max-iterations");
772
+ flags.maxIterations = Number.parseInt(value, 10);
773
+ index += 1;
774
+ continue;
775
+ }
776
+ if (arg.startsWith("--max-time=")) {
777
+ flags.maxTime = Number.parseInt(arg.slice(11), 10);
778
+ continue;
779
+ }
780
+ if (arg === "--max-time") {
781
+ const value = argv[index + 1];
782
+ if (!value) throw new Error("Missing value for --max-time");
783
+ flags.maxTime = Number.parseInt(value, 10);
784
+ index += 1;
785
+ continue;
786
+ }
787
+ if (arg === "--no-stop-on-failure") {
788
+ flags.noStopOnFailure = true;
789
+ continue;
790
+ }
791
+ throw new Error(`Unknown argument: ${arg}`);
792
+ }
793
+ return flags;
794
+ }
795
+ function loadUserConfig() {
796
+ const configPath = getUserConfigPath();
797
+ if (!existsSync(configPath)) return {};
798
+ return JSON.parse(readFileSync(configPath, "utf-8"));
799
+ }
800
+ function resolveLoopConfig(argv) {
801
+ const flags = parseLoopCliFlags(argv);
802
+ const configPath = getUserConfigPath();
803
+ const userConfig = loadUserConfig();
804
+ const configDir = dirname(configPath);
805
+ return {
806
+ repo: resolvePathValue(flags.repo, process.cwd()) ?? resolvePathValue(userConfig.repo, configDir) ?? resolve(process.cwd()),
807
+ plan: resolvePathValue(flags.plan, process.cwd()) ?? resolvePathValue(userConfig.plan, configDir) ?? null,
808
+ goal: flags.goal ?? userConfig.goal ?? DEFAULT_GOAL,
809
+ maxIterations: flags.maxIterations ?? DEFAULT_MAX_ITERATIONS,
810
+ maxTimeMinutes: flags.maxTime ?? DEFAULT_MAX_TIME_MINUTES,
811
+ stopOnPiFailure: !flags.noStopOnFailure
812
+ };
813
+ }
814
+
815
+ //#endregion
816
+ //#region src/loop.ts
817
+ function assertDirectory(path, label) {
818
+ if (!existsSync(path)) throw new Error(`${label} does not exist: ${path}`);
819
+ if (!statSync(path).isDirectory()) throw new Error(`${label} is not a directory: ${path}`);
820
+ }
821
+ function formatMs(ms) {
822
+ if (ms < 1e3) return `${ms}ms`;
823
+ return `${(ms / 1e3).toFixed(1)}s`;
824
+ }
825
+ function loopTown(argv = process.argv.slice(2)) {
826
+ const config = resolveLoopConfig(argv);
827
+ assertDirectory(config.repo, "Target repo");
828
+ if (config.plan) assertDirectory(config.plan, "Plan path");
829
+ mkdirSync(getTownHomeDir(), { recursive: true });
830
+ const repoRoot = getRepoRoot(config.repo);
831
+ const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
832
+ const recommendedPlanDir = config.plan ? null : getRecommendedPlanDir(repoSlug);
833
+ const artifactsDir = getRepoArtifactsDir(repoSlug);
834
+ console.log(`[pitown-loop] starting loop (max ${config.maxIterations} iterations, ${config.maxTimeMinutes}min wall time)`);
835
+ const result = runLoop({
836
+ runOptions: {
837
+ artifactsDir,
838
+ cwd: repoRoot,
839
+ goal: config.goal,
840
+ mode: "single-pi",
841
+ planPath: config.plan,
842
+ recommendedPlanDir,
843
+ appendedSystemPrompt: readPiTownMayorPrompt(),
844
+ extensionPath: resolvePiTownExtensionPath()
845
+ },
846
+ maxIterations: config.maxIterations,
847
+ maxWallTimeMs: config.maxTimeMinutes * 6e4,
848
+ stopOnPiFailure: config.stopOnPiFailure,
849
+ onIterationComplete(iteration) {
850
+ const board = iteration.boardSnapshot;
851
+ const taskSummary = board.tasks.length > 0 ? `${board.tasks.length} tasks (${board.tasks.filter((t) => t.status === "completed").length} completed, ${board.tasks.filter((t) => t.status === "running").length} running)` : "no tasks tracked";
852
+ const mayorStatus = board.agents.find((a) => a.agentId === "mayor")?.status ?? "unknown";
853
+ console.log(`[pitown-loop] iteration ${iteration.iteration}/${config.maxIterations} completed (${formatMs(iteration.elapsedMs)})`);
854
+ console.log(` - pi exit code: ${iteration.controllerResult.piInvocation.exitCode}`);
855
+ console.log(` - run: ${iteration.controllerResult.runId}`);
856
+ console.log(` - board: ${taskSummary}, mayor ${mayorStatus}`);
857
+ console.log(` - metrics: interrupt rate ${iteration.metrics.interruptRate}, autonomous completion ${iteration.metrics.autonomousCompletionRate}`);
858
+ if (iteration.stopReason) console.log(` - stopping: ${iteration.stopReason}`);
859
+ else console.log(` - continuing: ${iteration.continueReason}`);
860
+ }
861
+ });
862
+ console.log(`[pitown-loop] stopped after ${result.totalIterations} iteration${result.totalIterations === 1 ? "" : "s"} (${formatMs(result.totalElapsedMs)} total)`);
863
+ console.log(` - reason: ${result.stopReason}`);
864
+ console.log(` - aggregate metrics: interrupt rate ${result.aggregateMetrics.interruptRate}, autonomous completion ${result.aggregateMetrics.autonomousCompletionRate}`);
865
+ return result;
866
+ }
867
+
868
+ //#endregion
869
+ //#region src/mayor.ts
870
+ function startFreshMayorSession(repoRoot, artifactsDir) {
871
+ const sessionDir = getAgentSessionsDir(artifactsDir, "mayor");
872
+ writeAgentState(artifactsDir, createAgentState({
873
+ agentId: "mayor",
874
+ role: "mayor",
875
+ status: "running",
876
+ task: "open the mayor session and plan the next steps for this repository",
877
+ lastMessage: "Mayor session opened",
878
+ session: createAgentSessionRecord({ sessionDir })
879
+ }));
880
+ console.log("[pitown] mayor");
881
+ console.log(`- repo root: ${repoRoot}`);
882
+ console.log("- starting a new mayor session");
883
+ const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
884
+ agentId: "mayor",
885
+ sessionDir
886
+ }), {
887
+ cwd: repoRoot,
888
+ env: process.env
889
+ });
890
+ const latestSession = getLatestAgentSession(artifactsDir, "mayor");
891
+ const previousState = readAgentState(artifactsDir, "mayor");
892
+ if (previousState !== null) writeAgentState(artifactsDir, createAgentState({
893
+ ...previousState,
894
+ status: exitCode === 0 ? "idle" : "blocked",
895
+ lastMessage: exitCode === 0 ? "Mayor session closed" : `Mayor session exited with code ${exitCode}`,
896
+ blocked: exitCode !== 0,
897
+ waitingOn: exitCode === 0 ? null : "human-or-follow-up-run",
898
+ session: createAgentSessionRecord({
899
+ sessionDir: latestSession.sessionDir,
900
+ sessionId: latestSession.sessionId,
901
+ sessionPath: latestSession.sessionPath
902
+ })
903
+ }));
904
+ if (exitCode !== 0) process.exitCode = exitCode;
905
+ }
906
+ function openTownMayor(argv = process.argv.slice(2)) {
907
+ const repo = resolveRepoContext(argv);
908
+ const message = repo.rest.join(" ").trim();
909
+ if (readAgentState(repo.artifactsDir, "mayor") === null) {
910
+ assertCommandAvailable("pi");
911
+ if (message) {
912
+ runTown([
913
+ "--repo",
914
+ repo.repoRoot,
915
+ "--goal",
916
+ message
917
+ ]);
918
+ return;
919
+ }
920
+ startFreshMayorSession(repo.repoRoot, repo.artifactsDir);
921
+ return;
922
+ }
923
+ if (message) {
924
+ continueTownAgent([
925
+ "--repo",
926
+ repo.repoRoot,
927
+ "mayor",
928
+ message
929
+ ]);
930
+ return;
931
+ }
932
+ attachTownAgent([
933
+ "--repo",
934
+ repo.repoRoot,
935
+ "mayor"
936
+ ]);
937
+ }
938
+
939
+ //#endregion
940
+ //#region src/msg.ts
941
+ function messageTownAgent(argv = process.argv.slice(2)) {
942
+ const repo = resolveRepoContext(argv);
943
+ const [agentArg, ...messageParts] = repo.rest;
944
+ if (!agentArg || messageParts.length === 0) throw new Error("Usage: pitown msg [--repo <path>] <agent> \"message\"");
945
+ const agentId = normalizeAgentId(agentArg);
946
+ const state = readAgentState(repo.artifactsDir, agentId);
947
+ if (state === null) throw new Error(`Unknown agent: ${agentId}`);
948
+ const body = messageParts.join(" ").trim();
949
+ queueAgentMessage({
950
+ artifactsDir: repo.artifactsDir,
951
+ agentId,
952
+ from: "human",
953
+ body
954
+ });
955
+ const deliveredResult = state.role === "mayor" ? runAgentTurn({
956
+ repoRoot: repo.repoRoot,
957
+ artifactsDir: repo.artifactsDir,
958
+ agentId,
959
+ message: body,
960
+ from: "human",
961
+ runtimeArgs: createPiTownRuntimeArgs({
962
+ agentId,
963
+ sessionPath: state.session.sessionPath,
964
+ prompt: body
965
+ })
966
+ }) : null;
967
+ console.log("[pitown] msg");
968
+ console.log(`- repo root: ${repo.repoRoot}`);
969
+ console.log(`- agent: ${agentId}`);
970
+ console.log(`- queued message: ${body}`);
971
+ if (deliveredResult) {
972
+ console.log(`- delivered to session: ${deliveredResult.latestSession.sessionPath}`);
973
+ console.log(`- mayor response: ${deliveredResult.completionMessage}`);
974
+ }
975
+ }
976
+
977
+ //#endregion
978
+ //#region src/peek.ts
979
+ function printMessages(label, lines) {
980
+ console.log(`- ${label}:`);
981
+ if (lines.length === 0) {
982
+ console.log(" (empty)");
983
+ return;
984
+ }
985
+ for (const line of lines) console.log(` ${line.createdAt} ${line.from}: ${line.body}`);
986
+ }
987
+ function peekTownAgent(argv = process.argv.slice(2)) {
988
+ const repo = resolveRepoContext(argv);
989
+ const [agentArg] = repo.rest;
990
+ const agentId = normalizeAgentId(agentArg ?? "mayor");
991
+ const state = readAgentState(repo.artifactsDir, agentId);
992
+ if (state === null) throw new Error(`Unknown agent: ${agentId}`);
993
+ console.log("[pitown] peek");
994
+ console.log(`- repo root: ${repo.repoRoot}`);
995
+ console.log(`- agent: ${state.agentId}`);
996
+ console.log(`- role: ${state.role}`);
997
+ console.log(`- status: ${state.status}`);
998
+ if (state.taskId) console.log(`- task id: ${state.taskId}`);
999
+ if (state.task) console.log(`- task: ${state.task}`);
1000
+ if (state.branch) console.log(`- branch: ${state.branch}`);
1001
+ console.log(`- blocked: ${state.blocked}`);
1002
+ if (state.waitingOn) console.log(`- waiting on: ${state.waitingOn}`);
1003
+ if (state.lastMessage) console.log(`- last message: ${state.lastMessage}`);
1004
+ if (state.session.sessionId) console.log(`- session id: ${state.session.sessionId}`);
1005
+ if (state.session.sessionPath) console.log(`- session path: ${state.session.sessionPath}`);
1006
+ console.log(`- updated at: ${state.updatedAt}`);
1007
+ printMessages("recent inbox", readAgentMessages(repo.artifactsDir, agentId, "inbox").slice(-5));
1008
+ printMessages("recent outbox", readAgentMessages(repo.artifactsDir, agentId, "outbox").slice(-5));
1009
+ }
1010
+
1011
+ //#endregion
1012
+ //#region src/spawn.ts
1013
+ function parseSpawnFlags(argv) {
1014
+ let role = null;
1015
+ let agentId = null;
1016
+ let task = null;
1017
+ for (let index = 0; index < argv.length; index += 1) {
1018
+ const arg = argv[index];
1019
+ if (arg === void 0) continue;
1020
+ if (arg.startsWith("--role=")) {
1021
+ role = arg.slice(7);
1022
+ continue;
1023
+ }
1024
+ if (arg === "--role") {
1025
+ role = argv[index + 1] ?? null;
1026
+ index += 1;
1027
+ continue;
1028
+ }
1029
+ if (arg.startsWith("--agent=")) {
1030
+ agentId = arg.slice(8);
1031
+ continue;
1032
+ }
1033
+ if (arg === "--agent") {
1034
+ agentId = argv[index + 1] ?? null;
1035
+ index += 1;
1036
+ continue;
1037
+ }
1038
+ if (arg.startsWith("--task=")) {
1039
+ task = arg.slice(7);
1040
+ continue;
1041
+ }
1042
+ if (arg === "--task") {
1043
+ task = argv[index + 1] ?? null;
1044
+ index += 1;
1045
+ continue;
1046
+ }
1047
+ throw new Error(`Unknown argument: ${arg}`);
1048
+ }
1049
+ if (!role) throw new Error("Usage: pitown spawn [--repo <path>] --role <role> [--agent <id>] [--task <text>]");
1050
+ return {
1051
+ role,
1052
+ agentId,
1053
+ task
1054
+ };
1055
+ }
1056
+ function spawnAgent(options) {
1057
+ return spawnAgentRun(options);
1058
+ }
1059
+ function spawnTownAgent(argv = process.argv.slice(2)) {
1060
+ const repo = resolveRepoContext(argv);
1061
+ const flags = parseSpawnFlags(repo.rest);
1062
+ const agentId = flags.agentId ?? `${flags.role}-${Date.now()}`;
1063
+ const task = flags.task;
1064
+ const { launch, latestSession } = spawnAgent({
1065
+ repoRoot: repo.repoRoot,
1066
+ artifactsDir: repo.artifactsDir,
1067
+ role: flags.role,
1068
+ agentId,
1069
+ task,
1070
+ extensionPath: resolvePiTownExtensionPath()
1071
+ });
1072
+ console.log("[pitown] spawn");
1073
+ console.log(`- repo root: ${repo.repoRoot}`);
1074
+ console.log(`- agent: ${agentId}`);
1075
+ console.log(`- role: ${flags.role}`);
1076
+ console.log(`- status: running`);
1077
+ console.log(`- launch pid: ${launch.processId}`);
1078
+ if (task) console.log(`- task: ${task}`);
1079
+ if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
1080
+ else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
1081
+ }
1082
+
1083
+ //#endregion
1084
+ //#region src/stop.ts
1085
+ function parseStopFlags(argv) {
1086
+ let all = false;
1087
+ let agentId = null;
1088
+ let force = false;
1089
+ for (let index = 0; index < argv.length; index += 1) {
1090
+ const arg = argv[index];
1091
+ if (arg === void 0) continue;
1092
+ if (arg === "--all") {
1093
+ all = true;
1094
+ continue;
1095
+ }
1096
+ if (arg === "--force") {
1097
+ force = true;
1098
+ continue;
1099
+ }
1100
+ if (arg.startsWith("--agent=")) {
1101
+ agentId = normalizeAgentId(arg.slice(8));
1102
+ continue;
1103
+ }
1104
+ if (arg === "--agent") {
1105
+ const value = argv[index + 1];
1106
+ if (!value) throw new Error("Missing value for --agent");
1107
+ agentId = normalizeAgentId(value);
1108
+ index += 1;
1109
+ continue;
1110
+ }
1111
+ throw new Error(`Unknown argument: ${arg}`);
1112
+ }
1113
+ return {
1114
+ all,
1115
+ agentId,
1116
+ force
1117
+ };
1118
+ }
1119
+ function stopRepo(repoRoot, artifactsDir, flags) {
1120
+ const repoId = getRepoIdentity(repoRoot);
1121
+ const leaseResult = flags.agentId ? { signaledProcesses: 0 } : stopRepoLeases({
1122
+ repoId,
1123
+ force: flags.force
1124
+ });
1125
+ const agentResult = stopManagedAgents({
1126
+ artifactsDir,
1127
+ agentId: flags.agentId,
1128
+ actorId: "human",
1129
+ reason: flags.agentId ? `Stopped ${flags.agentId} via pitown stop` : "Stopped via pitown stop",
1130
+ force: flags.force
1131
+ });
1132
+ return {
1133
+ repoLabel: repoRoot,
1134
+ stoppedAgents: agentResult.stoppedAgents,
1135
+ signaledAgentProcesses: agentResult.signaledProcesses,
1136
+ signaledLeaseProcesses: leaseResult.signaledProcesses
1137
+ };
1138
+ }
1139
+ function listTrackedArtifactsDirs() {
1140
+ const reposRoot = getReposRootDir();
1141
+ if (!existsSync(reposRoot)) return [];
1142
+ return readdirSync(reposRoot).map((entry) => getRepoArtifactsDir(entry)).filter((path) => existsSync(path));
1143
+ }
1144
+ function stopTown(argv = process.argv.slice(2)) {
1145
+ const { repo, rest } = parseOptionalRepoFlag(argv);
1146
+ const flags = parseStopFlags(rest);
1147
+ if (flags.all && repo) throw new Error("Do not combine --all with --repo");
1148
+ if (flags.all && flags.agentId) throw new Error("Do not combine --all with --agent");
1149
+ if (flags.all) {
1150
+ const repoSummaries = listTrackedArtifactsDirs().map((artifactsDir) => {
1151
+ const result = stopManagedAgents({
1152
+ artifactsDir,
1153
+ actorId: "human",
1154
+ reason: "Stopped via pitown stop --all",
1155
+ force: flags.force
1156
+ });
1157
+ return {
1158
+ repoLabel: artifactsDir,
1159
+ stoppedAgents: result.stoppedAgents,
1160
+ signaledAgentProcesses: result.signaledProcesses,
1161
+ signaledLeaseProcesses: 0
1162
+ };
1163
+ });
1164
+ const leaseResult = stopRepoLeases({ force: flags.force });
1165
+ const totalAgents = repoSummaries.reduce((sum, result) => sum + result.stoppedAgents, 0);
1166
+ const totalAgentProcesses = repoSummaries.reduce((sum, result) => sum + result.signaledAgentProcesses, 0);
1167
+ const totalLeaseProcesses = leaseResult.signaledProcesses + repoSummaries.reduce((sum, result) => sum + result.signaledLeaseProcesses, 0);
1168
+ console.log("[pitown] stop");
1169
+ console.log("- scope: all repos");
1170
+ console.log(`- stopped agents: ${totalAgents}`);
1171
+ console.log(`- signaled agent processes: ${totalAgentProcesses}`);
1172
+ console.log(`- signaled lease processes: ${totalLeaseProcesses}`);
1173
+ if (repoSummaries.length === 0 && leaseResult.results.length === 0) console.log("- nothing was running");
1174
+ return repoSummaries;
1175
+ }
1176
+ const resolved = repo ? resolveRepoContext(["--repo", repo]) : resolveRepoContext([]);
1177
+ const result = stopRepo(resolved.repoRoot, resolved.artifactsDir, flags);
1178
+ console.log("[pitown] stop");
1179
+ console.log(`- repo root: ${result.repoLabel}`);
1180
+ if (flags.agentId) console.log(`- agent: ${flags.agentId}`);
1181
+ console.log(`- stopped agents: ${result.stoppedAgents}`);
1182
+ console.log(`- signaled agent processes: ${result.signaledAgentProcesses}`);
1183
+ if (!flags.agentId) console.log(`- signaled lease processes: ${result.signaledLeaseProcesses}`);
1184
+ if (result.stoppedAgents === 0 && result.signaledAgentProcesses === 0 && result.signaledLeaseProcesses === 0) console.log("- nothing was running");
1185
+ return [result];
1186
+ }
1187
+
1188
+ //#endregion
1189
+ //#region src/version.ts
1190
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
1191
+ const CLI_VERSION = packageJson.version;
1192
+
1193
+ //#endregion
7
1194
  //#region src/index.ts
8
- function printHelp() {
1195
+ function printHelp(showAdvanced = false) {
9
1196
  console.log([
10
1197
  "pitown",
11
1198
  "",
12
1199
  "Usage:",
13
- " pitown run [--repo <path>] [--plan <path>] [--goal <text>]",
1200
+ " pitown [--repo <path>] [\"message\"]",
1201
+ " pitown board [--repo <path>]",
1202
+ " pitown peek [--repo <path>] [agent]",
1203
+ " pitown msg [--repo <path>] <agent> \"message\"",
14
1204
  " pitown status [--repo <path>]",
15
- " pitown watch [--repo <path>]",
16
- " pitown help",
17
- " pitown --help"
1205
+ " pitown stop [--repo <path>] [--agent <id>] [--all] [--force]",
1206
+ " pitown doctor",
1207
+ " pitown --version",
1208
+ "",
1209
+ "Mayor workflow:",
1210
+ " pitown",
1211
+ " pitown \"plan the next milestones\"",
1212
+ " /plan",
1213
+ " /todos",
1214
+ "",
1215
+ "Inside the mayor session, `/plan` toggles read-only planning mode and `/todos` shows the captured plan.",
1216
+ "Aliases still work: `pitown mayor`, `pitown help`, `pitown --help`, `pitown -v`.",
1217
+ "",
1218
+ "If --repo is omitted, Pi Town uses the repo for the current working directory when possible.",
1219
+ ...showAdvanced ? [
1220
+ "",
1221
+ "Advanced commands:",
1222
+ " pitown run [--repo <path>] [--plan <path>] [--goal <text>]",
1223
+ " pitown loop [--repo <path>] [--plan <path>] [--goal <text>] [--max-iterations N] [--max-time M] [--no-stop-on-failure]",
1224
+ " pitown attach [--repo <path>] <agent>",
1225
+ " pitown continue [--repo <path>] <agent> [\"message\"]",
1226
+ " pitown delegate [--repo <path>] [--from <agent>] [--role <role>] [--agent <id>] --task <text>",
1227
+ " pitown spawn [--repo <path>] --role <role> [--agent <id>] [--task <text>]",
1228
+ " pitown watch [--repo <path>]"
1229
+ ] : []
18
1230
  ].join("\n"));
19
1231
  }
20
1232
  function runCli(argv = process.argv.slice(2)) {
21
1233
  const [command, ...args] = argv;
1234
+ const showAdvancedHelp = args.includes("--all");
22
1235
  switch (command) {
23
1236
  case void 0:
1237
+ openTownMayor([]);
1238
+ break;
24
1239
  case "help":
25
1240
  case "--help":
26
1241
  case "-h":
27
- printHelp();
1242
+ printHelp(showAdvancedHelp);
1243
+ break;
1244
+ case "-v":
1245
+ case "--version":
1246
+ case "version":
1247
+ console.log(CLI_VERSION);
28
1248
  break;
29
1249
  case "run": {
30
1250
  const result = runTown(args);
31
- if (result.piInvocation.exitCode !== 0) process.exitCode = result.piInvocation.exitCode;
1251
+ const latestIteration = result.iterations[result.iterations.length - 1];
1252
+ if (latestIteration && latestIteration.controllerResult.piInvocation.exitCode !== 0) process.exitCode = latestIteration.controllerResult.piInvocation.exitCode;
1253
+ break;
1254
+ }
1255
+ case "loop": {
1256
+ const result = loopTown(args);
1257
+ const lastIteration = result.iterations[result.iterations.length - 1];
1258
+ if (lastIteration && lastIteration.controllerResult.piInvocation.exitCode !== 0) process.exitCode = lastIteration.controllerResult.piInvocation.exitCode;
32
1259
  break;
33
1260
  }
1261
+ case "attach":
1262
+ attachTownAgent(args);
1263
+ break;
1264
+ case "board":
1265
+ showTownBoard(args);
1266
+ break;
1267
+ case "continue":
1268
+ continueTownAgent(args);
1269
+ break;
1270
+ case "delegate":
1271
+ delegateTownTask(args);
1272
+ break;
1273
+ case "mayor":
1274
+ openTownMayor(args);
1275
+ break;
1276
+ case "msg":
1277
+ messageTownAgent(args);
1278
+ break;
1279
+ case "peek":
1280
+ peekTownAgent(args);
1281
+ break;
1282
+ case "spawn":
1283
+ spawnTownAgent(args);
1284
+ break;
34
1285
  case "status":
35
1286
  showTownStatus(args);
36
1287
  break;
1288
+ case "stop":
1289
+ stopTown(args);
1290
+ break;
37
1291
  case "watch":
38
1292
  watchTown(args);
39
1293
  break;
1294
+ case "doctor":
1295
+ if (!runDoctor().ok) process.exitCode = 1;
1296
+ break;
40
1297
  default:
41
1298
  console.log(`Unknown command: ${command}`);
42
1299
  printHelp();