@schilderlabs/pitown-core 0.1.0 → 0.2.1

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,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import { homedir, hostname } from "node:os";
4
4
  import { createHash, randomUUID } from "node:crypto";
@@ -20,6 +20,120 @@ function readJsonl(filePath) {
20
20
  }
21
21
  }
22
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
+
23
137
  //#endregion
24
138
  //#region src/lease.ts
25
139
  function sanitize$1(value) {
@@ -120,6 +234,16 @@ function computeMetrics(input) {
120
234
  };
121
235
  }
122
236
 
237
+ //#endregion
238
+ //#region src/pi.ts
239
+ function detectPiAuthFailure(stderr, stdout) {
240
+ const output = `${stdout}\n${stderr}`.toLowerCase();
241
+ return output.includes("no models available") || output.includes("not authenticated") || output.includes("authentication") || output.includes("/login") || output.includes("api key");
242
+ }
243
+ function createPiAuthHelpMessage() {
244
+ return "Pi appears to be installed but not authenticated or configured. Verify Pi works first: pi -p \"hello\". Either set an API key or run `pi` and use `/login`.";
245
+ }
246
+
123
247
  //#endregion
124
248
  //#region src/shell.ts
125
249
  function runCommandSync(command, args, options) {
@@ -136,6 +260,22 @@ function runCommandSync(command, args, options) {
136
260
  exitCode: result.status ?? 1
137
261
  };
138
262
  }
263
+ function assertCommandAvailable(command) {
264
+ const result = spawnSync(command, ["--help"], {
265
+ encoding: "utf-8",
266
+ stdio: "ignore"
267
+ });
268
+ if (result.error instanceof Error) throw new Error(result.error.message);
269
+ }
270
+ function runCommandInteractive(command, args, options) {
271
+ const result = spawnSync(command, args, {
272
+ cwd: options?.cwd,
273
+ env: options?.env,
274
+ stdio: "inherit"
275
+ });
276
+ if (result.error instanceof Error) throw new Error(result.error.message);
277
+ return result.status ?? 1;
278
+ }
139
279
  function assertSuccess(result, context) {
140
280
  if (result.exitCode === 0) return;
141
281
  const details = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
@@ -196,7 +336,17 @@ function createRepoSlug(repoId, repoRoot) {
196
336
  function createRunId() {
197
337
  return `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
198
338
  }
199
- function writeJson(path, value) {
339
+ function createPiInvocationArgs$1(input) {
340
+ const args = [];
341
+ if (input.extensionPath) args.push("--extension", input.extensionPath);
342
+ if (input.appendedSystemPrompt) args.push("--append-system-prompt", input.appendedSystemPrompt);
343
+ if (input.sessionPath) args.push("--session", input.sessionPath);
344
+ else if (input.sessionDir) args.push("--session-dir", input.sessionDir);
345
+ else throw new Error("Pi invocation requires a session path or session directory");
346
+ args.push("-p", input.prompt);
347
+ return args;
348
+ }
349
+ function writeJson$2(path, value) {
200
350
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
201
351
  }
202
352
  function writeText(path, value) {
@@ -205,6 +355,9 @@ function writeText(path, value) {
205
355
  function createPiPrompt(input) {
206
356
  const goal = input.goal ?? "continue from current scaffold state";
207
357
  if (input.planPath) return [
358
+ "You are the Pi Town leader agent for this repository.",
359
+ "Coordinate the next bounded unit of work, keep updates concise, and leave a durable artifact trail.",
360
+ "",
208
361
  "Read the private plans in:",
209
362
  `- ${input.planPath}`,
210
363
  "",
@@ -216,6 +369,9 @@ function createPiPrompt(input) {
216
369
  "Keep any persisted run artifacts high-signal and avoid copying private plan contents into them."
217
370
  ].join("\n");
218
371
  return [
372
+ "You are the Pi Town leader agent for this repository.",
373
+ "Coordinate the next bounded unit of work, keep updates concise, and leave a durable artifact trail.",
374
+ "",
219
375
  `Work in the repository at: ${input.repoRoot}`,
220
376
  `Goal: ${goal}`,
221
377
  "No private plan path is configured for this run.",
@@ -223,6 +379,23 @@ function createPiPrompt(input) {
223
379
  "Continue from the current scaffold state."
224
380
  ].join("\n");
225
381
  }
382
+ function assertPiRuntimeAvailable(piCommand) {
383
+ try {
384
+ assertCommandAvailable(piCommand);
385
+ } catch (error) {
386
+ if (piCommand === "pi") throw new Error([
387
+ "Pi Town requires the `pi` CLI to run `pitown run`.",
388
+ "Install Pi: npm install -g @mariozechner/pi-coding-agent",
389
+ "Then authenticate Pi and verify it works: pi -p \"hello\"",
390
+ `Details: ${error.message}`
391
+ ].join("\n"));
392
+ throw new Error([
393
+ `Pi Town could not execute the configured Pi command: ${piCommand}`,
394
+ "Make sure the command exists on PATH or points to an executable file.",
395
+ `Details: ${error.message}`
396
+ ].join("\n"));
397
+ }
398
+ }
226
399
  function createManifest(input) {
227
400
  return {
228
401
  runId: input.runId,
@@ -248,12 +421,13 @@ function createManifest(input) {
248
421
  function createSummary(input) {
249
422
  const success = input.exitCode === 0;
250
423
  const recommendation = input.recommendedPlanDir === null ? "" : ` No plan path was configured. Recommended private plans location: ${input.recommendedPlanDir}.`;
424
+ const authHelp = success || !detectPiAuthFailure(input.stderr, input.stdout) ? "" : ` ${createPiAuthHelpMessage()}`;
251
425
  return {
252
426
  runId: input.runId,
253
427
  mode: input.mode,
254
428
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
255
429
  success,
256
- message: success ? `Pi invocation completed.${recommendation}` : `Pi invocation failed.${recommendation}`,
430
+ message: success ? `Pi invocation completed.${recommendation}` : `Pi invocation failed.${authHelp}${recommendation}`,
257
431
  piExitCode: input.exitCode,
258
432
  recommendedPlanDir: input.recommendedPlanDir
259
433
  };
@@ -281,14 +455,33 @@ function runController(options) {
281
455
  goal,
282
456
  recommendedPlanDir
283
457
  });
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",
464
+ status: "starting",
465
+ task: goal,
466
+ branch,
467
+ lastMessage: goal ? `Starting leader run for goal: ${goal}` : "Starting leader run",
468
+ runId,
469
+ session: createAgentSessionRecord({
470
+ sessionDir: leaderSessionDir,
471
+ sessionId: existingLeaderSession.sessionId,
472
+ sessionPath: existingLeaderSession.sessionPath
473
+ })
474
+ });
475
+ assertPiRuntimeAvailable(piCommand);
284
476
  mkdirSync(runDir, { recursive: true });
285
477
  mkdirSync(latestDir, { recursive: true });
286
478
  writeText(join(runDir, "questions.jsonl"), "");
287
479
  writeText(join(runDir, "interventions.jsonl"), "");
288
- writeJson(join(runDir, "agent-state.json"), {
480
+ writeJson$2(join(runDir, "agent-state.json"), {
289
481
  status: "starting",
290
482
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
291
483
  });
484
+ writeAgentState(artifactsDir, leaderState);
292
485
  const lease = acquireRepoLease(runId, repoId, branch);
293
486
  try {
294
487
  const manifest = createManifest({
@@ -318,15 +511,23 @@ function runController(options) {
318
511
  command: piCommand,
319
512
  createdAt: piStartedAt
320
513
  });
321
- const piResult = runCommandSync(piCommand, [
322
- "--no-session",
323
- "-p",
324
- prompt
325
- ], {
514
+ writeAgentState(artifactsDir, createAgentState({
515
+ ...leaderState,
516
+ status: "running",
517
+ lastMessage: goal ? `Leader working on: ${goal}` : "Leader working"
518
+ }));
519
+ const piResult = runCommandSync(piCommand, createPiInvocationArgs$1({
520
+ sessionDir: leaderState.session.sessionPath === null ? leaderSessionDir : null,
521
+ sessionPath: leaderState.session.sessionPath,
522
+ prompt,
523
+ appendedSystemPrompt: options.appendedSystemPrompt,
524
+ extensionPath: options.extensionPath
525
+ }), {
326
526
  cwd: repoRoot,
327
527
  env: process.env
328
528
  });
329
529
  const piEndedAt = (/* @__PURE__ */ new Date()).toISOString();
530
+ const latestLeaderSession = getLatestAgentSession(artifactsDir, "leader");
330
531
  writeText(stdoutPath, piResult.stdout);
331
532
  writeText(stderrPath, piResult.stderr);
332
533
  const piInvocation = {
@@ -335,6 +536,9 @@ function runController(options) {
335
536
  repoRoot,
336
537
  planPath,
337
538
  goal,
539
+ sessionDir: latestLeaderSession.sessionDir,
540
+ sessionId: latestLeaderSession.sessionId,
541
+ sessionPath: latestLeaderSession.sessionPath,
338
542
  startedAt: piStartedAt,
339
543
  endedAt: piEndedAt,
340
544
  exitCode: piResult.exitCode,
@@ -342,7 +546,7 @@ function runController(options) {
342
546
  stderrPath,
343
547
  promptSummary: planPath ? "Read private plan path and continue from current scaffold state." : "Continue from current scaffold state without a configured private plan path."
344
548
  };
345
- writeJson(join(runDir, "pi-invocation.json"), piInvocation);
549
+ writeJson$2(join(runDir, "pi-invocation.json"), piInvocation);
346
550
  appendJsonl(join(runDir, "events.jsonl"), {
347
551
  type: "pi_invocation_finished",
348
552
  runId,
@@ -358,6 +562,8 @@ function runController(options) {
358
562
  runId,
359
563
  mode,
360
564
  exitCode: piInvocation.exitCode,
565
+ stdout: piResult.stdout,
566
+ stderr: piResult.stderr,
361
567
  recommendedPlanDir
362
568
  });
363
569
  const finalManifest = {
@@ -366,17 +572,29 @@ function runController(options) {
366
572
  stopReason: piInvocation.exitCode === 0 ? "pi invocation completed" : `pi invocation exited with code ${piInvocation.exitCode}`,
367
573
  piExitCode: piInvocation.exitCode
368
574
  };
369
- writeJson(join(runDir, "manifest.json"), finalManifest);
370
- writeJson(join(runDir, "metrics.json"), metrics);
371
- writeJson(join(runDir, "run-summary.json"), summary);
372
- writeJson(join(runDir, "agent-state.json"), {
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"), {
373
579
  status: summary.success ? "completed" : "failed",
374
580
  updatedAt: piEndedAt,
375
581
  exitCode: piInvocation.exitCode
376
582
  });
377
- writeJson(join(latestDir, "manifest.json"), finalManifest);
378
- writeJson(join(latestDir, "metrics.json"), metrics);
379
- writeJson(join(latestDir, "run-summary.json"), summary);
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);
586
+ writeAgentState(artifactsDir, createAgentState({
587
+ ...leaderState,
588
+ 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}`,
590
+ blocked: piInvocation.exitCode !== 0,
591
+ waitingOn: piInvocation.exitCode === 0 ? null : "human-or-follow-up-run",
592
+ session: createAgentSessionRecord({
593
+ sessionDir: latestLeaderSession.sessionDir,
594
+ sessionId: latestLeaderSession.sessionId,
595
+ sessionPath: latestLeaderSession.sessionPath
596
+ })
597
+ }));
380
598
  appendJsonl(join(runDir, "events.jsonl"), {
381
599
  type: "run_finished",
382
600
  runId,
@@ -421,5 +639,589 @@ function resolveInterrupt(interrupt, options) {
421
639
  }
422
640
 
423
641
  //#endregion
424
- export { acquireRepoLease, appendJsonl, assertSuccess, computeAutonomousCompletionRate, computeContextCoverageScore, computeFeedbackToDemoCycleTime, computeInterruptRate, computeMeanTimeToCorrect, computeMetrics, createInterruptRecord, createRepoSlug, getCurrentBranch, getRepoIdentity, getRepoRoot, isGitRepo, readJsonl, resolveInterrupt, runCommandSync, runController };
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
+ //#endregion
700
+ //#region src/loop.ts
701
+ const DEFAULT_MAX_ITERATIONS = 10;
702
+ const DEFAULT_MAX_WALL_TIME_MS = 36e5;
703
+ function createLoopId() {
704
+ return `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
705
+ }
706
+ function writeJson(path, value) {
707
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
708
+ }
709
+ function snapshotBoard(artifactsDir) {
710
+ const tasks = listTaskRecords(artifactsDir);
711
+ const agents = listAgentStates(artifactsDir);
712
+ return {
713
+ tasks: tasks.map((task) => ({
714
+ taskId: task.taskId,
715
+ status: task.status
716
+ })),
717
+ agents: agents.map((agent) => ({
718
+ agentId: agent.agentId,
719
+ status: agent.status,
720
+ blocked: agent.blocked
721
+ })),
722
+ 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,
725
+ hasQueuedOrRunningWork: agents.some((agent) => agent.status === "queued" || agent.status === "running" || agent.status === "starting") || tasks.some((task) => task.status === "queued" || task.status === "running")
726
+ };
727
+ }
728
+ function evaluateStopCondition(input) {
729
+ if (input.iteration >= input.maxIterations) return {
730
+ stopReason: "max-iterations-reached",
731
+ continueReason: null
732
+ };
733
+ if (input.elapsedMs >= input.maxWallTimeMs) return {
734
+ stopReason: "max-wall-time-reached",
735
+ continueReason: null
736
+ };
737
+ if (input.stopOnPiFailure && input.piExitCode !== 0) return {
738
+ stopReason: "pi-exit-nonzero",
739
+ continueReason: null
740
+ };
741
+ if (input.board.allTasksCompleted) return {
742
+ stopReason: "all-tasks-completed",
743
+ continueReason: null
744
+ };
745
+ if (input.board.leaderBlocked) return {
746
+ stopReason: "leader-blocked",
747
+ continueReason: null
748
+ };
749
+ if (input.board.allRemainingTasksBlocked) return {
750
+ stopReason: "all-remaining-tasks-blocked",
751
+ continueReason: null
752
+ };
753
+ if (input.interruptRateThreshold !== null && input.metrics.interruptRate > input.interruptRateThreshold) return {
754
+ stopReason: "high-interrupt-rate",
755
+ continueReason: null
756
+ };
757
+ const reasons = [];
758
+ if (input.board.hasQueuedOrRunningWork) reasons.push("queued or running work remains");
759
+ 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");
761
+ return {
762
+ stopReason: null,
763
+ continueReason: reasons.join("; ")
764
+ };
765
+ }
766
+ function aggregateMetrics(iterations) {
767
+ if (iterations.length === 0) return computeMetrics({
768
+ taskAttempts: [],
769
+ interrupts: []
770
+ });
771
+ let totalTaskAttempts = 0;
772
+ let totalCompletedTasks = 0;
773
+ let totalInterrupts = 0;
774
+ let totalObservedInterruptCategories = 0;
775
+ let totalCoveredInterruptCategories = 0;
776
+ let interruptRateSum = 0;
777
+ let autonomousCompletionRateSum = 0;
778
+ let contextCoverageScoreSum = 0;
779
+ let mttcValues = [];
780
+ let ftdValues = [];
781
+ for (const iter of iterations) {
782
+ const m = iter.metrics;
783
+ totalTaskAttempts += m.totals.taskAttempts;
784
+ totalCompletedTasks += m.totals.completedTasks;
785
+ totalInterrupts += m.totals.interrupts;
786
+ totalObservedInterruptCategories += m.totals.observedInterruptCategories;
787
+ totalCoveredInterruptCategories += m.totals.coveredInterruptCategories;
788
+ interruptRateSum += m.interruptRate;
789
+ autonomousCompletionRateSum += m.autonomousCompletionRate;
790
+ contextCoverageScoreSum += m.contextCoverageScore;
791
+ if (m.meanTimeToCorrectHours !== null) mttcValues.push(m.meanTimeToCorrectHours);
792
+ if (m.feedbackToDemoCycleTimeHours !== null) ftdValues.push(m.feedbackToDemoCycleTimeHours);
793
+ }
794
+ const count = iterations.length;
795
+ const round = (v) => Math.round(v * 1e3) / 1e3;
796
+ const avg = (values) => values.length === 0 ? null : round(values.reduce((s, v) => s + v, 0) / values.length);
797
+ return {
798
+ interruptRate: round(interruptRateSum / count),
799
+ autonomousCompletionRate: round(autonomousCompletionRateSum / count),
800
+ contextCoverageScore: round(contextCoverageScoreSum / count),
801
+ meanTimeToCorrectHours: avg(mttcValues),
802
+ feedbackToDemoCycleTimeHours: avg(ftdValues),
803
+ totals: {
804
+ taskAttempts: totalTaskAttempts,
805
+ completedTasks: totalCompletedTasks,
806
+ interrupts: totalInterrupts,
807
+ observedInterruptCategories: totalObservedInterruptCategories,
808
+ coveredInterruptCategories: totalCoveredInterruptCategories
809
+ }
810
+ };
811
+ }
812
+ function runLoop(options) {
813
+ const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
814
+ const maxWallTimeMs = options.maxWallTimeMs ?? DEFAULT_MAX_WALL_TIME_MS;
815
+ const stopOnPiFailure = options.stopOnPiFailure ?? true;
816
+ const interruptRateThreshold = options.interruptRateThreshold ?? null;
817
+ const loopId = createLoopId();
818
+ const artifactsDir = options.runOptions.artifactsDir;
819
+ const loopDir = join(artifactsDir, "loops", loopId);
820
+ mkdirSync(loopDir, { recursive: true });
821
+ const loopStartedAt = Date.now();
822
+ const iterations = [];
823
+ let finalStopReason = "max-iterations-reached";
824
+ appendJsonl(join(loopDir, "events.jsonl"), {
825
+ type: "loop_started",
826
+ loopId,
827
+ maxIterations,
828
+ maxWallTimeMs,
829
+ stopOnPiFailure,
830
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
831
+ });
832
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
833
+ const iterationStart = Date.now();
834
+ let controllerResult;
835
+ try {
836
+ controllerResult = runController(options.runOptions);
837
+ } catch (error) {
838
+ appendJsonl(join(loopDir, "events.jsonl"), {
839
+ type: "loop_iteration_error",
840
+ loopId,
841
+ iteration,
842
+ error: error.message,
843
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
844
+ });
845
+ finalStopReason = "pi-exit-nonzero";
846
+ break;
847
+ }
848
+ const iterationElapsedMs = Date.now() - iterationStart;
849
+ const totalElapsedMs = Date.now() - loopStartedAt;
850
+ const board = snapshotBoard(artifactsDir);
851
+ const metrics = controllerResult.metrics;
852
+ const { stopReason, continueReason } = evaluateStopCondition({
853
+ iteration,
854
+ maxIterations,
855
+ elapsedMs: totalElapsedMs,
856
+ maxWallTimeMs,
857
+ piExitCode: controllerResult.piInvocation.exitCode,
858
+ stopOnPiFailure,
859
+ board,
860
+ metrics,
861
+ interruptRateThreshold
862
+ });
863
+ const iterationResult = {
864
+ iteration,
865
+ controllerResult,
866
+ boardSnapshot: board,
867
+ metrics,
868
+ elapsedMs: iterationElapsedMs,
869
+ continueReason,
870
+ stopReason
871
+ };
872
+ iterations.push(iterationResult);
873
+ writeJson(join(loopDir, `iteration-${iteration}.json`), {
874
+ iteration,
875
+ runId: controllerResult.runId,
876
+ boardSnapshot: board,
877
+ metrics,
878
+ elapsedMs: iterationElapsedMs,
879
+ continueReason,
880
+ stopReason
881
+ });
882
+ appendJsonl(join(loopDir, "events.jsonl"), {
883
+ type: "loop_iteration_completed",
884
+ loopId,
885
+ iteration,
886
+ runId: controllerResult.runId,
887
+ piExitCode: controllerResult.piInvocation.exitCode,
888
+ stopReason,
889
+ continueReason,
890
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
891
+ });
892
+ if (options.onIterationComplete) options.onIterationComplete(iterationResult);
893
+ if (stopReason !== null) {
894
+ finalStopReason = stopReason;
895
+ break;
896
+ }
897
+ }
898
+ const totalElapsedMs = Date.now() - loopStartedAt;
899
+ const finalBoard = iterations.length > 0 ? iterations[iterations.length - 1].boardSnapshot : snapshotBoard(artifactsDir);
900
+ const aggregate = aggregateMetrics(iterations);
901
+ const loopResult = {
902
+ loopId,
903
+ iterations,
904
+ stopReason: finalStopReason,
905
+ totalIterations: iterations.length,
906
+ totalElapsedMs,
907
+ finalBoardSnapshot: finalBoard,
908
+ aggregateMetrics: aggregate
909
+ };
910
+ writeJson(join(loopDir, "loop-summary.json"), {
911
+ loopId,
912
+ stopReason: finalStopReason,
913
+ totalIterations: iterations.length,
914
+ totalElapsedMs,
915
+ finalBoardSnapshot: finalBoard,
916
+ aggregateMetrics: aggregate
917
+ });
918
+ appendJsonl(join(loopDir, "events.jsonl"), {
919
+ type: "loop_finished",
920
+ loopId,
921
+ stopReason: finalStopReason,
922
+ totalIterations: iterations.length,
923
+ totalElapsedMs,
924
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
925
+ });
926
+ return loopResult;
927
+ }
928
+
929
+ //#endregion
930
+ //#region src/orchestration.ts
931
+ function createPiInvocationArgs(input) {
932
+ const args = [];
933
+ if (input.extensionPath) args.push("--extension", input.extensionPath);
934
+ if (input.appendedSystemPrompt) args.push("--append-system-prompt", input.appendedSystemPrompt);
935
+ if (input.sessionPath) args.push("--session", input.sessionPath);
936
+ else if (input.sessionDir) args.push("--session-dir", input.sessionDir);
937
+ else throw new Error("Pi invocation requires a session path or session directory");
938
+ if (input.prompt) args.push("-p", input.prompt);
939
+ return args;
940
+ }
941
+ function createRolePrompt(input) {
942
+ const task = input.task ?? "pick the next bounded task from the current repo context";
943
+ switch (input.role) {
944
+ case "leader": return [
945
+ "You are the Pi Town leader.",
946
+ "You coordinate work for this repository and act as the primary human-facing agent.",
947
+ "",
948
+ `Repository: ${input.repoRoot}`,
949
+ `Task: ${task}`,
950
+ "Keep updates concise, choose bounded next steps, and leave a durable artifact trail."
951
+ ].join("\n");
952
+ case "reviewer": return [
953
+ "You are the Pi Town reviewer.",
954
+ "You review work for correctness, safety, and completeness.",
955
+ "",
956
+ `Repository: ${input.repoRoot}`,
957
+ `Task: ${task}`,
958
+ "Focus on validation confidence, regressions, and whether the output is ready for a human handoff."
959
+ ].join("\n");
960
+ case "docs-keeper": return [
961
+ "You are the Pi Town docs keeper.",
962
+ "You summarize outcomes, blockers, and continuity in compact factual language.",
963
+ "",
964
+ `Repository: ${input.repoRoot}`,
965
+ `Task: ${task}`,
966
+ "Keep the output concise and useful for the next run or human review."
967
+ ].join("\n");
968
+ default: return [
969
+ "You are the Pi Town worker.",
970
+ "You implement one bounded task at a time.",
971
+ "",
972
+ `Repository: ${input.repoRoot}`,
973
+ `Task: ${task}`,
974
+ "Keep scope tight, prefer explicit validations, and summarize what changed and what still needs follow-up."
975
+ ].join("\n");
976
+ }
977
+ }
978
+ function resolveAgentSession(agentId, artifactsDir) {
979
+ const state = readAgentState(artifactsDir, agentId);
980
+ if (state === null) throw new Error(`Unknown agent: ${agentId}`);
981
+ const latestSession = getLatestAgentSession(artifactsDir, agentId);
982
+ const sessionPath = state.session.sessionPath ?? latestSession.sessionPath;
983
+ const sessionId = state.session.sessionId ?? latestSession.sessionId;
984
+ const sessionDir = state.session.sessionDir ?? latestSession.sessionDir ?? getAgentSessionsDir(artifactsDir, agentId);
985
+ if (sessionPath === null) throw new Error(`Agent ${agentId} does not have a persisted Pi session yet.`);
986
+ return {
987
+ state,
988
+ session: createAgentSessionRecord({
989
+ sessionDir,
990
+ sessionId,
991
+ sessionPath,
992
+ lastAttachedAt: (/* @__PURE__ */ new Date()).toISOString()
993
+ })
994
+ };
995
+ }
996
+ function queueAgentMessage(input) {
997
+ const state = readAgentState(input.artifactsDir, input.agentId);
998
+ if (state === null) throw new Error(`Unknown agent: ${input.agentId}`);
999
+ appendAgentMessage({
1000
+ artifactsDir: input.artifactsDir,
1001
+ agentId: input.agentId,
1002
+ box: "inbox",
1003
+ from: input.from,
1004
+ body: input.body
1005
+ });
1006
+ writeAgentState(input.artifactsDir, createAgentState({
1007
+ ...state,
1008
+ status: state.status === "idle" ? "queued" : state.status,
1009
+ lastMessage: input.body,
1010
+ waitingOn: null,
1011
+ blocked: false,
1012
+ session: createAgentSessionRecord({
1013
+ sessionDir: state.session.sessionDir ?? getAgentSessionsDir(input.artifactsDir, input.agentId),
1014
+ sessionId: state.session.sessionId,
1015
+ sessionPath: state.session.sessionPath,
1016
+ lastAttachedAt: state.session.lastAttachedAt
1017
+ })
1018
+ }));
1019
+ }
1020
+ function updateAgentStatus(input) {
1021
+ const state = readAgentState(input.artifactsDir, input.agentId);
1022
+ if (state === null) throw new Error(`Unknown agent: ${input.agentId}`);
1023
+ 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;
1025
+ if (taskStatus) updateTaskRecordStatus(input.artifactsDir, state.taskId, taskStatus);
1026
+ }
1027
+ writeAgentState(input.artifactsDir, createAgentState({
1028
+ ...state,
1029
+ status: input.status,
1030
+ lastMessage: input.lastMessage ?? state.lastMessage,
1031
+ waitingOn: input.waitingOn ?? state.waitingOn,
1032
+ blocked: input.blocked ?? state.blocked,
1033
+ session: state.session
1034
+ }));
1035
+ }
1036
+ function spawnAgentRun(options) {
1037
+ const sessionDir = getAgentSessionsDir(options.artifactsDir, options.agentId);
1038
+ if (readAgentState(options.artifactsDir, options.agentId) !== null) throw new Error(`Agent already exists: ${options.agentId}`);
1039
+ assertCommandAvailable("pi");
1040
+ const state = createAgentState({
1041
+ agentId: options.agentId,
1042
+ role: options.role,
1043
+ status: "queued",
1044
+ taskId: options.taskId ?? null,
1045
+ task: options.task,
1046
+ lastMessage: options.task ? `Spawned with task: ${options.task}` : `Spawned ${options.role} agent`,
1047
+ session: createAgentSessionRecord({ sessionDir })
1048
+ });
1049
+ writeAgentState(options.artifactsDir, state);
1050
+ if (options.task) appendAgentMessage({
1051
+ artifactsDir: options.artifactsDir,
1052
+ agentId: options.agentId,
1053
+ box: "inbox",
1054
+ from: "system",
1055
+ body: options.task
1056
+ });
1057
+ writeAgentState(options.artifactsDir, createAgentState({
1058
+ ...state,
1059
+ status: "running",
1060
+ lastMessage: options.task ? `Running ${options.role} task: ${options.task}` : `Running ${options.role} agent`
1061
+ }));
1062
+ const piArgs = createPiInvocationArgs({
1063
+ sessionDir,
1064
+ prompt: createRolePrompt({
1065
+ role: options.role,
1066
+ task: options.task,
1067
+ repoRoot: options.repoRoot
1068
+ }),
1069
+ appendedSystemPrompt: options.appendedSystemPrompt,
1070
+ extensionPath: options.extensionPath
1071
+ });
1072
+ const piResult = runCommandSync("pi", piArgs, {
1073
+ cwd: options.repoRoot,
1074
+ env: process.env
1075
+ });
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({
1081
+ command: "pi",
1082
+ args: piArgs,
1083
+ exitCode: piResult.exitCode,
1084
+ sessionDir,
1085
+ sessionPath: latestSession.sessionPath,
1086
+ sessionId: latestSession.sessionId
1087
+ }, 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
+ return {
1109
+ piResult,
1110
+ latestSession,
1111
+ completionMessage
1112
+ };
1113
+ }
1114
+ function runAgentTurn(options) {
1115
+ assertCommandAvailable("pi");
1116
+ const resolved = resolveAgentSession(options.agentId, options.artifactsDir);
1117
+ const messageSource = options.from ?? "human";
1118
+ writeAgentState(options.artifactsDir, createAgentState({
1119
+ ...resolved.state,
1120
+ status: "running",
1121
+ lastMessage: `Responding to ${messageSource}: ${options.message}`,
1122
+ waitingOn: null,
1123
+ blocked: false,
1124
+ session: resolved.session
1125
+ }));
1126
+ const piArgs = options.runtimeArgs && options.runtimeArgs.length > 0 ? options.runtimeArgs : createPiInvocationArgs({
1127
+ sessionPath: resolved.session.sessionPath,
1128
+ prompt: options.message
1129
+ });
1130
+ const piResult = runCommandSync("pi", piArgs, {
1131
+ cwd: options.repoRoot,
1132
+ env: process.env
1133
+ });
1134
+ const latestSession = getLatestAgentSession(options.artifactsDir, options.agentId);
1135
+ const agentArtifactsDir = getAgentDir(options.artifactsDir, options.agentId);
1136
+ writeFileSync(`${agentArtifactsDir}/latest-stdout.txt`, piResult.stdout, "utf-8");
1137
+ writeFileSync(`${agentArtifactsDir}/latest-stderr.txt`, piResult.stderr, "utf-8");
1138
+ writeFileSync(`${agentArtifactsDir}/latest-invocation.json`, `${JSON.stringify({
1139
+ command: "pi",
1140
+ args: piArgs,
1141
+ exitCode: piResult.exitCode,
1142
+ sessionDir: latestSession.sessionDir,
1143
+ sessionPath: latestSession.sessionPath,
1144
+ sessionId: latestSession.sessionId
1145
+ }, null, 2)}\n`, "utf-8");
1146
+ const completionMessage = piResult.stdout.trim() || (piResult.exitCode === 0 ? `${resolved.state.role} turn completed` : `${resolved.state.role} turn exited with code ${piResult.exitCode}`);
1147
+ appendAgentMessage({
1148
+ artifactsDir: options.artifactsDir,
1149
+ agentId: options.agentId,
1150
+ box: "outbox",
1151
+ from: options.agentId,
1152
+ body: completionMessage
1153
+ });
1154
+ writeAgentState(options.artifactsDir, createAgentState({
1155
+ ...resolved.state,
1156
+ status: piResult.exitCode === 0 ? "idle" : "blocked",
1157
+ lastMessage: completionMessage,
1158
+ waitingOn: piResult.exitCode === 0 ? null : "human-or-follow-up-run",
1159
+ blocked: piResult.exitCode !== 0,
1160
+ session: createAgentSessionRecord({
1161
+ sessionDir: latestSession.sessionDir,
1162
+ sessionId: latestSession.sessionId,
1163
+ sessionPath: latestSession.sessionPath
1164
+ })
1165
+ }));
1166
+ return {
1167
+ piResult,
1168
+ latestSession,
1169
+ completionMessage
1170
+ };
1171
+ }
1172
+ function delegateTask(options) {
1173
+ if (readAgentState(options.artifactsDir, options.fromAgentId) === null) throw new Error(`Unknown delegating agent: ${options.fromAgentId}`);
1174
+ const agentId = options.agentId ?? `${options.role}-${Date.now()}`;
1175
+ const task = createTaskRecord({
1176
+ taskId: `task-${Date.now()}`,
1177
+ title: options.task,
1178
+ status: "queued",
1179
+ role: options.role,
1180
+ assignedAgentId: agentId,
1181
+ createdBy: options.fromAgentId
1182
+ });
1183
+ writeTaskRecord(options.artifactsDir, task);
1184
+ appendAgentMessage({
1185
+ artifactsDir: options.artifactsDir,
1186
+ agentId: options.fromAgentId,
1187
+ box: "outbox",
1188
+ from: options.fromAgentId,
1189
+ body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
1190
+ });
1191
+ const { piResult, latestSession } = spawnAgentRun({
1192
+ repoRoot: options.repoRoot,
1193
+ artifactsDir: options.artifactsDir,
1194
+ role: options.role,
1195
+ agentId,
1196
+ appendedSystemPrompt: options.appendedSystemPrompt,
1197
+ extensionPath: options.extensionPath,
1198
+ task: options.task,
1199
+ taskId: task.taskId
1200
+ });
1201
+ appendAgentMessage({
1202
+ artifactsDir: options.artifactsDir,
1203
+ agentId,
1204
+ box: "inbox",
1205
+ from: options.fromAgentId,
1206
+ body: `Delegated by ${options.fromAgentId} as ${task.taskId}: ${options.task}`
1207
+ });
1208
+ writeTaskRecord(options.artifactsDir, {
1209
+ ...task,
1210
+ status: piResult.exitCode === 0 ? "completed" : "blocked",
1211
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1212
+ });
1213
+ return {
1214
+ task: {
1215
+ ...task,
1216
+ status: piResult.exitCode === 0 ? "completed" : "blocked",
1217
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1218
+ },
1219
+ agentId,
1220
+ piResult,
1221
+ latestSession
1222
+ };
1223
+ }
1224
+
1225
+ //#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 };
425
1227
  //# sourceMappingURL=index.mjs.map