@schilderlabs/pitown-core 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,145 +1,17 @@
1
+ import { C as appendJsonl, S as writeAgentState, _ as getLatestAgentSession, a as readTaskRecord, b as readAgentMessages, c as appendAgentMessage, d as getAgentDir, f as getAgentMailboxPath, g as getAgentsDir, h as getAgentStatePath, i as listTaskRecords, l as createAgentSessionRecord, m as getAgentSessionsDir, n as getTaskPath, o as updateTaskRecordStatus, p as getAgentSessionPath, r as getTasksDir, s as writeTaskRecord, t as createTaskRecord, u as createAgentState, v as getSessionIdFromPath, w as readJsonl, x as readAgentState, y as listAgentStates } from "./tasks-BfQm-7-O.mjs";
2
+ import { createRequire } from "node:module";
1
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join, resolve } from "node:path";
4
+ import { basename, join, resolve } from "node:path";
3
5
  import { homedir, hostname } from "node:os";
4
6
  import { createHash, randomUUID } from "node:crypto";
5
- import { spawnSync } from "node:child_process";
7
+ import { spawn, spawnSync } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
6
9
 
7
- //#region src/events.ts
8
- function appendJsonl(filePath, value) {
9
- mkdirSync(dirname(filePath), { recursive: true });
10
- writeFileSync(filePath, `${JSON.stringify(value)}\n`, {
11
- encoding: "utf-8",
12
- flag: "a"
13
- });
14
- }
15
- function readJsonl(filePath) {
16
- try {
17
- return readFileSync(filePath, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
18
- } catch {
19
- return [];
20
- }
21
- }
22
-
23
- //#endregion
24
- //#region src/agents.ts
25
- function writeJson$3(path, value) {
26
- mkdirSync(dirname(path), { recursive: true });
27
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
28
- }
29
- function ensureMailbox(path) {
30
- mkdirSync(dirname(path), { recursive: true });
31
- if (!existsSync(path)) writeFileSync(path, "", "utf-8");
32
- }
33
- function getAgentsDir(artifactsDir) {
34
- return join(artifactsDir, "agents");
35
- }
36
- function getAgentDir(artifactsDir, agentId) {
37
- return join(getAgentsDir(artifactsDir), agentId);
38
- }
39
- function getAgentStatePath(artifactsDir, agentId) {
40
- return join(getAgentDir(artifactsDir, agentId), "state.json");
41
- }
42
- function getAgentSessionPath(artifactsDir, agentId) {
43
- return join(getAgentDir(artifactsDir, agentId), "session.json");
44
- }
45
- function getAgentMailboxPath(artifactsDir, agentId, box) {
46
- return join(getAgentDir(artifactsDir, agentId), `${box}.jsonl`);
47
- }
48
- function getAgentSessionsDir(artifactsDir, agentId) {
49
- return join(getAgentDir(artifactsDir, agentId), "sessions");
50
- }
51
- function getSessionIdFromPath(sessionPath) {
52
- if (!sessionPath) return null;
53
- return /_([0-9a-f-]+)\.jsonl$/i.exec(sessionPath)?.[1] ?? null;
54
- }
55
- function createAgentSessionRecord(input) {
56
- return {
57
- runtime: "pi",
58
- persisted: true,
59
- sessionDir: input?.sessionDir ?? null,
60
- sessionId: input?.sessionId ?? null,
61
- sessionPath: input?.sessionPath ?? null,
62
- lastAttachedAt: input?.lastAttachedAt ?? null
63
- };
64
- }
65
- function createAgentState(input) {
66
- return {
67
- agentId: input.agentId,
68
- role: input.role,
69
- status: input.status,
70
- taskId: input.taskId ?? null,
71
- task: input.task ?? null,
72
- branch: input.branch ?? null,
73
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
74
- lastMessage: input.lastMessage ?? null,
75
- waitingOn: input.waitingOn ?? null,
76
- blocked: input.blocked ?? false,
77
- runId: input.runId ?? null,
78
- session: input.session ?? createAgentSessionRecord()
79
- };
80
- }
81
- function writeAgentState(artifactsDir, state) {
82
- mkdirSync(getAgentDir(artifactsDir, state.agentId), { recursive: true });
83
- ensureMailbox(getAgentMailboxPath(artifactsDir, state.agentId, "inbox"));
84
- ensureMailbox(getAgentMailboxPath(artifactsDir, state.agentId, "outbox"));
85
- writeJson$3(getAgentStatePath(artifactsDir, state.agentId), state);
86
- writeJson$3(getAgentSessionPath(artifactsDir, state.agentId), state.session);
87
- }
88
- function readAgentState(artifactsDir, agentId) {
89
- const statePath = getAgentStatePath(artifactsDir, agentId);
90
- try {
91
- return JSON.parse(readFileSync(statePath, "utf-8"));
92
- } catch {
93
- return null;
94
- }
95
- }
96
- function listAgentStates(artifactsDir) {
97
- const agentsDir = getAgentsDir(artifactsDir);
98
- let entries;
99
- try {
100
- entries = readdirSync(agentsDir);
101
- } catch {
102
- return [];
103
- }
104
- return entries.map((entry) => readAgentState(artifactsDir, entry)).filter((state) => state !== null).sort((left, right) => left.agentId.localeCompare(right.agentId));
105
- }
106
- function appendAgentMessage(input) {
107
- const record = {
108
- box: input.box,
109
- from: input.from,
110
- body: input.body,
111
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
112
- };
113
- appendJsonl(getAgentMailboxPath(input.artifactsDir, input.agentId, input.box), record);
114
- return record;
115
- }
116
- function readAgentMessages(artifactsDir, agentId, box) {
117
- return readJsonl(getAgentMailboxPath(artifactsDir, agentId, box));
118
- }
119
- function getLatestAgentSession(artifactsDir, agentId) {
120
- const sessionDir = getAgentSessionsDir(artifactsDir, agentId);
121
- let entries;
122
- try {
123
- entries = readdirSync(sessionDir);
124
- } catch {
125
- return createAgentSessionRecord({ sessionDir });
126
- }
127
- const latestSessionPath = entries.filter((entry) => entry.endsWith(".jsonl")).sort().at(-1) ?? null;
128
- if (latestSessionPath === null) return createAgentSessionRecord({ sessionDir });
129
- const sessionPath = join(sessionDir, latestSessionPath);
130
- return createAgentSessionRecord({
131
- sessionDir,
132
- sessionPath,
133
- sessionId: getSessionIdFromPath(sessionPath)
134
- });
135
- }
136
-
137
- //#endregion
138
10
  //#region src/lease.ts
139
11
  function sanitize$1(value) {
140
12
  return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
141
13
  }
142
- function processAlive(pid) {
14
+ function processAlive$1(pid) {
143
15
  if (!Number.isFinite(pid) || pid <= 0) return false;
144
16
  try {
145
17
  process.kill(pid, 0);
@@ -162,7 +34,7 @@ function acquireRepoLease(runId, repoId, branch) {
162
34
  };
163
35
  try {
164
36
  const current = JSON.parse(readFileSync(leasePath, "utf-8"));
165
- if (processAlive(current.pid)) throw new Error(`Pi Town lease already held by pid ${current.pid} on ${current.hostname} for run ${current.runId}.`);
37
+ if (processAlive$1(current.pid)) throw new Error(`Pi Town lease already held by pid ${current.pid} on ${current.hostname} for run ${current.runId}.`);
166
38
  rmSync(leasePath, { force: true });
167
39
  } catch (error) {
168
40
  if (error.code !== "ENOENT") {
@@ -346,7 +218,7 @@ function createPiInvocationArgs$1(input) {
346
218
  args.push("-p", input.prompt);
347
219
  return args;
348
220
  }
349
- function writeJson$2(path, value) {
221
+ function writeJson$1(path, value) {
350
222
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
351
223
  }
352
224
  function writeText(path, value) {
@@ -355,7 +227,7 @@ function writeText(path, value) {
355
227
  function createPiPrompt(input) {
356
228
  const goal = input.goal ?? "continue from current scaffold state";
357
229
  if (input.planPath) return [
358
- "You are the Pi Town leader agent for this repository.",
230
+ "You are the Pi Town mayor agent for this repository.",
359
231
  "Coordinate the next bounded unit of work, keep updates concise, and leave a durable artifact trail.",
360
232
  "",
361
233
  "Read the private plans in:",
@@ -369,7 +241,7 @@ function createPiPrompt(input) {
369
241
  "Keep any persisted run artifacts high-signal and avoid copying private plan contents into them."
370
242
  ].join("\n");
371
243
  return [
372
- "You are the Pi Town leader agent for this repository.",
244
+ "You are the Pi Town mayor agent for this repository.",
373
245
  "Coordinate the next bounded unit of work, keep updates concise, and leave a durable artifact trail.",
374
246
  "",
375
247
  `Work in the repository at: ${input.repoRoot}`,
@@ -455,21 +327,21 @@ function runController(options) {
455
327
  goal,
456
328
  recommendedPlanDir
457
329
  });
458
- const existingLeaderState = readAgentState(artifactsDir, "leader");
459
- const existingLeaderSession = existingLeaderState?.session.sessionPath || existingLeaderState?.session.sessionDir ? existingLeaderState.session : getLatestAgentSession(artifactsDir, "leader");
460
- const leaderSessionDir = existingLeaderSession.sessionDir ?? getAgentSessionsDir(artifactsDir, "leader");
461
- const leaderState = createAgentState({
462
- agentId: "leader",
463
- role: "leader",
330
+ const existingMayorState = readAgentState(artifactsDir, "mayor");
331
+ const existingMayorSession = existingMayorState?.session.sessionPath || existingMayorState?.session.sessionDir ? existingMayorState.session : getLatestAgentSession(artifactsDir, "mayor");
332
+ const mayorSessionDir = existingMayorSession.sessionDir ?? getAgentSessionsDir(artifactsDir, "mayor");
333
+ const mayorState = createAgentState({
334
+ agentId: "mayor",
335
+ role: "mayor",
464
336
  status: "starting",
465
337
  task: goal,
466
338
  branch,
467
- lastMessage: goal ? `Starting leader run for goal: ${goal}` : "Starting leader run",
339
+ lastMessage: goal ? `Starting mayor run for goal: ${goal}` : "Starting mayor run",
468
340
  runId,
469
341
  session: createAgentSessionRecord({
470
- sessionDir: leaderSessionDir,
471
- sessionId: existingLeaderSession.sessionId,
472
- sessionPath: existingLeaderSession.sessionPath
342
+ sessionDir: mayorSessionDir,
343
+ sessionId: existingMayorSession.sessionId,
344
+ sessionPath: existingMayorSession.sessionPath
473
345
  })
474
346
  });
475
347
  assertPiRuntimeAvailable(piCommand);
@@ -477,11 +349,11 @@ function runController(options) {
477
349
  mkdirSync(latestDir, { recursive: true });
478
350
  writeText(join(runDir, "questions.jsonl"), "");
479
351
  writeText(join(runDir, "interventions.jsonl"), "");
480
- writeJson$2(join(runDir, "agent-state.json"), {
352
+ writeJson$1(join(runDir, "agent-state.json"), {
481
353
  status: "starting",
482
354
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
483
355
  });
484
- writeAgentState(artifactsDir, leaderState);
356
+ writeAgentState(artifactsDir, mayorState);
485
357
  const lease = acquireRepoLease(runId, repoId, branch);
486
358
  try {
487
359
  const manifest = createManifest({
@@ -512,13 +384,13 @@ function runController(options) {
512
384
  createdAt: piStartedAt
513
385
  });
514
386
  writeAgentState(artifactsDir, createAgentState({
515
- ...leaderState,
387
+ ...mayorState,
516
388
  status: "running",
517
- lastMessage: goal ? `Leader working on: ${goal}` : "Leader working"
389
+ lastMessage: goal ? `Mayor working on: ${goal}` : "Mayor working"
518
390
  }));
519
391
  const piResult = runCommandSync(piCommand, createPiInvocationArgs$1({
520
- sessionDir: leaderState.session.sessionPath === null ? leaderSessionDir : null,
521
- sessionPath: leaderState.session.sessionPath,
392
+ sessionDir: mayorState.session.sessionPath === null ? mayorSessionDir : null,
393
+ sessionPath: mayorState.session.sessionPath,
522
394
  prompt,
523
395
  appendedSystemPrompt: options.appendedSystemPrompt,
524
396
  extensionPath: options.extensionPath
@@ -527,7 +399,7 @@ function runController(options) {
527
399
  env: process.env
528
400
  });
529
401
  const piEndedAt = (/* @__PURE__ */ new Date()).toISOString();
530
- const latestLeaderSession = getLatestAgentSession(artifactsDir, "leader");
402
+ const latestMayorSession = getLatestAgentSession(artifactsDir, "mayor");
531
403
  writeText(stdoutPath, piResult.stdout);
532
404
  writeText(stderrPath, piResult.stderr);
533
405
  const piInvocation = {
@@ -536,9 +408,9 @@ function runController(options) {
536
408
  repoRoot,
537
409
  planPath,
538
410
  goal,
539
- sessionDir: latestLeaderSession.sessionDir,
540
- sessionId: latestLeaderSession.sessionId,
541
- sessionPath: latestLeaderSession.sessionPath,
411
+ sessionDir: latestMayorSession.sessionDir,
412
+ sessionId: latestMayorSession.sessionId,
413
+ sessionPath: latestMayorSession.sessionPath,
542
414
  startedAt: piStartedAt,
543
415
  endedAt: piEndedAt,
544
416
  exitCode: piResult.exitCode,
@@ -546,7 +418,7 @@ function runController(options) {
546
418
  stderrPath,
547
419
  promptSummary: planPath ? "Read private plan path and continue from current scaffold state." : "Continue from current scaffold state without a configured private plan path."
548
420
  };
549
- writeJson$2(join(runDir, "pi-invocation.json"), piInvocation);
421
+ writeJson$1(join(runDir, "pi-invocation.json"), piInvocation);
550
422
  appendJsonl(join(runDir, "events.jsonl"), {
551
423
  type: "pi_invocation_finished",
552
424
  runId,
@@ -572,27 +444,27 @@ function runController(options) {
572
444
  stopReason: piInvocation.exitCode === 0 ? "pi invocation completed" : `pi invocation exited with code ${piInvocation.exitCode}`,
573
445
  piExitCode: piInvocation.exitCode
574
446
  };
575
- writeJson$2(join(runDir, "manifest.json"), finalManifest);
576
- writeJson$2(join(runDir, "metrics.json"), metrics);
577
- writeJson$2(join(runDir, "run-summary.json"), summary);
578
- writeJson$2(join(runDir, "agent-state.json"), {
447
+ writeJson$1(join(runDir, "manifest.json"), finalManifest);
448
+ writeJson$1(join(runDir, "metrics.json"), metrics);
449
+ writeJson$1(join(runDir, "run-summary.json"), summary);
450
+ writeJson$1(join(runDir, "agent-state.json"), {
579
451
  status: summary.success ? "completed" : "failed",
580
452
  updatedAt: piEndedAt,
581
453
  exitCode: piInvocation.exitCode
582
454
  });
583
- writeJson$2(join(latestDir, "manifest.json"), finalManifest);
584
- writeJson$2(join(latestDir, "metrics.json"), metrics);
585
- writeJson$2(join(latestDir, "run-summary.json"), summary);
455
+ writeJson$1(join(latestDir, "manifest.json"), finalManifest);
456
+ writeJson$1(join(latestDir, "metrics.json"), metrics);
457
+ writeJson$1(join(latestDir, "run-summary.json"), summary);
586
458
  writeAgentState(artifactsDir, createAgentState({
587
- ...leaderState,
459
+ ...mayorState,
588
460
  status: piInvocation.exitCode === 0 ? "idle" : "blocked",
589
- lastMessage: piInvocation.exitCode === 0 ? "Leader run completed and is ready for the next instruction" : `Leader run stopped with exit code ${piInvocation.exitCode}`,
461
+ lastMessage: piInvocation.exitCode === 0 ? "Mayor run completed and is ready for the next instruction" : `Mayor run stopped with exit code ${piInvocation.exitCode}`,
590
462
  blocked: piInvocation.exitCode !== 0,
591
463
  waitingOn: piInvocation.exitCode === 0 ? null : "human-or-follow-up-run",
592
464
  session: createAgentSessionRecord({
593
- sessionDir: latestLeaderSession.sessionDir,
594
- sessionId: latestLeaderSession.sessionId,
595
- sessionPath: latestLeaderSession.sessionPath
465
+ sessionDir: latestMayorSession.sessionDir,
466
+ sessionId: latestMayorSession.sessionId,
467
+ sessionPath: latestMayorSession.sessionPath
596
468
  })
597
469
  }));
598
470
  appendJsonl(join(runDir, "events.jsonl"), {
@@ -638,74 +510,39 @@ function resolveInterrupt(interrupt, options) {
638
510
  };
639
511
  }
640
512
 
641
- //#endregion
642
- //#region src/tasks.ts
643
- function writeJson$1(path, value) {
644
- mkdirSync(dirname(path), { recursive: true });
645
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
646
- }
647
- function getTasksDir(artifactsDir) {
648
- return join(artifactsDir, "tasks");
649
- }
650
- function getTaskPath(artifactsDir, taskId) {
651
- return join(getTasksDir(artifactsDir), `${taskId}.json`);
652
- }
653
- function createTaskRecord(input) {
654
- const now = (/* @__PURE__ */ new Date()).toISOString();
655
- return {
656
- taskId: input.taskId,
657
- title: input.title,
658
- status: input.status,
659
- role: input.role,
660
- assignedAgentId: input.assignedAgentId,
661
- createdBy: input.createdBy,
662
- createdAt: now,
663
- updatedAt: now
664
- };
665
- }
666
- function writeTaskRecord(artifactsDir, task) {
667
- writeJson$1(getTaskPath(artifactsDir, task.taskId), task);
668
- }
669
- function updateTaskRecordStatus(artifactsDir, taskId, status) {
670
- const task = readTaskRecord(artifactsDir, taskId);
671
- if (task === null) return null;
672
- const updatedTask = {
673
- ...task,
674
- status,
675
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
676
- };
677
- writeTaskRecord(artifactsDir, updatedTask);
678
- return updatedTask;
679
- }
680
- function readTaskRecord(artifactsDir, taskId) {
681
- const path = getTaskPath(artifactsDir, taskId);
682
- try {
683
- return JSON.parse(readFileSync(path, "utf-8"));
684
- } catch {
685
- return null;
686
- }
687
- }
688
- function listTaskRecords(artifactsDir) {
689
- const tasksDir = getTasksDir(artifactsDir);
690
- let entries;
691
- try {
692
- entries = readdirSync(tasksDir);
693
- } catch {
694
- return [];
695
- }
696
- 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));
697
- }
698
-
699
513
  //#endregion
700
514
  //#region src/loop.ts
701
515
  const DEFAULT_MAX_ITERATIONS = 10;
702
516
  const DEFAULT_MAX_WALL_TIME_MS = 36e5;
517
+ const DEFAULT_BACKGROUND_POLL_MS = 250;
703
518
  function createLoopId() {
704
519
  return `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
705
520
  }
706
521
  function writeJson(path, value) {
707
522
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
708
523
  }
524
+ function sleepMs$1(ms) {
525
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
526
+ }
527
+ function hasBackgroundWork(board) {
528
+ return board.agents.some((agent) => agent.agentId !== "mayor" && (agent.status === "queued" || agent.status === "running" || agent.status === "starting")) || board.tasks.some((task) => task.status === "queued" || task.status === "running");
529
+ }
530
+ function waitForBackgroundWorkToSettle(input) {
531
+ const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_BACKGROUND_POLL_MS;
532
+ let board = snapshotBoard(input.artifactsDir);
533
+ while (hasBackgroundWork(board)) {
534
+ if (Date.now() - input.loopStartedAt >= input.maxWallTimeMs) return {
535
+ timedOut: true,
536
+ board
537
+ };
538
+ sleepMs$1(pollIntervalMs);
539
+ board = snapshotBoard(input.artifactsDir);
540
+ }
541
+ return {
542
+ timedOut: false,
543
+ board
544
+ };
545
+ }
709
546
  function snapshotBoard(artifactsDir) {
710
547
  const tasks = listTaskRecords(artifactsDir);
711
548
  const agents = listAgentStates(artifactsDir);
@@ -720,8 +557,8 @@ function snapshotBoard(artifactsDir) {
720
557
  blocked: agent.blocked
721
558
  })),
722
559
  allTasksCompleted: tasks.length > 0 && tasks.every((task) => task.status === "completed"),
723
- allRemainingTasksBlocked: tasks.length > 0 && tasks.every((task) => task.status === "completed" || task.status === "blocked"),
724
- leaderBlocked: agents.find((agent) => agent.agentId === "leader")?.blocked === true,
560
+ allRemainingTasksBlocked: tasks.length > 0 && tasks.every((task) => task.status === "completed" || task.status === "blocked" || task.status === "aborted"),
561
+ mayorBlocked: agents.find((agent) => agent.agentId === "mayor")?.blocked === true,
725
562
  hasQueuedOrRunningWork: agents.some((agent) => agent.status === "queued" || agent.status === "running" || agent.status === "starting") || tasks.some((task) => task.status === "queued" || task.status === "running")
726
563
  };
727
564
  }
@@ -742,8 +579,13 @@ function evaluateStopCondition(input) {
742
579
  stopReason: "all-tasks-completed",
743
580
  continueReason: null
744
581
  };
745
- if (input.board.leaderBlocked) return {
746
- stopReason: "leader-blocked",
582
+ if (input.board.mayorBlocked) return {
583
+ stopReason: "mayor-blocked",
584
+ continueReason: null
585
+ };
586
+ const mayor = input.board.agents.find((agent) => agent.agentId === "mayor");
587
+ if (input.stopOnMayorIdleNoWork && mayor && !input.board.hasQueuedOrRunningWork && input.board.tasks.length === 0 && mayor.status === "idle") return {
588
+ stopReason: "mayor-idle-no-work",
747
589
  continueReason: null
748
590
  };
749
591
  if (input.board.allRemainingTasksBlocked) return {
@@ -757,7 +599,7 @@ function evaluateStopCondition(input) {
757
599
  const reasons = [];
758
600
  if (input.board.hasQueuedOrRunningWork) reasons.push("queued or running work remains");
759
601
  if (input.board.tasks.length === 0) reasons.push("no tasks tracked yet");
760
- if (reasons.length === 0) reasons.push("leader idle, no stop condition met");
602
+ if (reasons.length === 0) reasons.push("mayor idle, no stop condition met");
761
603
  return {
762
604
  stopReason: null,
763
605
  continueReason: reasons.join("; ")
@@ -813,6 +655,7 @@ function runLoop(options) {
813
655
  const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
814
656
  const maxWallTimeMs = options.maxWallTimeMs ?? DEFAULT_MAX_WALL_TIME_MS;
815
657
  const stopOnPiFailure = options.stopOnPiFailure ?? true;
658
+ const stopOnMayorIdleNoWork = options.stopOnMayorIdleNoWork ?? false;
816
659
  const interruptRateThreshold = options.interruptRateThreshold ?? null;
817
660
  const loopId = createLoopId();
818
661
  const artifactsDir = options.runOptions.artifactsDir;
@@ -821,6 +664,7 @@ function runLoop(options) {
821
664
  const loopStartedAt = Date.now();
822
665
  const iterations = [];
823
666
  let finalStopReason = "max-iterations-reached";
667
+ let needsMayorFollowUp = false;
824
668
  appendJsonl(join(loopDir, "events.jsonl"), {
825
669
  type: "loop_started",
826
670
  loopId,
@@ -830,6 +674,26 @@ function runLoop(options) {
830
674
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
831
675
  });
832
676
  for (let iteration = 1; iteration <= maxIterations; iteration++) {
677
+ if (needsMayorFollowUp) {
678
+ const settled = waitForBackgroundWorkToSettle({
679
+ artifactsDir,
680
+ maxWallTimeMs,
681
+ loopStartedAt
682
+ });
683
+ appendJsonl(join(loopDir, "events.jsonl"), {
684
+ type: "loop_background_work_settled",
685
+ loopId,
686
+ iteration,
687
+ timedOut: settled.timedOut,
688
+ boardSnapshot: settled.board,
689
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
690
+ });
691
+ if (settled.timedOut) {
692
+ finalStopReason = "max-wall-time-reached";
693
+ break;
694
+ }
695
+ needsMayorFollowUp = false;
696
+ }
833
697
  const iterationStart = Date.now();
834
698
  let controllerResult;
835
699
  try {
@@ -856,6 +720,7 @@ function runLoop(options) {
856
720
  maxWallTimeMs,
857
721
  piExitCode: controllerResult.piInvocation.exitCode,
858
722
  stopOnPiFailure,
723
+ stopOnMayorIdleNoWork,
859
724
  board,
860
725
  metrics,
861
726
  interruptRateThreshold
@@ -894,9 +759,11 @@ function runLoop(options) {
894
759
  finalStopReason = stopReason;
895
760
  break;
896
761
  }
762
+ needsMayorFollowUp = hasBackgroundWork(board);
897
763
  }
898
764
  const totalElapsedMs = Date.now() - loopStartedAt;
899
- const finalBoard = iterations.length > 0 ? iterations[iterations.length - 1].boardSnapshot : snapshotBoard(artifactsDir);
765
+ const lastIteration = iterations.at(-1);
766
+ const finalBoard = lastIteration ? lastIteration.boardSnapshot : snapshotBoard(artifactsDir);
900
767
  const aggregate = aggregateMetrics(iterations);
901
768
  const loopResult = {
902
769
  loopId,
@@ -938,11 +805,29 @@ function createPiInvocationArgs(input) {
938
805
  if (input.prompt) args.push("-p", input.prompt);
939
806
  return args;
940
807
  }
808
+ function createDetachedRunnerInvocation(encodedPayload) {
809
+ if (fileURLToPath(import.meta.url).endsWith(".ts")) {
810
+ const require = createRequire(import.meta.url);
811
+ return {
812
+ command: process.execPath,
813
+ args: [
814
+ "--import",
815
+ require.resolve("tsx"),
816
+ fileURLToPath(new URL("./agent-runner.ts", import.meta.url)),
817
+ encodedPayload
818
+ ]
819
+ };
820
+ }
821
+ return {
822
+ command: process.execPath,
823
+ args: [fileURLToPath(new URL("./agent-runner.mjs", import.meta.url)), encodedPayload]
824
+ };
825
+ }
941
826
  function createRolePrompt(input) {
942
827
  const task = input.task ?? "pick the next bounded task from the current repo context";
943
828
  switch (input.role) {
944
- case "leader": return [
945
- "You are the Pi Town leader.",
829
+ case "mayor": return [
830
+ "You are the Pi Town mayor.",
946
831
  "You coordinate work for this repository and act as the primary human-facing agent.",
947
832
  "",
948
833
  `Repository: ${input.repoRoot}`,
@@ -989,6 +874,7 @@ function resolveAgentSession(agentId, artifactsDir) {
989
874
  sessionDir,
990
875
  sessionId,
991
876
  sessionPath,
877
+ processId: state.session.processId,
992
878
  lastAttachedAt: (/* @__PURE__ */ new Date()).toISOString()
993
879
  })
994
880
  };
@@ -1013,6 +899,7 @@ function queueAgentMessage(input) {
1013
899
  sessionDir: state.session.sessionDir ?? getAgentSessionsDir(input.artifactsDir, input.agentId),
1014
900
  sessionId: state.session.sessionId,
1015
901
  sessionPath: state.session.sessionPath,
902
+ processId: state.session.processId,
1016
903
  lastAttachedAt: state.session.lastAttachedAt
1017
904
  })
1018
905
  }));
@@ -1021,7 +908,7 @@ function updateAgentStatus(input) {
1021
908
  const state = readAgentState(input.artifactsDir, input.agentId);
1022
909
  if (state === null) throw new Error(`Unknown agent: ${input.agentId}`);
1023
910
  if (state.taskId) {
1024
- const taskStatus = input.status === "completed" ? "completed" : input.status === "blocked" || input.status === "failed" ? "blocked" : input.status === "running" || input.status === "queued" ? "running" : null;
911
+ const taskStatus = input.status === "completed" ? "completed" : input.status === "blocked" || input.status === "failed" ? "blocked" : input.status === "stopped" ? "aborted" : input.status === "running" || input.status === "queued" ? "running" : null;
1025
912
  if (taskStatus) updateTaskRecordStatus(input.artifactsDir, state.taskId, taskStatus);
1026
913
  }
1027
914
  writeAgentState(input.artifactsDir, createAgentState({
@@ -1069,46 +956,41 @@ function spawnAgentRun(options) {
1069
956
  appendedSystemPrompt: options.appendedSystemPrompt,
1070
957
  extensionPath: options.extensionPath
1071
958
  });
1072
- const piResult = runCommandSync("pi", piArgs, {
959
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
960
+ const runner = createDetachedRunnerInvocation(Buffer.from(JSON.stringify({
961
+ repoRoot: options.repoRoot,
962
+ artifactsDir: options.artifactsDir,
963
+ agentId: options.agentId,
964
+ role: options.role,
965
+ task: options.task,
966
+ taskId: options.taskId ?? null,
967
+ sessionDir,
968
+ piArgs
969
+ }), "utf-8").toString("base64url"));
970
+ const child = spawn(runner.command, runner.args, {
1073
971
  cwd: options.repoRoot,
1074
- env: process.env
972
+ detached: true,
973
+ env: process.env,
974
+ stdio: "ignore"
1075
975
  });
1076
- const latestSession = getLatestAgentSession(options.artifactsDir, options.agentId);
1077
- const agentArtifactsDir = getAgentDir(options.artifactsDir, options.agentId);
1078
- writeFileSync(`${agentArtifactsDir}/latest-stdout.txt`, piResult.stdout, "utf-8");
1079
- writeFileSync(`${agentArtifactsDir}/latest-stderr.txt`, piResult.stderr, "utf-8");
1080
- writeFileSync(`${agentArtifactsDir}/latest-invocation.json`, `${JSON.stringify({
976
+ child.unref();
977
+ if (!child.pid) throw new Error(`Failed to launch detached ${options.role} run for ${options.agentId}`);
978
+ writeFileSync(`${getAgentDir(options.artifactsDir, options.agentId)}/latest-invocation.json`, `${JSON.stringify({
1081
979
  command: "pi",
1082
980
  args: piArgs,
1083
- exitCode: piResult.exitCode,
981
+ exitCode: null,
1084
982
  sessionDir,
1085
- sessionPath: latestSession.sessionPath,
1086
- sessionId: latestSession.sessionId
983
+ sessionPath: null,
984
+ sessionId: null,
985
+ processId: child.pid,
986
+ startedAt
1087
987
  }, null, 2)}\n`, "utf-8");
1088
- const completionMessage = piResult.stdout.trim() || (piResult.exitCode === 0 ? `${options.role} run completed` : `${options.role} run exited with code ${piResult.exitCode}`);
1089
- appendAgentMessage({
1090
- artifactsDir: options.artifactsDir,
1091
- agentId: options.agentId,
1092
- box: "outbox",
1093
- from: options.agentId,
1094
- body: completionMessage
1095
- });
1096
- writeAgentState(options.artifactsDir, createAgentState({
1097
- ...state,
1098
- status: piResult.exitCode === 0 ? "idle" : "blocked",
1099
- lastMessage: completionMessage,
1100
- blocked: piResult.exitCode !== 0,
1101
- waitingOn: piResult.exitCode === 0 ? null : "human-or-follow-up-run",
1102
- session: createAgentSessionRecord({
1103
- sessionDir: latestSession.sessionDir,
1104
- sessionId: latestSession.sessionId,
1105
- sessionPath: latestSession.sessionPath
1106
- })
1107
- }));
1108
988
  return {
1109
- piResult,
1110
- latestSession,
1111
- completionMessage
989
+ launch: {
990
+ processId: child.pid,
991
+ startedAt
992
+ },
993
+ latestSession: createAgentSessionRecord({ sessionDir })
1112
994
  };
1113
995
  }
1114
996
  function runAgentTurn(options) {
@@ -1160,7 +1042,8 @@ function runAgentTurn(options) {
1160
1042
  session: createAgentSessionRecord({
1161
1043
  sessionDir: latestSession.sessionDir,
1162
1044
  sessionId: latestSession.sessionId,
1163
- sessionPath: latestSession.sessionPath
1045
+ sessionPath: latestSession.sessionPath,
1046
+ processId: null
1164
1047
  })
1165
1048
  }));
1166
1049
  return {
@@ -1188,7 +1071,7 @@ function delegateTask(options) {
1188
1071
  from: options.fromAgentId,
1189
1072
  body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
1190
1073
  });
1191
- const { piResult, latestSession } = spawnAgentRun({
1074
+ const { launch, latestSession } = spawnAgentRun({
1192
1075
  repoRoot: options.repoRoot,
1193
1076
  artifactsDir: options.artifactsDir,
1194
1077
  role: options.role,
@@ -1207,21 +1090,188 @@ function delegateTask(options) {
1207
1090
  });
1208
1091
  writeTaskRecord(options.artifactsDir, {
1209
1092
  ...task,
1210
- status: piResult.exitCode === 0 ? "completed" : "blocked",
1093
+ status: "running",
1211
1094
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1212
1095
  });
1213
1096
  return {
1214
1097
  task: {
1215
1098
  ...task,
1216
- status: piResult.exitCode === 0 ? "completed" : "blocked",
1099
+ status: "running",
1217
1100
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1218
1101
  },
1219
1102
  agentId,
1220
- piResult,
1103
+ launch,
1221
1104
  latestSession
1222
1105
  };
1223
1106
  }
1224
1107
 
1225
1108
  //#endregion
1226
- export { acquireRepoLease, appendAgentMessage, appendJsonl, assertCommandAvailable, assertSuccess, computeAutonomousCompletionRate, computeContextCoverageScore, computeFeedbackToDemoCycleTime, computeInterruptRate, computeMeanTimeToCorrect, computeMetrics, createAgentSessionRecord, createAgentState, createInterruptRecord, createPiAuthHelpMessage, createRepoSlug, createRolePrompt, createTaskRecord, delegateTask, detectPiAuthFailure, evaluateStopCondition, getAgentDir, getAgentMailboxPath, getAgentSessionPath, getAgentSessionsDir, getAgentStatePath, getAgentsDir, getCurrentBranch, getLatestAgentSession, getRepoIdentity, getRepoRoot, getSessionIdFromPath, getTaskPath, getTasksDir, isGitRepo, listAgentStates, listTaskRecords, queueAgentMessage, readAgentMessages, readAgentState, readJsonl, readTaskRecord, resolveAgentSession, resolveInterrupt, runAgentTurn, runCommandInteractive, runCommandSync, runController, runLoop, snapshotBoard, spawnAgentRun, updateAgentStatus, updateTaskRecordStatus, writeAgentState, writeTaskRecord };
1109
+ //#region src/stop.ts
1110
+ const DEFAULT_GRACE_MS = 750;
1111
+ function sleepMs(ms) {
1112
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1113
+ }
1114
+ function getLocksDir() {
1115
+ return join(homedir(), ".pi-town", "locks");
1116
+ }
1117
+ function processAlive(pid) {
1118
+ if (!Number.isFinite(pid) || pid <= 0) return false;
1119
+ try {
1120
+ process.kill(pid, 0);
1121
+ return true;
1122
+ } catch {
1123
+ return false;
1124
+ }
1125
+ }
1126
+ function terminateProcess(pid, options) {
1127
+ if (!processAlive(pid)) return {
1128
+ signal: null,
1129
+ exited: true
1130
+ };
1131
+ try {
1132
+ process.kill(pid, "SIGTERM");
1133
+ } catch {
1134
+ return {
1135
+ signal: null,
1136
+ exited: !processAlive(pid)
1137
+ };
1138
+ }
1139
+ const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
1140
+ const deadline = Date.now() + graceMs;
1141
+ while (Date.now() < deadline) {
1142
+ if (!processAlive(pid)) return {
1143
+ signal: "SIGTERM",
1144
+ exited: true
1145
+ };
1146
+ sleepMs(25);
1147
+ }
1148
+ if (!options.force) return {
1149
+ signal: "SIGTERM",
1150
+ exited: !processAlive(pid)
1151
+ };
1152
+ try {
1153
+ process.kill(pid, "SIGKILL");
1154
+ } catch {
1155
+ return {
1156
+ signal: "SIGTERM",
1157
+ exited: !processAlive(pid)
1158
+ };
1159
+ }
1160
+ return {
1161
+ signal: "SIGKILL",
1162
+ exited: !processAlive(pid)
1163
+ };
1164
+ }
1165
+ function readLease(path) {
1166
+ try {
1167
+ return {
1168
+ ...JSON.parse(readFileSync(path, "utf-8")),
1169
+ path
1170
+ };
1171
+ } catch {
1172
+ return null;
1173
+ }
1174
+ }
1175
+ function createStopMessage(options) {
1176
+ if (options.reason) return options.reason;
1177
+ if (options.actorId) return `Stopped by ${options.actorId}`;
1178
+ return "Stopped by operator";
1179
+ }
1180
+ function createStopMessageInput(options) {
1181
+ return {
1182
+ ...options.actorId === void 0 ? {} : { actorId: options.actorId },
1183
+ ...options.reason === void 0 ? {} : { reason: options.reason }
1184
+ };
1185
+ }
1186
+ function createTerminateOptions(options) {
1187
+ return {
1188
+ ...options.force === void 0 ? {} : { force: options.force },
1189
+ ...options.graceMs === void 0 ? {} : { graceMs: options.graceMs }
1190
+ };
1191
+ }
1192
+ function listRepoLeases(repoId) {
1193
+ let entries;
1194
+ try {
1195
+ entries = readdirSync(getLocksDir());
1196
+ } catch {
1197
+ return [];
1198
+ }
1199
+ 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);
1200
+ }
1201
+ function stopRepoLeases(options) {
1202
+ const results = listRepoLeases(options.repoId).map((lease) => {
1203
+ const termination = terminateProcess(lease.pid, options);
1204
+ if (termination.exited) rmSync(lease.path, { force: true });
1205
+ return {
1206
+ path: lease.path,
1207
+ runId: lease.runId,
1208
+ repoId: lease.repoId,
1209
+ branch: lease.branch,
1210
+ processId: lease.pid,
1211
+ signal: termination.signal,
1212
+ exited: termination.exited
1213
+ };
1214
+ });
1215
+ return {
1216
+ results,
1217
+ signaledProcesses: results.filter((result) => result.signal !== null).length
1218
+ };
1219
+ }
1220
+ function stopManagedAgents(options) {
1221
+ const reason = createStopMessage(createStopMessageInput(options));
1222
+ const excluded = new Set(options.excludeAgentIds ?? []);
1223
+ const results = listAgentStates(options.artifactsDir).filter((agent) => {
1224
+ if (excluded.has(agent.agentId)) return false;
1225
+ if (options.agentId && agent.agentId !== options.agentId) return false;
1226
+ return ![
1227
+ "completed",
1228
+ "failed",
1229
+ "stopped"
1230
+ ].includes(agent.status);
1231
+ }).map((state) => {
1232
+ const processId = state.session.processId;
1233
+ const termination = processId === null ? {
1234
+ signal: null,
1235
+ exited: true
1236
+ } : terminateProcess(processId, createTerminateOptions(options));
1237
+ if (state.taskId) updateTaskRecordStatus(options.artifactsDir, state.taskId, "aborted");
1238
+ appendAgentMessage({
1239
+ artifactsDir: options.artifactsDir,
1240
+ agentId: state.agentId,
1241
+ box: "outbox",
1242
+ from: options.actorId ?? "system",
1243
+ body: reason
1244
+ });
1245
+ writeAgentState(options.artifactsDir, createAgentState({
1246
+ ...state,
1247
+ status: "stopped",
1248
+ lastMessage: reason,
1249
+ waitingOn: "stopped",
1250
+ blocked: true,
1251
+ session: createAgentSessionRecord({
1252
+ sessionDir: state.session.sessionDir,
1253
+ sessionId: state.session.sessionId,
1254
+ sessionPath: state.session.sessionPath,
1255
+ processId: null,
1256
+ lastAttachedAt: state.session.lastAttachedAt
1257
+ })
1258
+ }));
1259
+ return {
1260
+ agentId: state.agentId,
1261
+ previousStatus: state.status,
1262
+ nextStatus: "stopped",
1263
+ processId,
1264
+ signal: termination.signal,
1265
+ exited: termination.exited
1266
+ };
1267
+ });
1268
+ return {
1269
+ results,
1270
+ stoppedAgents: results.length,
1271
+ signaledProcesses: results.filter((result) => result.signal !== null).length
1272
+ };
1273
+ }
1274
+
1275
+ //#endregion
1276
+ export { acquireRepoLease, appendAgentMessage, appendJsonl, assertCommandAvailable, assertSuccess, computeAutonomousCompletionRate, computeContextCoverageScore, computeFeedbackToDemoCycleTime, computeInterruptRate, computeMeanTimeToCorrect, computeMetrics, createAgentSessionRecord, createAgentState, createInterruptRecord, createPiAuthHelpMessage, createRepoSlug, createRolePrompt, createTaskRecord, delegateTask, detectPiAuthFailure, evaluateStopCondition, getAgentDir, getAgentMailboxPath, getAgentSessionPath, getAgentSessionsDir, getAgentStatePath, getAgentsDir, getCurrentBranch, getLatestAgentSession, getRepoIdentity, getRepoRoot, getSessionIdFromPath, getTaskPath, getTasksDir, isGitRepo, listAgentStates, listRepoLeases, listTaskRecords, queueAgentMessage, readAgentMessages, readAgentState, readJsonl, readTaskRecord, resolveAgentSession, resolveInterrupt, runAgentTurn, runCommandInteractive, runCommandSync, runController, runLoop, snapshotBoard, spawnAgentRun, stopManagedAgents, stopRepoLeases, updateAgentStatus, updateTaskRecordStatus, writeAgentState, writeTaskRecord };
1227
1277
  //# sourceMappingURL=index.mjs.map