@schilderlabs/pitown 0.2.1 → 0.2.7

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 { homedir } from "node:os";
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, hostname } 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,42 @@ 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
+ autoResumeTarget: options.autoResumeTarget ?? null
176
+ }), "utf-8").toString("base64url"));
177
+ const child = spawn(runner.command, runner.args, {
418
178
  cwd: options.repoRoot,
419
- env: process.env
179
+ detached: true,
180
+ env: process.env,
181
+ stdio: "ignore"
420
182
  });
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({
183
+ child.unref();
184
+ if (!child.pid) throw new Error(`Failed to launch detached ${options.role} run for ${options.agentId}`);
185
+ writeFileSync(`${getAgentDir(options.artifactsDir, options.agentId)}/latest-invocation.json`, `${JSON.stringify({
426
186
  command: "pi",
427
187
  args: piArgs,
428
- exitCode: piResult.exitCode,
188
+ exitCode: null,
429
189
  sessionDir,
430
- sessionPath: latestSession.sessionPath,
431
- sessionId: latestSession.sessionId
190
+ sessionPath: null,
191
+ sessionId: null,
192
+ processId: child.pid,
193
+ startedAt
432
194
  }, 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
195
  return {
454
- piResult,
455
- latestSession,
456
- completionMessage
196
+ launch: {
197
+ processId: child.pid,
198
+ startedAt
199
+ },
200
+ latestSession: createAgentSessionRecord({ sessionDir })
457
201
  };
458
202
  }
459
203
  function runAgentTurn(options) {
@@ -505,7 +249,8 @@ function runAgentTurn(options) {
505
249
  session: createAgentSessionRecord({
506
250
  sessionDir: latestSession.sessionDir,
507
251
  sessionId: latestSession.sessionId,
508
- sessionPath: latestSession.sessionPath
252
+ sessionPath: latestSession.sessionPath,
253
+ processId: null
509
254
  })
510
255
  }));
511
256
  return {
@@ -533,13 +278,14 @@ function delegateTask(options) {
533
278
  from: options.fromAgentId,
534
279
  body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
535
280
  });
536
- const { piResult, latestSession } = spawnAgentRun({
281
+ const { launch, latestSession } = spawnAgentRun({
537
282
  repoRoot: options.repoRoot,
538
283
  artifactsDir: options.artifactsDir,
539
284
  role: options.role,
540
285
  agentId,
541
286
  appendedSystemPrompt: options.appendedSystemPrompt,
542
287
  extensionPath: options.extensionPath,
288
+ autoResumeTarget: options.completionAutoResumeTarget,
543
289
  task: options.task,
544
290
  taskId: task.taskId
545
291
  });
@@ -552,31 +298,198 @@ function delegateTask(options) {
552
298
  });
553
299
  writeTaskRecord(options.artifactsDir, {
554
300
  ...task,
555
- status: piResult.exitCode === 0 ? "completed" : "blocked",
301
+ status: "running",
556
302
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
557
303
  });
558
304
  return {
559
305
  task: {
560
306
  ...task,
561
- status: piResult.exitCode === 0 ? "completed" : "blocked",
307
+ status: "running",
562
308
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
563
309
  },
564
310
  agentId,
565
- piResult,
311
+ launch,
566
312
  latestSession
567
313
  };
568
314
  }
569
315
 
316
+ //#endregion
317
+ //#region ../core/src/stop.ts
318
+ const DEFAULT_GRACE_MS = 750;
319
+ function sleepMs(ms) {
320
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
321
+ }
322
+ function getLocksDir() {
323
+ return join(homedir(), ".pi-town", "locks");
324
+ }
325
+ function processAlive(pid) {
326
+ if (!Number.isFinite(pid) || pid <= 0) return false;
327
+ try {
328
+ process.kill(pid, 0);
329
+ return true;
330
+ } catch {
331
+ return false;
332
+ }
333
+ }
334
+ function terminateProcess(pid, options) {
335
+ if (!processAlive(pid)) return {
336
+ signal: null,
337
+ exited: true
338
+ };
339
+ try {
340
+ process.kill(pid, "SIGTERM");
341
+ } catch {
342
+ return {
343
+ signal: null,
344
+ exited: !processAlive(pid)
345
+ };
346
+ }
347
+ const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
348
+ const deadline = Date.now() + graceMs;
349
+ while (Date.now() < deadline) {
350
+ if (!processAlive(pid)) return {
351
+ signal: "SIGTERM",
352
+ exited: true
353
+ };
354
+ sleepMs(25);
355
+ }
356
+ if (!options.force) return {
357
+ signal: "SIGTERM",
358
+ exited: !processAlive(pid)
359
+ };
360
+ try {
361
+ process.kill(pid, "SIGKILL");
362
+ } catch {
363
+ return {
364
+ signal: "SIGTERM",
365
+ exited: !processAlive(pid)
366
+ };
367
+ }
368
+ return {
369
+ signal: "SIGKILL",
370
+ exited: !processAlive(pid)
371
+ };
372
+ }
373
+ function readLease(path) {
374
+ try {
375
+ return {
376
+ ...JSON.parse(readFileSync(path, "utf-8")),
377
+ path
378
+ };
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+ function createStopMessage(options) {
384
+ if (options.reason) return options.reason;
385
+ if (options.actorId) return `Stopped by ${options.actorId}`;
386
+ return "Stopped by operator";
387
+ }
388
+ function createStopMessageInput(options) {
389
+ return {
390
+ ...options.actorId === void 0 ? {} : { actorId: options.actorId },
391
+ ...options.reason === void 0 ? {} : { reason: options.reason }
392
+ };
393
+ }
394
+ function createTerminateOptions(options) {
395
+ return {
396
+ ...options.force === void 0 ? {} : { force: options.force },
397
+ ...options.graceMs === void 0 ? {} : { graceMs: options.graceMs }
398
+ };
399
+ }
400
+ function listRepoLeases(repoId) {
401
+ let entries;
402
+ try {
403
+ entries = readdirSync(getLocksDir());
404
+ } catch {
405
+ return [];
406
+ }
407
+ 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);
408
+ }
409
+ function stopRepoLeases(options) {
410
+ const results = listRepoLeases(options.repoId).map((lease) => {
411
+ const termination = terminateProcess(lease.pid, options);
412
+ if (termination.exited) rmSync(lease.path, { force: true });
413
+ return {
414
+ path: lease.path,
415
+ runId: lease.runId,
416
+ repoId: lease.repoId,
417
+ branch: lease.branch,
418
+ processId: lease.pid,
419
+ signal: termination.signal,
420
+ exited: termination.exited
421
+ };
422
+ });
423
+ return {
424
+ results,
425
+ signaledProcesses: results.filter((result) => result.signal !== null).length
426
+ };
427
+ }
428
+ function stopManagedAgents(options) {
429
+ const reason = createStopMessage(createStopMessageInput(options));
430
+ const excluded = new Set(options.excludeAgentIds ?? []);
431
+ const results = listAgentStates(options.artifactsDir).filter((agent) => {
432
+ if (excluded.has(agent.agentId)) return false;
433
+ if (options.agentId && agent.agentId !== options.agentId) return false;
434
+ return ![
435
+ "completed",
436
+ "failed",
437
+ "stopped"
438
+ ].includes(agent.status);
439
+ }).map((state) => {
440
+ const processId = state.session.processId;
441
+ const termination = processId === null ? {
442
+ signal: null,
443
+ exited: true
444
+ } : terminateProcess(processId, createTerminateOptions(options));
445
+ if (state.taskId) updateTaskRecordStatus(options.artifactsDir, state.taskId, "aborted");
446
+ appendAgentMessage({
447
+ artifactsDir: options.artifactsDir,
448
+ agentId: state.agentId,
449
+ box: "outbox",
450
+ from: options.actorId ?? "system",
451
+ body: reason
452
+ });
453
+ writeAgentState(options.artifactsDir, createAgentState({
454
+ ...state,
455
+ status: "stopped",
456
+ lastMessage: reason,
457
+ waitingOn: "stopped",
458
+ blocked: true,
459
+ session: createAgentSessionRecord({
460
+ sessionDir: state.session.sessionDir,
461
+ sessionId: state.session.sessionId,
462
+ sessionPath: state.session.sessionPath,
463
+ processId: null,
464
+ lastAttachedAt: state.session.lastAttachedAt
465
+ })
466
+ }));
467
+ return {
468
+ agentId: state.agentId,
469
+ previousStatus: state.status,
470
+ nextStatus: "stopped",
471
+ processId,
472
+ signal: termination.signal,
473
+ exited: termination.exited
474
+ };
475
+ });
476
+ return {
477
+ results,
478
+ stoppedAgents: results.length,
479
+ signaledProcesses: results.filter((result) => result.signal !== null).length
480
+ };
481
+ }
482
+
570
483
  //#endregion
571
484
  //#region src/agent-id.ts
572
485
  function normalizeAgentId(agentId) {
573
- return agentId === "mayor" ? "leader" : agentId;
486
+ return agentId;
574
487
  }
575
488
 
576
489
  //#endregion
577
490
  //#region src/pi-runtime.ts
578
491
  function isMayorAgent(agentId) {
579
- return agentId === "leader" || agentId === "mayor";
492
+ return agentId === "mayor";
580
493
  }
581
494
  function createPiTownRuntimeArgs(options) {
582
495
  const args = ["--extension", resolvePiTownExtensionPath()];
@@ -589,46 +502,6 @@ function createPiTownRuntimeArgs(options) {
589
502
  return args;
590
503
  }
591
504
 
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
505
  //#endregion
633
506
  //#region src/attach.ts
634
507
  function attachTownAgent(argv = process.argv.slice(2)) {
@@ -658,23 +531,71 @@ function attachTownAgent(argv = process.argv.slice(2)) {
658
531
 
659
532
  //#endregion
660
533
  //#region src/board.ts
534
+ function truncate(text, max) {
535
+ if (!text) return "—";
536
+ const single = text.replace(/\n/g, " ").trim();
537
+ if (single.length <= max) return single;
538
+ return `${single.slice(0, max - 1)}…`;
539
+ }
661
540
  function showTownBoard(argv = process.argv.slice(2)) {
662
541
  const repo = resolveRepoContext(argv);
663
542
  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;
543
+ const tasks = listTaskRecords(repo.artifactsDir);
544
+ const repoName = basename(repo.repoRoot);
545
+ const branch = getCurrentBranch(repo.repoRoot);
546
+ const branchLabel = branch ? ` (${branch})` : "";
547
+ const mayor = agents.find((a) => a.agentId === "mayor");
548
+ const workers = agents.filter((a) => a.agentId !== "mayor");
549
+ const workersByStatus = (status) => workers.filter((a) => a.status === status).length;
550
+ console.log(`[pitown] board — ${repoName}${branchLabel}`);
551
+ if (mayor) {
552
+ const spawned = workers.length;
553
+ const running = workersByStatus("running") + workersByStatus("starting");
554
+ const completed = workersByStatus("completed") + workersByStatus("idle");
555
+ const blocked = workersByStatus("blocked") + workersByStatus("failed");
556
+ const parts = [`${spawned} spawned`];
557
+ if (running > 0) parts.push(`${running} running`);
558
+ if (completed > 0) parts.push(`${completed} done`);
559
+ if (blocked > 0) parts.push(`${blocked} blocked`);
560
+ console.log(`Mayor workers: ${parts.join(", ")}`);
670
561
  }
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}`);
562
+ console.log("");
563
+ console.log("Agents:");
564
+ if (agents.length === 0) console.log(" (no agents)");
565
+ else for (const agent of agents) {
566
+ const id = agent.agentId.padEnd(14);
567
+ const role = agent.role.padEnd(10);
568
+ const status = agent.status.padEnd(10);
569
+ const task = truncate(agent.task, 60);
570
+ const msg = agent.lastMessage ? ` | ${truncate(agent.lastMessage, 40)}` : "";
571
+ const waiting = agent.waitingOn ? ` | waiting on: ${agent.waitingOn}` : "";
572
+ console.log(` ${id}${role}${status}${task}${msg}${waiting}`);
677
573
  }
574
+ console.log("");
575
+ console.log("Tasks:");
576
+ if (tasks.length === 0) console.log(" (no tasks)");
577
+ else for (const task of tasks) {
578
+ const id = task.taskId.padEnd(14);
579
+ const status = task.status.padEnd(12);
580
+ const assignee = (task.assignedAgentId ?? "—").padEnd(14);
581
+ const title = truncate(task.title, 60);
582
+ console.log(` ${id}${status}${assignee}${title}`);
583
+ }
584
+ const metricsPath = join(repo.artifactsDir, "latest", "metrics.json");
585
+ if (existsSync(metricsPath)) try {
586
+ const metrics = JSON.parse(readFileSync(metricsPath, "utf-8"));
587
+ console.log("");
588
+ console.log("Metrics (latest run):");
589
+ console.log(` Interrupt Rate: ${fmt(metrics.interruptRate)}`);
590
+ console.log(` Autonomous Completion Rate: ${fmt(metrics.autonomousCompletionRate)}`);
591
+ console.log(` Context Coverage Score: ${fmt(metrics.contextCoverageScore)}`);
592
+ console.log(` MTTC: ${metrics.meanTimeToCorrectHours != null ? `${metrics.meanTimeToCorrectHours}h` : "—"}`);
593
+ console.log(` Feedback-to-Demo: ${metrics.feedbackToDemoCycleTimeHours != null ? `${metrics.feedbackToDemoCycleTimeHours}h` : "—"}`);
594
+ } catch {}
595
+ }
596
+ function fmt(value) {
597
+ if (value == null) return "—";
598
+ return String(value);
678
599
  }
679
600
 
680
601
  //#endregion
@@ -711,12 +632,13 @@ function continueTownAgent(argv = process.argv.slice(2)) {
711
632
  //#endregion
712
633
  //#region src/delegate.ts
713
634
  function parseDelegateFlags(argv) {
714
- let from = "leader";
635
+ let from = "mayor";
715
636
  let role = "worker";
716
637
  let agentId = null;
717
638
  let task = null;
718
639
  for (let index = 0; index < argv.length; index += 1) {
719
640
  const arg = argv[index];
641
+ if (arg === void 0) continue;
720
642
  if (arg.startsWith("--from=")) {
721
643
  from = arg.slice(7);
722
644
  continue;
@@ -768,14 +690,20 @@ function delegateTownTask(argv = process.argv.slice(2)) {
768
690
  const flags = parseDelegateFlags(repo.rest);
769
691
  const fromAgentId = normalizeAgentId(flags.from);
770
692
  if (readAgentState(repo.artifactsDir, fromAgentId) === null) throw new Error(`Unknown delegating agent: ${fromAgentId}`);
771
- const { agentId, latestSession, piResult, task } = delegateTask({
693
+ const { agentId, latestSession, launch, task } = delegateTask({
772
694
  repoRoot: repo.repoRoot,
773
695
  artifactsDir: repo.artifactsDir,
774
696
  fromAgentId,
775
697
  role: flags.role,
776
698
  agentId: flags.agentId,
777
699
  task: flags.task,
778
- extensionPath: resolvePiTownExtensionPath()
700
+ extensionPath: resolvePiTownExtensionPath(),
701
+ completionAutoResumeTarget: fromAgentId === "mayor" ? {
702
+ agentId: "mayor",
703
+ message: "New agent check-ins arrived. Review the latest board and inbox updates, then decide the next bounded action.",
704
+ extensionPath: resolvePiTownExtensionPath(),
705
+ appendedSystemPrompt: readPiTownMayorPrompt()
706
+ } : null
779
707
  });
780
708
  console.log("[pitown] delegate");
781
709
  console.log(`- repo root: ${repo.repoRoot}`);
@@ -783,8 +711,10 @@ function delegateTownTask(argv = process.argv.slice(2)) {
783
711
  console.log(`- task id: ${task.taskId}`);
784
712
  console.log(`- agent: ${agentId}`);
785
713
  console.log(`- role: ${flags.role}`);
786
- console.log(`- pi exit code: ${piResult.exitCode}`);
714
+ console.log(`- status: ${task.status}`);
715
+ console.log(`- launch pid: ${launch.processId}`);
787
716
  if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
717
+ else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
788
718
  }
789
719
 
790
720
  //#endregion
@@ -806,6 +736,7 @@ function parseLoopCliFlags(argv) {
806
736
  const flags = { noStopOnFailure: false };
807
737
  for (let index = 0; index < argv.length; index += 1) {
808
738
  const arg = argv[index];
739
+ if (arg === void 0) continue;
809
740
  if (arg.startsWith("--repo=")) {
810
741
  flags.repo = arg.slice(7);
811
742
  continue;
@@ -926,11 +857,11 @@ function loopTown(argv = process.argv.slice(2)) {
926
857
  onIterationComplete(iteration) {
927
858
  const board = iteration.boardSnapshot;
928
859
  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";
860
+ const mayorStatus = board.agents.find((a) => a.agentId === "mayor")?.status ?? "unknown";
930
861
  console.log(`[pitown-loop] iteration ${iteration.iteration}/${config.maxIterations} completed (${formatMs(iteration.elapsedMs)})`);
931
862
  console.log(` - pi exit code: ${iteration.controllerResult.piInvocation.exitCode}`);
932
863
  console.log(` - run: ${iteration.controllerResult.runId}`);
933
- console.log(` - board: ${taskSummary}, leader ${leaderStatus}`);
864
+ console.log(` - board: ${taskSummary}, mayor ${mayorStatus}`);
934
865
  console.log(` - metrics: interrupt rate ${iteration.metrics.interruptRate}, autonomous completion ${iteration.metrics.autonomousCompletionRate}`);
935
866
  if (iteration.stopReason) console.log(` - stopping: ${iteration.stopReason}`);
936
867
  else console.log(` - continuing: ${iteration.continueReason}`);
@@ -945,10 +876,10 @@ function loopTown(argv = process.argv.slice(2)) {
945
876
  //#endregion
946
877
  //#region src/mayor.ts
947
878
  function startFreshMayorSession(repoRoot, artifactsDir) {
948
- const sessionDir = getAgentSessionsDir(artifactsDir, "leader");
879
+ const sessionDir = getAgentSessionsDir(artifactsDir, "mayor");
949
880
  writeAgentState(artifactsDir, createAgentState({
950
- agentId: "leader",
951
- role: "leader",
881
+ agentId: "mayor",
882
+ role: "mayor",
952
883
  status: "running",
953
884
  task: "open the mayor session and plan the next steps for this repository",
954
885
  lastMessage: "Mayor session opened",
@@ -958,14 +889,14 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
958
889
  console.log(`- repo root: ${repoRoot}`);
959
890
  console.log("- starting a new mayor session");
960
891
  const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
961
- agentId: "leader",
892
+ agentId: "mayor",
962
893
  sessionDir
963
894
  }), {
964
895
  cwd: repoRoot,
965
896
  env: process.env
966
897
  });
967
- const latestSession = getLatestAgentSession(artifactsDir, "leader");
968
- const previousState = readAgentState(artifactsDir, "leader");
898
+ const latestSession = getLatestAgentSession(artifactsDir, "mayor");
899
+ const previousState = readAgentState(artifactsDir, "mayor");
969
900
  if (previousState !== null) writeAgentState(artifactsDir, createAgentState({
970
901
  ...previousState,
971
902
  status: exitCode === 0 ? "idle" : "blocked",
@@ -983,7 +914,7 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
983
914
  function openTownMayor(argv = process.argv.slice(2)) {
984
915
  const repo = resolveRepoContext(argv);
985
916
  const message = repo.rest.join(" ").trim();
986
- if (readAgentState(repo.artifactsDir, "leader") === null) {
917
+ if (readAgentState(repo.artifactsDir, "mayor") === null) {
987
918
  assertCommandAvailable("pi");
988
919
  if (message) {
989
920
  runTown([
@@ -1029,7 +960,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
1029
960
  from: "human",
1030
961
  body
1031
962
  });
1032
- const deliveredResult = state.role === "leader" ? runAgentTurn({
963
+ const deliveredResult = state.role === "mayor" ? runAgentTurn({
1033
964
  repoRoot: repo.repoRoot,
1034
965
  artifactsDir: repo.artifactsDir,
1035
966
  agentId,
@@ -1047,7 +978,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
1047
978
  console.log(`- queued message: ${body}`);
1048
979
  if (deliveredResult) {
1049
980
  console.log(`- delivered to session: ${deliveredResult.latestSession.sessionPath}`);
1050
- console.log(`- leader response: ${deliveredResult.completionMessage}`);
981
+ console.log(`- mayor response: ${deliveredResult.completionMessage}`);
1051
982
  }
1052
983
  }
1053
984
 
@@ -1093,6 +1024,7 @@ function parseSpawnFlags(argv) {
1093
1024
  let task = null;
1094
1025
  for (let index = 0; index < argv.length; index += 1) {
1095
1026
  const arg = argv[index];
1027
+ if (arg === void 0) continue;
1096
1028
  if (arg.startsWith("--role=")) {
1097
1029
  role = arg.slice(7);
1098
1030
  continue;
@@ -1137,7 +1069,7 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
1137
1069
  const flags = parseSpawnFlags(repo.rest);
1138
1070
  const agentId = flags.agentId ?? `${flags.role}-${Date.now()}`;
1139
1071
  const task = flags.task;
1140
- const { piResult, latestSession } = spawnAgent({
1072
+ const { launch, latestSession } = spawnAgent({
1141
1073
  repoRoot: repo.repoRoot,
1142
1074
  artifactsDir: repo.artifactsDir,
1143
1075
  role: flags.role,
@@ -1149,18 +1081,122 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
1149
1081
  console.log(`- repo root: ${repo.repoRoot}`);
1150
1082
  console.log(`- agent: ${agentId}`);
1151
1083
  console.log(`- role: ${flags.role}`);
1152
- console.log(`- pi exit code: ${piResult.exitCode}`);
1084
+ console.log(`- status: running`);
1085
+ console.log(`- launch pid: ${launch.processId}`);
1153
1086
  if (task) console.log(`- task: ${task}`);
1154
1087
  if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
1088
+ else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
1155
1089
  }
1156
1090
 
1157
1091
  //#endregion
1158
- //#region package.json
1159
- var version = "0.2.1";
1092
+ //#region src/stop.ts
1093
+ function parseStopFlags(argv) {
1094
+ let all = false;
1095
+ let agentId = null;
1096
+ let force = false;
1097
+ for (let index = 0; index < argv.length; index += 1) {
1098
+ const arg = argv[index];
1099
+ if (arg === void 0) continue;
1100
+ if (arg === "--all") {
1101
+ all = true;
1102
+ continue;
1103
+ }
1104
+ if (arg === "--force") {
1105
+ force = true;
1106
+ continue;
1107
+ }
1108
+ if (arg.startsWith("--agent=")) {
1109
+ agentId = normalizeAgentId(arg.slice(8));
1110
+ continue;
1111
+ }
1112
+ if (arg === "--agent") {
1113
+ const value = argv[index + 1];
1114
+ if (!value) throw new Error("Missing value for --agent");
1115
+ agentId = normalizeAgentId(value);
1116
+ index += 1;
1117
+ continue;
1118
+ }
1119
+ throw new Error(`Unknown argument: ${arg}`);
1120
+ }
1121
+ return {
1122
+ all,
1123
+ agentId,
1124
+ force
1125
+ };
1126
+ }
1127
+ function stopRepo(repoRoot, artifactsDir, flags) {
1128
+ const repoId = getRepoIdentity(repoRoot);
1129
+ const leaseResult = flags.agentId ? { signaledProcesses: 0 } : stopRepoLeases({
1130
+ repoId,
1131
+ force: flags.force
1132
+ });
1133
+ const agentResult = stopManagedAgents({
1134
+ artifactsDir,
1135
+ agentId: flags.agentId,
1136
+ actorId: "human",
1137
+ reason: flags.agentId ? `Stopped ${flags.agentId} via pitown stop` : "Stopped via pitown stop",
1138
+ force: flags.force
1139
+ });
1140
+ return {
1141
+ repoLabel: repoRoot,
1142
+ stoppedAgents: agentResult.stoppedAgents,
1143
+ signaledAgentProcesses: agentResult.signaledProcesses,
1144
+ signaledLeaseProcesses: leaseResult.signaledProcesses
1145
+ };
1146
+ }
1147
+ function listTrackedArtifactsDirs() {
1148
+ const reposRoot = getReposRootDir();
1149
+ if (!existsSync(reposRoot)) return [];
1150
+ return readdirSync(reposRoot).map((entry) => getRepoArtifactsDir(entry)).filter((path) => existsSync(path));
1151
+ }
1152
+ function stopTown(argv = process.argv.slice(2)) {
1153
+ const { repo, rest } = parseOptionalRepoFlag(argv);
1154
+ const flags = parseStopFlags(rest);
1155
+ if (flags.all && repo) throw new Error("Do not combine --all with --repo");
1156
+ if (flags.all && flags.agentId) throw new Error("Do not combine --all with --agent");
1157
+ if (flags.all) {
1158
+ const repoSummaries = listTrackedArtifactsDirs().map((artifactsDir) => {
1159
+ const result = stopManagedAgents({
1160
+ artifactsDir,
1161
+ actorId: "human",
1162
+ reason: "Stopped via pitown stop --all",
1163
+ force: flags.force
1164
+ });
1165
+ return {
1166
+ repoLabel: artifactsDir,
1167
+ stoppedAgents: result.stoppedAgents,
1168
+ signaledAgentProcesses: result.signaledProcesses,
1169
+ signaledLeaseProcesses: 0
1170
+ };
1171
+ });
1172
+ const leaseResult = stopRepoLeases({ force: flags.force });
1173
+ const totalAgents = repoSummaries.reduce((sum, result) => sum + result.stoppedAgents, 0);
1174
+ const totalAgentProcesses = repoSummaries.reduce((sum, result) => sum + result.signaledAgentProcesses, 0);
1175
+ const totalLeaseProcesses = leaseResult.signaledProcesses + repoSummaries.reduce((sum, result) => sum + result.signaledLeaseProcesses, 0);
1176
+ console.log("[pitown] stop");
1177
+ console.log("- scope: all repos");
1178
+ console.log(`- stopped agents: ${totalAgents}`);
1179
+ console.log(`- signaled agent processes: ${totalAgentProcesses}`);
1180
+ console.log(`- signaled lease processes: ${totalLeaseProcesses}`);
1181
+ if (repoSummaries.length === 0 && leaseResult.results.length === 0) console.log("- nothing was running");
1182
+ return repoSummaries;
1183
+ }
1184
+ const resolved = repo ? resolveRepoContext(["--repo", repo]) : resolveRepoContext([]);
1185
+ const result = stopRepo(resolved.repoRoot, resolved.artifactsDir, flags);
1186
+ console.log("[pitown] stop");
1187
+ console.log(`- repo root: ${result.repoLabel}`);
1188
+ if (flags.agentId) console.log(`- agent: ${flags.agentId}`);
1189
+ console.log(`- stopped agents: ${result.stoppedAgents}`);
1190
+ console.log(`- signaled agent processes: ${result.signaledAgentProcesses}`);
1191
+ if (!flags.agentId) console.log(`- signaled lease processes: ${result.signaledLeaseProcesses}`);
1192
+ if (result.stoppedAgents === 0 && result.signaledAgentProcesses === 0 && result.signaledLeaseProcesses === 0) console.log("- nothing was running");
1193
+ return [result];
1194
+ }
1160
1195
 
1161
1196
  //#endregion
1162
1197
  //#region src/version.ts
1163
- const CLI_VERSION = version;
1198
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
1199
+ const CLI_VERSION = packageJson.version;
1164
1200
 
1165
1201
  //#endregion
1166
1202
  //#region src/index.ts
@@ -1169,27 +1205,23 @@ function printHelp(showAdvanced = false) {
1169
1205
  "pitown",
1170
1206
  "",
1171
1207
  "Usage:",
1172
- " pitown",
1173
- " pitown mayor [--repo <path>] [\"message\"]",
1208
+ " pitown [--repo <path>] [\"message\"]",
1174
1209
  " pitown board [--repo <path>]",
1175
1210
  " pitown peek [--repo <path>] [agent]",
1176
- " pitown msg [--repo <path>] mayor \"message\"",
1211
+ " pitown msg [--repo <path>] <agent> \"message\"",
1177
1212
  " pitown status [--repo <path>]",
1213
+ " pitown stop [--repo <path>] [--agent <id>] [--all] [--force]",
1178
1214
  " pitown doctor",
1179
- " pitown help",
1180
- " pitown help --all",
1181
- " pitown --help",
1182
- " pitown -v",
1183
1215
  " pitown --version",
1184
1216
  "",
1185
1217
  "Mayor workflow:",
1186
1218
  " pitown",
1187
- " pitown mayor",
1188
- " pitown mayor \"plan the next milestones\"",
1219
+ " pitown \"plan the next milestones\"",
1189
1220
  " /plan",
1190
1221
  " /todos",
1191
1222
  "",
1192
1223
  "Inside the mayor session, `/plan` toggles read-only planning mode and `/todos` shows the captured plan.",
1224
+ "Aliases still work: `pitown mayor`, `pitown help`, `pitown --help`, `pitown -v`.",
1193
1225
  "",
1194
1226
  "If --repo is omitted, Pi Town uses the repo for the current working directory when possible.",
1195
1227
  ...showAdvanced ? [
@@ -1224,7 +1256,8 @@ function runCli(argv = process.argv.slice(2)) {
1224
1256
  break;
1225
1257
  case "run": {
1226
1258
  const result = runTown(args);
1227
- if (result.piInvocation.exitCode !== 0) process.exitCode = result.piInvocation.exitCode;
1259
+ const latestIteration = result.iterations[result.iterations.length - 1];
1260
+ if (latestIteration && latestIteration.controllerResult.piInvocation.exitCode !== 0) process.exitCode = latestIteration.controllerResult.piInvocation.exitCode;
1228
1261
  break;
1229
1262
  }
1230
1263
  case "loop": {
@@ -1260,6 +1293,9 @@ function runCli(argv = process.argv.slice(2)) {
1260
1293
  case "status":
1261
1294
  showTownStatus(args);
1262
1295
  break;
1296
+ case "stop":
1297
+ stopTown(args);
1298
+ break;
1263
1299
  case "watch":
1264
1300
  watchTown(args);
1265
1301
  break;