@schilderlabs/pitown 0.2.1 → 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,293 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import { a as createAgentState, c as getLatestAgentSession, d as readAgentState, f as writeAgentState, i as createAgentSessionRecord, l as listAgentStates, n as computeMetrics, o as getAgentDir, p as appendJsonl, r as appendAgentMessage, s as getAgentSessionsDir, t as runController, u as readAgentMessages } from "./controller-9ihAZj3V.mjs";
3
- import { a as runCommandSync, i as runCommandInteractive, n as assertCommandAvailable, t as isDirectExecution } from "./entrypoint-CyJDLudQ.mjs";
4
- import { a as getRecommendedPlanDir, d as createRepoSlug, h as isGitRepo, i as getLatestRunPointerPath, l as getTownHomeDir, m as getRepoRoot, n as parseOptionalRepoFlag, o as getRepoAgentsDir, p as getRepoIdentity, s as getRepoArtifactsDir, u as getUserConfigPath } from "./config-CUpe9o0x.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";
5
7
  import { runDoctor } from "./doctor.mjs";
6
8
  import { runTown } from "./run.mjs";
7
9
  import { showTownStatus } from "./status.mjs";
8
10
  import { watchTown } from "./watch.mjs";
9
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
10
- import { dirname, isAbsolute, join, resolve } from "node:path";
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";
11
14
  import { homedir } from "node:os";
15
+ import { spawn } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
12
17
  import { readPiTownMayorPrompt, resolvePiTownExtensionPath } from "@schilderlabs/pitown-package";
13
18
 
14
- //#region ../core/src/tasks.ts
15
- function writeJson$1(path, value) {
16
- mkdirSync(dirname(path), { recursive: true });
17
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
18
- }
19
- function getTasksDir(artifactsDir) {
20
- return join(artifactsDir, "tasks");
21
- }
22
- function getTaskPath(artifactsDir, taskId) {
23
- return join(getTasksDir(artifactsDir), `${taskId}.json`);
24
- }
25
- function createTaskRecord(input) {
26
- const now = (/* @__PURE__ */ new Date()).toISOString();
27
- return {
28
- taskId: input.taskId,
29
- title: input.title,
30
- status: input.status,
31
- role: input.role,
32
- assignedAgentId: input.assignedAgentId,
33
- createdBy: input.createdBy,
34
- createdAt: now,
35
- updatedAt: now
36
- };
37
- }
38
- function writeTaskRecord(artifactsDir, task) {
39
- writeJson$1(getTaskPath(artifactsDir, task.taskId), task);
40
- }
41
- function readTaskRecord(artifactsDir, taskId) {
42
- const path = getTaskPath(artifactsDir, taskId);
43
- try {
44
- return JSON.parse(readFileSync(path, "utf-8"));
45
- } catch {
46
- return null;
47
- }
48
- }
49
- function listTaskRecords(artifactsDir) {
50
- const tasksDir = getTasksDir(artifactsDir);
51
- let entries;
52
- try {
53
- entries = readdirSync(tasksDir);
54
- } catch {
55
- return [];
56
- }
57
- return entries.filter((entry) => entry.endsWith(".json")).map((entry) => readTaskRecord(artifactsDir, entry.replace(/\.json$/, ""))).filter((task) => task !== null).sort((left, right) => left.taskId.localeCompare(right.taskId));
58
- }
59
-
60
- //#endregion
61
- //#region ../core/src/loop.ts
62
- const DEFAULT_MAX_ITERATIONS$1 = 10;
63
- const DEFAULT_MAX_WALL_TIME_MS = 36e5;
64
- function createLoopId() {
65
- return `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
66
- }
67
- function writeJson(path, value) {
68
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
69
- }
70
- function snapshotBoard(artifactsDir) {
71
- const tasks = listTaskRecords(artifactsDir);
72
- const agents = listAgentStates(artifactsDir);
73
- return {
74
- tasks: tasks.map((task) => ({
75
- taskId: task.taskId,
76
- status: task.status
77
- })),
78
- agents: agents.map((agent) => ({
79
- agentId: agent.agentId,
80
- status: agent.status,
81
- blocked: agent.blocked
82
- })),
83
- allTasksCompleted: tasks.length > 0 && tasks.every((task) => task.status === "completed"),
84
- allRemainingTasksBlocked: tasks.length > 0 && tasks.every((task) => task.status === "completed" || task.status === "blocked"),
85
- leaderBlocked: agents.find((agent) => agent.agentId === "leader")?.blocked === true,
86
- hasQueuedOrRunningWork: agents.some((agent) => agent.status === "queued" || agent.status === "running" || agent.status === "starting") || tasks.some((task) => task.status === "queued" || task.status === "running")
87
- };
88
- }
89
- function evaluateStopCondition(input) {
90
- if (input.iteration >= input.maxIterations) return {
91
- stopReason: "max-iterations-reached",
92
- continueReason: null
93
- };
94
- if (input.elapsedMs >= input.maxWallTimeMs) return {
95
- stopReason: "max-wall-time-reached",
96
- continueReason: null
97
- };
98
- if (input.stopOnPiFailure && input.piExitCode !== 0) return {
99
- stopReason: "pi-exit-nonzero",
100
- continueReason: null
101
- };
102
- if (input.board.allTasksCompleted) return {
103
- stopReason: "all-tasks-completed",
104
- continueReason: null
105
- };
106
- if (input.board.leaderBlocked) return {
107
- stopReason: "leader-blocked",
108
- continueReason: null
109
- };
110
- if (input.board.allRemainingTasksBlocked) return {
111
- stopReason: "all-remaining-tasks-blocked",
112
- continueReason: null
113
- };
114
- if (input.interruptRateThreshold !== null && input.metrics.interruptRate > input.interruptRateThreshold) return {
115
- stopReason: "high-interrupt-rate",
116
- continueReason: null
117
- };
118
- const reasons = [];
119
- if (input.board.hasQueuedOrRunningWork) reasons.push("queued or running work remains");
120
- if (input.board.tasks.length === 0) reasons.push("no tasks tracked yet");
121
- if (reasons.length === 0) reasons.push("leader idle, no stop condition met");
122
- return {
123
- stopReason: null,
124
- continueReason: reasons.join("; ")
125
- };
126
- }
127
- function aggregateMetrics(iterations) {
128
- if (iterations.length === 0) return computeMetrics({
129
- taskAttempts: [],
130
- interrupts: []
131
- });
132
- let totalTaskAttempts = 0;
133
- let totalCompletedTasks = 0;
134
- let totalInterrupts = 0;
135
- let totalObservedInterruptCategories = 0;
136
- let totalCoveredInterruptCategories = 0;
137
- let interruptRateSum = 0;
138
- let autonomousCompletionRateSum = 0;
139
- let contextCoverageScoreSum = 0;
140
- let mttcValues = [];
141
- let ftdValues = [];
142
- for (const iter of iterations) {
143
- const m = iter.metrics;
144
- totalTaskAttempts += m.totals.taskAttempts;
145
- totalCompletedTasks += m.totals.completedTasks;
146
- totalInterrupts += m.totals.interrupts;
147
- totalObservedInterruptCategories += m.totals.observedInterruptCategories;
148
- totalCoveredInterruptCategories += m.totals.coveredInterruptCategories;
149
- interruptRateSum += m.interruptRate;
150
- autonomousCompletionRateSum += m.autonomousCompletionRate;
151
- contextCoverageScoreSum += m.contextCoverageScore;
152
- if (m.meanTimeToCorrectHours !== null) mttcValues.push(m.meanTimeToCorrectHours);
153
- if (m.feedbackToDemoCycleTimeHours !== null) ftdValues.push(m.feedbackToDemoCycleTimeHours);
154
- }
155
- const count = iterations.length;
156
- const round = (v) => Math.round(v * 1e3) / 1e3;
157
- const avg = (values) => values.length === 0 ? null : round(values.reduce((s, v) => s + v, 0) / values.length);
158
- return {
159
- interruptRate: round(interruptRateSum / count),
160
- autonomousCompletionRate: round(autonomousCompletionRateSum / count),
161
- contextCoverageScore: round(contextCoverageScoreSum / count),
162
- meanTimeToCorrectHours: avg(mttcValues),
163
- feedbackToDemoCycleTimeHours: avg(ftdValues),
164
- totals: {
165
- taskAttempts: totalTaskAttempts,
166
- completedTasks: totalCompletedTasks,
167
- interrupts: totalInterrupts,
168
- observedInterruptCategories: totalObservedInterruptCategories,
169
- coveredInterruptCategories: totalCoveredInterruptCategories
170
- }
171
- };
172
- }
173
- function runLoop(options) {
174
- const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS$1;
175
- const maxWallTimeMs = options.maxWallTimeMs ?? DEFAULT_MAX_WALL_TIME_MS;
176
- const stopOnPiFailure = options.stopOnPiFailure ?? true;
177
- const interruptRateThreshold = options.interruptRateThreshold ?? null;
178
- const loopId = createLoopId();
179
- const artifactsDir = options.runOptions.artifactsDir;
180
- const loopDir = join(artifactsDir, "loops", loopId);
181
- mkdirSync(loopDir, { recursive: true });
182
- const loopStartedAt = Date.now();
183
- const iterations = [];
184
- let finalStopReason = "max-iterations-reached";
185
- appendJsonl(join(loopDir, "events.jsonl"), {
186
- type: "loop_started",
187
- loopId,
188
- maxIterations,
189
- maxWallTimeMs,
190
- stopOnPiFailure,
191
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
192
- });
193
- for (let iteration = 1; iteration <= maxIterations; iteration++) {
194
- const iterationStart = Date.now();
195
- let controllerResult;
196
- try {
197
- controllerResult = runController(options.runOptions);
198
- } catch (error) {
199
- appendJsonl(join(loopDir, "events.jsonl"), {
200
- type: "loop_iteration_error",
201
- loopId,
202
- iteration,
203
- error: error.message,
204
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
205
- });
206
- finalStopReason = "pi-exit-nonzero";
207
- break;
208
- }
209
- const iterationElapsedMs = Date.now() - iterationStart;
210
- const totalElapsedMs = Date.now() - loopStartedAt;
211
- const board = snapshotBoard(artifactsDir);
212
- const metrics = controllerResult.metrics;
213
- const { stopReason, continueReason } = evaluateStopCondition({
214
- iteration,
215
- maxIterations,
216
- elapsedMs: totalElapsedMs,
217
- maxWallTimeMs,
218
- piExitCode: controllerResult.piInvocation.exitCode,
219
- stopOnPiFailure,
220
- board,
221
- metrics,
222
- interruptRateThreshold
223
- });
224
- const iterationResult = {
225
- iteration,
226
- controllerResult,
227
- boardSnapshot: board,
228
- metrics,
229
- elapsedMs: iterationElapsedMs,
230
- continueReason,
231
- stopReason
232
- };
233
- iterations.push(iterationResult);
234
- writeJson(join(loopDir, `iteration-${iteration}.json`), {
235
- iteration,
236
- runId: controllerResult.runId,
237
- boardSnapshot: board,
238
- metrics,
239
- elapsedMs: iterationElapsedMs,
240
- continueReason,
241
- stopReason
242
- });
243
- appendJsonl(join(loopDir, "events.jsonl"), {
244
- type: "loop_iteration_completed",
245
- loopId,
246
- iteration,
247
- runId: controllerResult.runId,
248
- piExitCode: controllerResult.piInvocation.exitCode,
249
- stopReason,
250
- continueReason,
251
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
252
- });
253
- if (options.onIterationComplete) options.onIterationComplete(iterationResult);
254
- if (stopReason !== null) {
255
- finalStopReason = stopReason;
256
- break;
257
- }
258
- }
259
- const totalElapsedMs = Date.now() - loopStartedAt;
260
- const finalBoard = iterations.length > 0 ? iterations[iterations.length - 1].boardSnapshot : snapshotBoard(artifactsDir);
261
- const aggregate = aggregateMetrics(iterations);
262
- const loopResult = {
263
- loopId,
264
- iterations,
265
- stopReason: finalStopReason,
266
- totalIterations: iterations.length,
267
- totalElapsedMs,
268
- finalBoardSnapshot: finalBoard,
269
- aggregateMetrics: aggregate
270
- };
271
- writeJson(join(loopDir, "loop-summary.json"), {
272
- loopId,
273
- stopReason: finalStopReason,
274
- totalIterations: iterations.length,
275
- totalElapsedMs,
276
- finalBoardSnapshot: finalBoard,
277
- aggregateMetrics: aggregate
278
- });
279
- appendJsonl(join(loopDir, "events.jsonl"), {
280
- type: "loop_finished",
281
- loopId,
282
- stopReason: finalStopReason,
283
- totalIterations: iterations.length,
284
- totalElapsedMs,
285
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
286
- });
287
- return loopResult;
288
- }
289
-
290
- //#endregion
291
19
  //#region ../core/src/orchestration.ts
292
20
  function createPiInvocationArgs(input) {
293
21
  const args = [];
@@ -299,11 +27,29 @@ function createPiInvocationArgs(input) {
299
27
  if (input.prompt) args.push("-p", input.prompt);
300
28
  return args;
301
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
+ }
302
48
  function createRolePrompt(input) {
303
49
  const task = input.task ?? "pick the next bounded task from the current repo context";
304
50
  switch (input.role) {
305
- case "leader": return [
306
- "You are the Pi Town leader.",
51
+ case "mayor": return [
52
+ "You are the Pi Town mayor.",
307
53
  "You coordinate work for this repository and act as the primary human-facing agent.",
308
54
  "",
309
55
  `Repository: ${input.repoRoot}`,
@@ -350,6 +96,7 @@ function resolveAgentSession(agentId, artifactsDir) {
350
96
  sessionDir,
351
97
  sessionId,
352
98
  sessionPath,
99
+ processId: state.session.processId,
353
100
  lastAttachedAt: (/* @__PURE__ */ new Date()).toISOString()
354
101
  })
355
102
  };
@@ -374,6 +121,7 @@ function queueAgentMessage(input) {
374
121
  sessionDir: state.session.sessionDir ?? getAgentSessionsDir(input.artifactsDir, input.agentId),
375
122
  sessionId: state.session.sessionId,
376
123
  sessionPath: state.session.sessionPath,
124
+ processId: state.session.processId,
377
125
  lastAttachedAt: state.session.lastAttachedAt
378
126
  })
379
127
  }));
@@ -414,46 +162,41 @@ function spawnAgentRun(options) {
414
162
  appendedSystemPrompt: options.appendedSystemPrompt,
415
163
  extensionPath: options.extensionPath
416
164
  });
417
- const piResult = runCommandSync("pi", piArgs, {
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, {
418
177
  cwd: options.repoRoot,
419
- env: process.env
178
+ detached: true,
179
+ env: process.env,
180
+ stdio: "ignore"
420
181
  });
421
- const latestSession = getLatestAgentSession(options.artifactsDir, options.agentId);
422
- const agentArtifactsDir = getAgentDir(options.artifactsDir, options.agentId);
423
- writeFileSync(`${agentArtifactsDir}/latest-stdout.txt`, piResult.stdout, "utf-8");
424
- writeFileSync(`${agentArtifactsDir}/latest-stderr.txt`, piResult.stderr, "utf-8");
425
- writeFileSync(`${agentArtifactsDir}/latest-invocation.json`, `${JSON.stringify({
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({
426
185
  command: "pi",
427
186
  args: piArgs,
428
- exitCode: piResult.exitCode,
187
+ exitCode: null,
429
188
  sessionDir,
430
- sessionPath: latestSession.sessionPath,
431
- sessionId: latestSession.sessionId
189
+ sessionPath: null,
190
+ sessionId: null,
191
+ processId: child.pid,
192
+ startedAt
432
193
  }, null, 2)}\n`, "utf-8");
433
- const completionMessage = piResult.stdout.trim() || (piResult.exitCode === 0 ? `${options.role} run completed` : `${options.role} run exited with code ${piResult.exitCode}`);
434
- appendAgentMessage({
435
- artifactsDir: options.artifactsDir,
436
- agentId: options.agentId,
437
- box: "outbox",
438
- from: options.agentId,
439
- body: completionMessage
440
- });
441
- writeAgentState(options.artifactsDir, createAgentState({
442
- ...state,
443
- status: piResult.exitCode === 0 ? "idle" : "blocked",
444
- lastMessage: completionMessage,
445
- blocked: piResult.exitCode !== 0,
446
- waitingOn: piResult.exitCode === 0 ? null : "human-or-follow-up-run",
447
- session: createAgentSessionRecord({
448
- sessionDir: latestSession.sessionDir,
449
- sessionId: latestSession.sessionId,
450
- sessionPath: latestSession.sessionPath
451
- })
452
- }));
453
194
  return {
454
- piResult,
455
- latestSession,
456
- completionMessage
195
+ launch: {
196
+ processId: child.pid,
197
+ startedAt
198
+ },
199
+ latestSession: createAgentSessionRecord({ sessionDir })
457
200
  };
458
201
  }
459
202
  function runAgentTurn(options) {
@@ -505,7 +248,8 @@ function runAgentTurn(options) {
505
248
  session: createAgentSessionRecord({
506
249
  sessionDir: latestSession.sessionDir,
507
250
  sessionId: latestSession.sessionId,
508
- sessionPath: latestSession.sessionPath
251
+ sessionPath: latestSession.sessionPath,
252
+ processId: null
509
253
  })
510
254
  }));
511
255
  return {
@@ -533,7 +277,7 @@ function delegateTask(options) {
533
277
  from: options.fromAgentId,
534
278
  body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
535
279
  });
536
- const { piResult, latestSession } = spawnAgentRun({
280
+ const { launch, latestSession } = spawnAgentRun({
537
281
  repoRoot: options.repoRoot,
538
282
  artifactsDir: options.artifactsDir,
539
283
  role: options.role,
@@ -552,31 +296,198 @@ function delegateTask(options) {
552
296
  });
553
297
  writeTaskRecord(options.artifactsDir, {
554
298
  ...task,
555
- status: piResult.exitCode === 0 ? "completed" : "blocked",
299
+ status: "running",
556
300
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
557
301
  });
558
302
  return {
559
303
  task: {
560
304
  ...task,
561
- status: piResult.exitCode === 0 ? "completed" : "blocked",
305
+ status: "running",
562
306
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
563
307
  },
564
308
  agentId,
565
- piResult,
309
+ launch,
566
310
  latestSession
567
311
  };
568
312
  }
569
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
+
570
481
  //#endregion
571
482
  //#region src/agent-id.ts
572
483
  function normalizeAgentId(agentId) {
573
- return agentId === "mayor" ? "leader" : agentId;
484
+ return agentId;
574
485
  }
575
486
 
576
487
  //#endregion
577
488
  //#region src/pi-runtime.ts
578
489
  function isMayorAgent(agentId) {
579
- return agentId === "leader" || agentId === "mayor";
490
+ return agentId === "mayor";
580
491
  }
581
492
  function createPiTownRuntimeArgs(options) {
582
493
  const args = ["--extension", resolvePiTownExtensionPath()];
@@ -589,46 +500,6 @@ function createPiTownRuntimeArgs(options) {
589
500
  return args;
590
501
  }
591
502
 
592
- //#endregion
593
- //#region src/repo-context.ts
594
- function resolveRepoContext(argv) {
595
- const { repo, rest } = parseOptionalRepoFlag(argv);
596
- if (repo) {
597
- const repoRoot = getRepoRoot(repo);
598
- const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
599
- return {
600
- repoRoot,
601
- repoSlug,
602
- artifactsDir: getRepoArtifactsDir(repoSlug),
603
- rest
604
- };
605
- }
606
- const cwd = process.cwd();
607
- const repoRoot = getRepoRoot(cwd);
608
- const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
609
- const artifactsDir = getRepoArtifactsDir(repoSlug);
610
- if (isGitRepo(cwd) || existsSync(artifactsDir)) return {
611
- repoRoot,
612
- repoSlug,
613
- artifactsDir,
614
- rest
615
- };
616
- const latestPointerPath = getLatestRunPointerPath();
617
- if (!existsSync(latestPointerPath)) return {
618
- repoRoot,
619
- repoSlug,
620
- artifactsDir,
621
- rest
622
- };
623
- const latest = JSON.parse(readFileSync(latestPointerPath, "utf-8"));
624
- return {
625
- repoRoot: latest.repoRoot,
626
- repoSlug: latest.repoSlug,
627
- artifactsDir: getRepoArtifactsDir(latest.repoSlug),
628
- rest
629
- };
630
- }
631
-
632
503
  //#endregion
633
504
  //#region src/attach.ts
634
505
  function attachTownAgent(argv = process.argv.slice(2)) {
@@ -658,23 +529,71 @@ function attachTownAgent(argv = process.argv.slice(2)) {
658
529
 
659
530
  //#endregion
660
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
+ }
661
538
  function showTownBoard(argv = process.argv.slice(2)) {
662
539
  const repo = resolveRepoContext(argv);
663
540
  const agents = listAgentStates(repo.artifactsDir);
664
- console.log("[pitown] board");
665
- console.log(`- repo root: ${repo.repoRoot}`);
666
- console.log(`- agents dir: ${getRepoAgentsDir(repo.repoSlug)}`);
667
- if (agents.length === 0) {
668
- console.log("- no agents found yet");
669
- return;
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}`);
670
571
  }
671
- for (const agent of agents) {
672
- const task = agent.task ?? "no active task";
673
- const note = agent.lastMessage ? ` | ${agent.lastMessage}` : "";
674
- const waitingOn = agent.waitingOn ? ` | waiting on: ${agent.waitingOn}` : "";
675
- const taskId = agent.taskId ? ` [${agent.taskId}]` : "";
676
- console.log(`${agent.agentId.padEnd(12)} ${agent.status.padEnd(8)} ${task}${taskId}${note}${waitingOn}`);
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}`);
677
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);
678
597
  }
679
598
 
680
599
  //#endregion
@@ -711,12 +630,13 @@ function continueTownAgent(argv = process.argv.slice(2)) {
711
630
  //#endregion
712
631
  //#region src/delegate.ts
713
632
  function parseDelegateFlags(argv) {
714
- let from = "leader";
633
+ let from = "mayor";
715
634
  let role = "worker";
716
635
  let agentId = null;
717
636
  let task = null;
718
637
  for (let index = 0; index < argv.length; index += 1) {
719
638
  const arg = argv[index];
639
+ if (arg === void 0) continue;
720
640
  if (arg.startsWith("--from=")) {
721
641
  from = arg.slice(7);
722
642
  continue;
@@ -768,7 +688,7 @@ function delegateTownTask(argv = process.argv.slice(2)) {
768
688
  const flags = parseDelegateFlags(repo.rest);
769
689
  const fromAgentId = normalizeAgentId(flags.from);
770
690
  if (readAgentState(repo.artifactsDir, fromAgentId) === null) throw new Error(`Unknown delegating agent: ${fromAgentId}`);
771
- const { agentId, latestSession, piResult, task } = delegateTask({
691
+ const { agentId, latestSession, launch, task } = delegateTask({
772
692
  repoRoot: repo.repoRoot,
773
693
  artifactsDir: repo.artifactsDir,
774
694
  fromAgentId,
@@ -783,8 +703,10 @@ function delegateTownTask(argv = process.argv.slice(2)) {
783
703
  console.log(`- task id: ${task.taskId}`);
784
704
  console.log(`- agent: ${agentId}`);
785
705
  console.log(`- role: ${flags.role}`);
786
- console.log(`- pi exit code: ${piResult.exitCode}`);
706
+ console.log(`- status: ${task.status}`);
707
+ console.log(`- launch pid: ${launch.processId}`);
787
708
  if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
709
+ else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
788
710
  }
789
711
 
790
712
  //#endregion
@@ -806,6 +728,7 @@ function parseLoopCliFlags(argv) {
806
728
  const flags = { noStopOnFailure: false };
807
729
  for (let index = 0; index < argv.length; index += 1) {
808
730
  const arg = argv[index];
731
+ if (arg === void 0) continue;
809
732
  if (arg.startsWith("--repo=")) {
810
733
  flags.repo = arg.slice(7);
811
734
  continue;
@@ -926,11 +849,11 @@ function loopTown(argv = process.argv.slice(2)) {
926
849
  onIterationComplete(iteration) {
927
850
  const board = iteration.boardSnapshot;
928
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";
929
- const leaderStatus = board.agents.find((a) => a.agentId === "leader")?.status ?? "unknown";
852
+ const mayorStatus = board.agents.find((a) => a.agentId === "mayor")?.status ?? "unknown";
930
853
  console.log(`[pitown-loop] iteration ${iteration.iteration}/${config.maxIterations} completed (${formatMs(iteration.elapsedMs)})`);
931
854
  console.log(` - pi exit code: ${iteration.controllerResult.piInvocation.exitCode}`);
932
855
  console.log(` - run: ${iteration.controllerResult.runId}`);
933
- console.log(` - board: ${taskSummary}, leader ${leaderStatus}`);
856
+ console.log(` - board: ${taskSummary}, mayor ${mayorStatus}`);
934
857
  console.log(` - metrics: interrupt rate ${iteration.metrics.interruptRate}, autonomous completion ${iteration.metrics.autonomousCompletionRate}`);
935
858
  if (iteration.stopReason) console.log(` - stopping: ${iteration.stopReason}`);
936
859
  else console.log(` - continuing: ${iteration.continueReason}`);
@@ -945,10 +868,10 @@ function loopTown(argv = process.argv.slice(2)) {
945
868
  //#endregion
946
869
  //#region src/mayor.ts
947
870
  function startFreshMayorSession(repoRoot, artifactsDir) {
948
- const sessionDir = getAgentSessionsDir(artifactsDir, "leader");
871
+ const sessionDir = getAgentSessionsDir(artifactsDir, "mayor");
949
872
  writeAgentState(artifactsDir, createAgentState({
950
- agentId: "leader",
951
- role: "leader",
873
+ agentId: "mayor",
874
+ role: "mayor",
952
875
  status: "running",
953
876
  task: "open the mayor session and plan the next steps for this repository",
954
877
  lastMessage: "Mayor session opened",
@@ -958,14 +881,14 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
958
881
  console.log(`- repo root: ${repoRoot}`);
959
882
  console.log("- starting a new mayor session");
960
883
  const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
961
- agentId: "leader",
884
+ agentId: "mayor",
962
885
  sessionDir
963
886
  }), {
964
887
  cwd: repoRoot,
965
888
  env: process.env
966
889
  });
967
- const latestSession = getLatestAgentSession(artifactsDir, "leader");
968
- const previousState = readAgentState(artifactsDir, "leader");
890
+ const latestSession = getLatestAgentSession(artifactsDir, "mayor");
891
+ const previousState = readAgentState(artifactsDir, "mayor");
969
892
  if (previousState !== null) writeAgentState(artifactsDir, createAgentState({
970
893
  ...previousState,
971
894
  status: exitCode === 0 ? "idle" : "blocked",
@@ -983,7 +906,7 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
983
906
  function openTownMayor(argv = process.argv.slice(2)) {
984
907
  const repo = resolveRepoContext(argv);
985
908
  const message = repo.rest.join(" ").trim();
986
- if (readAgentState(repo.artifactsDir, "leader") === null) {
909
+ if (readAgentState(repo.artifactsDir, "mayor") === null) {
987
910
  assertCommandAvailable("pi");
988
911
  if (message) {
989
912
  runTown([
@@ -1029,7 +952,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
1029
952
  from: "human",
1030
953
  body
1031
954
  });
1032
- const deliveredResult = state.role === "leader" ? runAgentTurn({
955
+ const deliveredResult = state.role === "mayor" ? runAgentTurn({
1033
956
  repoRoot: repo.repoRoot,
1034
957
  artifactsDir: repo.artifactsDir,
1035
958
  agentId,
@@ -1047,7 +970,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
1047
970
  console.log(`- queued message: ${body}`);
1048
971
  if (deliveredResult) {
1049
972
  console.log(`- delivered to session: ${deliveredResult.latestSession.sessionPath}`);
1050
- console.log(`- leader response: ${deliveredResult.completionMessage}`);
973
+ console.log(`- mayor response: ${deliveredResult.completionMessage}`);
1051
974
  }
1052
975
  }
1053
976
 
@@ -1093,6 +1016,7 @@ function parseSpawnFlags(argv) {
1093
1016
  let task = null;
1094
1017
  for (let index = 0; index < argv.length; index += 1) {
1095
1018
  const arg = argv[index];
1019
+ if (arg === void 0) continue;
1096
1020
  if (arg.startsWith("--role=")) {
1097
1021
  role = arg.slice(7);
1098
1022
  continue;
@@ -1137,7 +1061,7 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
1137
1061
  const flags = parseSpawnFlags(repo.rest);
1138
1062
  const agentId = flags.agentId ?? `${flags.role}-${Date.now()}`;
1139
1063
  const task = flags.task;
1140
- const { piResult, latestSession } = spawnAgent({
1064
+ const { launch, latestSession } = spawnAgent({
1141
1065
  repoRoot: repo.repoRoot,
1142
1066
  artifactsDir: repo.artifactsDir,
1143
1067
  role: flags.role,
@@ -1149,18 +1073,122 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
1149
1073
  console.log(`- repo root: ${repo.repoRoot}`);
1150
1074
  console.log(`- agent: ${agentId}`);
1151
1075
  console.log(`- role: ${flags.role}`);
1152
- console.log(`- pi exit code: ${piResult.exitCode}`);
1076
+ console.log(`- status: running`);
1077
+ console.log(`- launch pid: ${launch.processId}`);
1153
1078
  if (task) console.log(`- task: ${task}`);
1154
1079
  if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
1080
+ else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
1155
1081
  }
1156
1082
 
1157
1083
  //#endregion
1158
- //#region package.json
1159
- var version = "0.2.1";
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
+ }
1160
1187
 
1161
1188
  //#endregion
1162
1189
  //#region src/version.ts
1163
- const CLI_VERSION = version;
1190
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
1191
+ const CLI_VERSION = packageJson.version;
1164
1192
 
1165
1193
  //#endregion
1166
1194
  //#region src/index.ts
@@ -1169,27 +1197,23 @@ function printHelp(showAdvanced = false) {
1169
1197
  "pitown",
1170
1198
  "",
1171
1199
  "Usage:",
1172
- " pitown",
1173
- " pitown mayor [--repo <path>] [\"message\"]",
1200
+ " pitown [--repo <path>] [\"message\"]",
1174
1201
  " pitown board [--repo <path>]",
1175
1202
  " pitown peek [--repo <path>] [agent]",
1176
- " pitown msg [--repo <path>] mayor \"message\"",
1203
+ " pitown msg [--repo <path>] <agent> \"message\"",
1177
1204
  " pitown status [--repo <path>]",
1205
+ " pitown stop [--repo <path>] [--agent <id>] [--all] [--force]",
1178
1206
  " pitown doctor",
1179
- " pitown help",
1180
- " pitown help --all",
1181
- " pitown --help",
1182
- " pitown -v",
1183
1207
  " pitown --version",
1184
1208
  "",
1185
1209
  "Mayor workflow:",
1186
1210
  " pitown",
1187
- " pitown mayor",
1188
- " pitown mayor \"plan the next milestones\"",
1211
+ " pitown \"plan the next milestones\"",
1189
1212
  " /plan",
1190
1213
  " /todos",
1191
1214
  "",
1192
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`.",
1193
1217
  "",
1194
1218
  "If --repo is omitted, Pi Town uses the repo for the current working directory when possible.",
1195
1219
  ...showAdvanced ? [
@@ -1224,7 +1248,8 @@ function runCli(argv = process.argv.slice(2)) {
1224
1248
  break;
1225
1249
  case "run": {
1226
1250
  const result = runTown(args);
1227
- 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;
1228
1253
  break;
1229
1254
  }
1230
1255
  case "loop": {
@@ -1260,6 +1285,9 @@ function runCli(argv = process.argv.slice(2)) {
1260
1285
  case "status":
1261
1286
  showTownStatus(args);
1262
1287
  break;
1288
+ case "stop":
1289
+ stopTown(args);
1290
+ break;
1263
1291
  case "watch":
1264
1292
  watchTown(args);
1265
1293
  break;