@schilderlabs/pitown 0.1.2 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,678 @@
1
+ import { d as listAgentStates, h as appendJsonl, l as getAgentSessionsDir, m as writeAgentState, n as listTaskRecords, o as createAgentSessionRecord, p as readAgentState, s as createAgentState, u as getLatestAgentSession } from "./tasks-De4IAy3x.mjs";
2
+ import { n as detectPiAuthFailure, t as createPiAuthHelpMessage } from "./pi-C7HRNjBG.mjs";
3
+ import { a as runCommandSync, n as assertCommandAvailable } from "./entrypoint-WBAQmFbT.mjs";
4
+ import { d as createRepoSlug, f as getCurrentBranch, m as getRepoRoot, p as getRepoIdentity } from "./config-BG1v4iIi.mjs";
5
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { homedir, hostname } from "node:os";
8
+
9
+ //#region ../core/src/lease.ts
10
+ function sanitize(value) {
11
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
12
+ }
13
+ function processAlive(pid) {
14
+ if (!Number.isFinite(pid) || pid <= 0) return false;
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+ function acquireRepoLease(runId, repoId, branch) {
23
+ const locksDir = join(homedir(), ".pi-town", "locks");
24
+ mkdirSync(locksDir, { recursive: true });
25
+ const leasePath = join(locksDir, `pi-town-${sanitize(repoId)}-${sanitize(branch)}.json`);
26
+ const nextData = {
27
+ runId,
28
+ repoId,
29
+ branch,
30
+ pid: process.pid,
31
+ hostname: hostname(),
32
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
33
+ };
34
+ try {
35
+ const current = JSON.parse(readFileSync(leasePath, "utf-8"));
36
+ if (processAlive(current.pid)) throw new Error(`Pi Town lease already held by pid ${current.pid} on ${current.hostname} for run ${current.runId}.`);
37
+ rmSync(leasePath, { force: true });
38
+ } catch (error) {
39
+ if (error.code !== "ENOENT") {
40
+ if (error instanceof Error && error.message.startsWith("Pi Town lease already held")) throw error;
41
+ }
42
+ }
43
+ writeFileSync(leasePath, `${JSON.stringify(nextData, null, 2)}\n`, "utf-8");
44
+ return {
45
+ path: leasePath,
46
+ release: () => {
47
+ try {
48
+ if (JSON.parse(readFileSync(leasePath, "utf-8")).runId === runId) rmSync(leasePath, { force: true });
49
+ } catch {}
50
+ }
51
+ };
52
+ }
53
+
54
+ //#endregion
55
+ //#region ../core/src/metrics.ts
56
+ function round(value) {
57
+ return Math.round(value * 1e3) / 1e3;
58
+ }
59
+ function diffHours(start, end) {
60
+ return (Date.parse(end) - Date.parse(start)) / 36e5;
61
+ }
62
+ function average(values) {
63
+ if (values.length === 0) return null;
64
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
65
+ }
66
+ function computeInterruptRate(interrupts, taskAttempts) {
67
+ if (taskAttempts.length === 0) return 0;
68
+ return round(interrupts.length / taskAttempts.length);
69
+ }
70
+ function computeAutonomousCompletionRate(taskAttempts) {
71
+ const completed = taskAttempts.filter((task) => task.status === "completed");
72
+ if (completed.length === 0) return 0;
73
+ return round(completed.filter((task) => !task.interrupted).length / completed.length);
74
+ }
75
+ function computeContextCoverageScore(interrupts) {
76
+ const observed = new Set(interrupts.map((interrupt) => interrupt.category));
77
+ if (observed.size === 0) return 0;
78
+ return round(new Set(interrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category)).size / observed.size);
79
+ }
80
+ function computeMeanTimeToCorrect(interrupts) {
81
+ const value = average(interrupts.filter((interrupt) => interrupt.resolvedAt).map((interrupt) => diffHours(interrupt.createdAt, interrupt.resolvedAt)));
82
+ return value === null ? null : round(value);
83
+ }
84
+ function computeFeedbackToDemoCycleTime(feedbackCycles) {
85
+ const value = average(feedbackCycles.map((cycle) => diffHours(cycle.feedbackAt, cycle.demoReadyAt)));
86
+ return value === null ? null : round(value);
87
+ }
88
+ function computeMetrics(input) {
89
+ const observedCategories = new Set(input.interrupts.map((interrupt) => interrupt.category));
90
+ const coveredCategories = new Set(input.interrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category));
91
+ const completedTasks = input.taskAttempts.filter((task) => task.status === "completed").length;
92
+ return {
93
+ interruptRate: computeInterruptRate(input.interrupts, input.taskAttempts),
94
+ autonomousCompletionRate: computeAutonomousCompletionRate(input.taskAttempts),
95
+ contextCoverageScore: computeContextCoverageScore(input.interrupts),
96
+ meanTimeToCorrectHours: computeMeanTimeToCorrect(input.interrupts),
97
+ feedbackToDemoCycleTimeHours: computeFeedbackToDemoCycleTime(input.feedbackCycles ?? []),
98
+ totals: {
99
+ taskAttempts: input.taskAttempts.length,
100
+ completedTasks,
101
+ interrupts: input.interrupts.length,
102
+ observedInterruptCategories: observedCategories.size,
103
+ coveredInterruptCategories: coveredCategories.size
104
+ }
105
+ };
106
+ }
107
+
108
+ //#endregion
109
+ //#region ../core/src/controller.ts
110
+ function createRunId() {
111
+ return `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
112
+ }
113
+ function createPiInvocationArgs(input) {
114
+ const args = [];
115
+ if (input.extensionPath) args.push("--extension", input.extensionPath);
116
+ if (input.appendedSystemPrompt) args.push("--append-system-prompt", input.appendedSystemPrompt);
117
+ if (input.sessionPath) args.push("--session", input.sessionPath);
118
+ else if (input.sessionDir) args.push("--session-dir", input.sessionDir);
119
+ else throw new Error("Pi invocation requires a session path or session directory");
120
+ args.push("-p", input.prompt);
121
+ return args;
122
+ }
123
+ function writeJson$1(path, value) {
124
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
125
+ }
126
+ function writeText(path, value) {
127
+ writeFileSync(path, value, "utf-8");
128
+ }
129
+ function createPiPrompt(input) {
130
+ const goal = input.goal ?? "continue from current scaffold state";
131
+ if (input.planPath) return [
132
+ "You are the Pi Town mayor agent for this repository.",
133
+ "Coordinate the next bounded unit of work, keep updates concise, and leave a durable artifact trail.",
134
+ "",
135
+ "Read the private plans in:",
136
+ `- ${input.planPath}`,
137
+ "",
138
+ "and the current code in:",
139
+ `- ${input.repoRoot}`,
140
+ "",
141
+ `Goal: ${goal}`,
142
+ "Continue from the current scaffold state.",
143
+ "Keep any persisted run artifacts high-signal and avoid copying private plan contents into them."
144
+ ].join("\n");
145
+ return [
146
+ "You are the Pi Town mayor agent for this repository.",
147
+ "Coordinate the next bounded unit of work, keep updates concise, and leave a durable artifact trail.",
148
+ "",
149
+ `Work in the repository at: ${input.repoRoot}`,
150
+ `Goal: ${goal}`,
151
+ "No private plan path is configured for this run.",
152
+ input.recommendedPlanDir ? `If you need private plans, use a user-owned location such as: ${input.recommendedPlanDir}` : "If you need private plans, keep them in a user-owned location outside the repo.",
153
+ "Continue from the current scaffold state."
154
+ ].join("\n");
155
+ }
156
+ function assertPiRuntimeAvailable(piCommand) {
157
+ try {
158
+ assertCommandAvailable(piCommand);
159
+ } catch (error) {
160
+ if (piCommand === "pi") throw new Error([
161
+ "Pi Town requires the `pi` CLI to run `pitown run`.",
162
+ "Install Pi: npm install -g @mariozechner/pi-coding-agent",
163
+ "Then authenticate Pi and verify it works: pi -p \"hello\"",
164
+ `Details: ${error.message}`
165
+ ].join("\n"));
166
+ throw new Error([
167
+ `Pi Town could not execute the configured Pi command: ${piCommand}`,
168
+ "Make sure the command exists on PATH or points to an executable file.",
169
+ `Details: ${error.message}`
170
+ ].join("\n"));
171
+ }
172
+ }
173
+ function createManifest(input) {
174
+ return {
175
+ runId: input.runId,
176
+ repoId: input.repoId,
177
+ repoSlug: input.repoSlug,
178
+ repoRoot: input.repoRoot,
179
+ branch: input.branch,
180
+ goal: input.goal,
181
+ planPath: input.planPath,
182
+ recommendedPlanDir: input.recommendedPlanDir,
183
+ mode: input.mode,
184
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
185
+ endedAt: null,
186
+ stopReason: null,
187
+ leasePath: input.leasePath,
188
+ piExitCode: null,
189
+ completedTaskCount: 0,
190
+ blockedTaskCount: 0,
191
+ skippedTaskCount: 0,
192
+ totalCostUsd: 0
193
+ };
194
+ }
195
+ function createSummary(input) {
196
+ const success = input.exitCode === 0;
197
+ const recommendation = input.recommendedPlanDir === null ? "" : ` No plan path was configured. Recommended private plans location: ${input.recommendedPlanDir}.`;
198
+ const authHelp = success || !detectPiAuthFailure(input.stderr, input.stdout) ? "" : ` ${createPiAuthHelpMessage()}`;
199
+ return {
200
+ runId: input.runId,
201
+ mode: input.mode,
202
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
203
+ success,
204
+ message: success ? `Pi invocation completed.${recommendation}` : `Pi invocation failed.${authHelp}${recommendation}`,
205
+ piExitCode: input.exitCode,
206
+ recommendedPlanDir: input.recommendedPlanDir
207
+ };
208
+ }
209
+ function runController(options) {
210
+ const cwd = options.cwd ?? process.cwd();
211
+ const artifactsDir = options.artifactsDir;
212
+ const repoRoot = getRepoRoot(cwd);
213
+ const repoId = getRepoIdentity(repoRoot);
214
+ const repoSlug = createRepoSlug(repoId, repoRoot);
215
+ const branch = options.branch ?? getCurrentBranch(repoRoot) ?? "workspace";
216
+ const goal = options.goal ?? null;
217
+ const planPath = options.planPath ?? null;
218
+ const recommendedPlanDir = planPath ? null : options.recommendedPlanDir ?? null;
219
+ const mode = options.mode ?? "single-pi";
220
+ const piCommand = options.piCommand ?? "pi";
221
+ const runId = createRunId();
222
+ const runDir = join(artifactsDir, "runs", runId);
223
+ const latestDir = join(artifactsDir, "latest");
224
+ const stdoutPath = join(runDir, "stdout.txt");
225
+ const stderrPath = join(runDir, "stderr.txt");
226
+ const prompt = createPiPrompt({
227
+ repoRoot,
228
+ planPath,
229
+ goal,
230
+ recommendedPlanDir
231
+ });
232
+ const existingMayorState = readAgentState(artifactsDir, "mayor");
233
+ const existingMayorSession = existingMayorState?.session.sessionPath || existingMayorState?.session.sessionDir ? existingMayorState.session : getLatestAgentSession(artifactsDir, "mayor");
234
+ const mayorSessionDir = existingMayorSession.sessionDir ?? getAgentSessionsDir(artifactsDir, "mayor");
235
+ const mayorState = createAgentState({
236
+ agentId: "mayor",
237
+ role: "mayor",
238
+ status: "starting",
239
+ task: goal,
240
+ branch,
241
+ lastMessage: goal ? `Starting mayor run for goal: ${goal}` : "Starting mayor run",
242
+ runId,
243
+ session: createAgentSessionRecord({
244
+ sessionDir: mayorSessionDir,
245
+ sessionId: existingMayorSession.sessionId,
246
+ sessionPath: existingMayorSession.sessionPath
247
+ })
248
+ });
249
+ assertPiRuntimeAvailable(piCommand);
250
+ mkdirSync(runDir, { recursive: true });
251
+ mkdirSync(latestDir, { recursive: true });
252
+ writeText(join(runDir, "questions.jsonl"), "");
253
+ writeText(join(runDir, "interventions.jsonl"), "");
254
+ writeJson$1(join(runDir, "agent-state.json"), {
255
+ status: "starting",
256
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
257
+ });
258
+ writeAgentState(artifactsDir, mayorState);
259
+ const lease = acquireRepoLease(runId, repoId, branch);
260
+ try {
261
+ const manifest = createManifest({
262
+ runId,
263
+ repoId,
264
+ repoSlug,
265
+ repoRoot,
266
+ branch,
267
+ goal,
268
+ planPath,
269
+ recommendedPlanDir,
270
+ mode,
271
+ leasePath: lease.path
272
+ });
273
+ appendJsonl(join(runDir, "events.jsonl"), {
274
+ type: "run_started",
275
+ runId,
276
+ repoId,
277
+ repoSlug,
278
+ branch,
279
+ createdAt: manifest.startedAt
280
+ });
281
+ const piStartedAt = (/* @__PURE__ */ new Date()).toISOString();
282
+ appendJsonl(join(runDir, "events.jsonl"), {
283
+ type: "pi_invocation_started",
284
+ runId,
285
+ command: piCommand,
286
+ createdAt: piStartedAt
287
+ });
288
+ writeAgentState(artifactsDir, createAgentState({
289
+ ...mayorState,
290
+ status: "running",
291
+ lastMessage: goal ? `Mayor working on: ${goal}` : "Mayor working"
292
+ }));
293
+ const piResult = runCommandSync(piCommand, createPiInvocationArgs({
294
+ sessionDir: mayorState.session.sessionPath === null ? mayorSessionDir : null,
295
+ sessionPath: mayorState.session.sessionPath,
296
+ prompt,
297
+ appendedSystemPrompt: options.appendedSystemPrompt,
298
+ extensionPath: options.extensionPath
299
+ }), {
300
+ cwd: repoRoot,
301
+ env: process.env
302
+ });
303
+ const piEndedAt = (/* @__PURE__ */ new Date()).toISOString();
304
+ const latestMayorSession = getLatestAgentSession(artifactsDir, "mayor");
305
+ writeText(stdoutPath, piResult.stdout);
306
+ writeText(stderrPath, piResult.stderr);
307
+ const piInvocation = {
308
+ command: piCommand,
309
+ cwd: repoRoot,
310
+ repoRoot,
311
+ planPath,
312
+ goal,
313
+ sessionDir: latestMayorSession.sessionDir,
314
+ sessionId: latestMayorSession.sessionId,
315
+ sessionPath: latestMayorSession.sessionPath,
316
+ startedAt: piStartedAt,
317
+ endedAt: piEndedAt,
318
+ exitCode: piResult.exitCode,
319
+ stdoutPath,
320
+ stderrPath,
321
+ promptSummary: planPath ? "Read private plan path and continue from current scaffold state." : "Continue from current scaffold state without a configured private plan path."
322
+ };
323
+ writeJson$1(join(runDir, "pi-invocation.json"), piInvocation);
324
+ appendJsonl(join(runDir, "events.jsonl"), {
325
+ type: "pi_invocation_finished",
326
+ runId,
327
+ command: piCommand,
328
+ exitCode: piInvocation.exitCode,
329
+ createdAt: piEndedAt
330
+ });
331
+ const metrics = computeMetrics({
332
+ taskAttempts: [],
333
+ interrupts: []
334
+ });
335
+ const summary = createSummary({
336
+ runId,
337
+ mode,
338
+ exitCode: piInvocation.exitCode,
339
+ stdout: piResult.stdout,
340
+ stderr: piResult.stderr,
341
+ recommendedPlanDir
342
+ });
343
+ const finalManifest = {
344
+ ...manifest,
345
+ endedAt: piEndedAt,
346
+ stopReason: piInvocation.exitCode === 0 ? "pi invocation completed" : `pi invocation exited with code ${piInvocation.exitCode}`,
347
+ piExitCode: piInvocation.exitCode
348
+ };
349
+ writeJson$1(join(runDir, "manifest.json"), finalManifest);
350
+ writeJson$1(join(runDir, "metrics.json"), metrics);
351
+ writeJson$1(join(runDir, "run-summary.json"), summary);
352
+ writeJson$1(join(runDir, "agent-state.json"), {
353
+ status: summary.success ? "completed" : "failed",
354
+ updatedAt: piEndedAt,
355
+ exitCode: piInvocation.exitCode
356
+ });
357
+ writeJson$1(join(latestDir, "manifest.json"), finalManifest);
358
+ writeJson$1(join(latestDir, "metrics.json"), metrics);
359
+ writeJson$1(join(latestDir, "run-summary.json"), summary);
360
+ writeAgentState(artifactsDir, createAgentState({
361
+ ...mayorState,
362
+ status: piInvocation.exitCode === 0 ? "idle" : "blocked",
363
+ lastMessage: piInvocation.exitCode === 0 ? "Mayor run completed and is ready for the next instruction" : `Mayor run stopped with exit code ${piInvocation.exitCode}`,
364
+ blocked: piInvocation.exitCode !== 0,
365
+ waitingOn: piInvocation.exitCode === 0 ? null : "human-or-follow-up-run",
366
+ session: createAgentSessionRecord({
367
+ sessionDir: latestMayorSession.sessionDir,
368
+ sessionId: latestMayorSession.sessionId,
369
+ sessionPath: latestMayorSession.sessionPath
370
+ })
371
+ }));
372
+ appendJsonl(join(runDir, "events.jsonl"), {
373
+ type: "run_finished",
374
+ runId,
375
+ createdAt: finalManifest.endedAt,
376
+ stopReason: finalManifest.stopReason,
377
+ metrics
378
+ });
379
+ return {
380
+ runId,
381
+ runDir,
382
+ latestDir,
383
+ manifest: finalManifest,
384
+ metrics,
385
+ summary,
386
+ piInvocation
387
+ };
388
+ } finally {
389
+ lease.release();
390
+ }
391
+ }
392
+
393
+ //#endregion
394
+ //#region ../core/src/loop.ts
395
+ const DEFAULT_MAX_ITERATIONS = 10;
396
+ const DEFAULT_MAX_WALL_TIME_MS = 36e5;
397
+ const DEFAULT_BACKGROUND_POLL_MS = 250;
398
+ function createLoopId() {
399
+ return `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
400
+ }
401
+ function writeJson(path, value) {
402
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
403
+ }
404
+ function sleepMs(ms) {
405
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
406
+ }
407
+ function hasBackgroundWork(board) {
408
+ 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");
409
+ }
410
+ function waitForBackgroundWorkToSettle(input) {
411
+ const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_BACKGROUND_POLL_MS;
412
+ let board = snapshotBoard(input.artifactsDir);
413
+ while (hasBackgroundWork(board)) {
414
+ if (Date.now() - input.loopStartedAt >= input.maxWallTimeMs) return {
415
+ timedOut: true,
416
+ board
417
+ };
418
+ sleepMs(pollIntervalMs);
419
+ board = snapshotBoard(input.artifactsDir);
420
+ }
421
+ return {
422
+ timedOut: false,
423
+ board
424
+ };
425
+ }
426
+ function snapshotBoard(artifactsDir) {
427
+ const tasks = listTaskRecords(artifactsDir);
428
+ const agents = listAgentStates(artifactsDir);
429
+ return {
430
+ tasks: tasks.map((task) => ({
431
+ taskId: task.taskId,
432
+ status: task.status
433
+ })),
434
+ agents: agents.map((agent) => ({
435
+ agentId: agent.agentId,
436
+ status: agent.status,
437
+ blocked: agent.blocked
438
+ })),
439
+ allTasksCompleted: tasks.length > 0 && tasks.every((task) => task.status === "completed"),
440
+ allRemainingTasksBlocked: tasks.length > 0 && tasks.every((task) => task.status === "completed" || task.status === "blocked" || task.status === "aborted"),
441
+ mayorBlocked: agents.find((agent) => agent.agentId === "mayor")?.blocked === true,
442
+ hasQueuedOrRunningWork: agents.some((agent) => agent.status === "queued" || agent.status === "running" || agent.status === "starting") || tasks.some((task) => task.status === "queued" || task.status === "running")
443
+ };
444
+ }
445
+ function evaluateStopCondition(input) {
446
+ if (input.iteration >= input.maxIterations) return {
447
+ stopReason: "max-iterations-reached",
448
+ continueReason: null
449
+ };
450
+ if (input.elapsedMs >= input.maxWallTimeMs) return {
451
+ stopReason: "max-wall-time-reached",
452
+ continueReason: null
453
+ };
454
+ if (input.stopOnPiFailure && input.piExitCode !== 0) return {
455
+ stopReason: "pi-exit-nonzero",
456
+ continueReason: null
457
+ };
458
+ if (input.board.allTasksCompleted) return {
459
+ stopReason: "all-tasks-completed",
460
+ continueReason: null
461
+ };
462
+ if (input.board.mayorBlocked) return {
463
+ stopReason: "mayor-blocked",
464
+ continueReason: null
465
+ };
466
+ const mayor = input.board.agents.find((agent) => agent.agentId === "mayor");
467
+ if (input.stopOnMayorIdleNoWork && mayor && !input.board.hasQueuedOrRunningWork && input.board.tasks.length === 0 && mayor.status === "idle") return {
468
+ stopReason: "mayor-idle-no-work",
469
+ continueReason: null
470
+ };
471
+ if (input.board.allRemainingTasksBlocked) return {
472
+ stopReason: "all-remaining-tasks-blocked",
473
+ continueReason: null
474
+ };
475
+ if (input.interruptRateThreshold !== null && input.metrics.interruptRate > input.interruptRateThreshold) return {
476
+ stopReason: "high-interrupt-rate",
477
+ continueReason: null
478
+ };
479
+ const reasons = [];
480
+ if (input.board.hasQueuedOrRunningWork) reasons.push("queued or running work remains");
481
+ if (input.board.tasks.length === 0) reasons.push("no tasks tracked yet");
482
+ if (reasons.length === 0) reasons.push("mayor idle, no stop condition met");
483
+ return {
484
+ stopReason: null,
485
+ continueReason: reasons.join("; ")
486
+ };
487
+ }
488
+ function aggregateMetrics(iterations) {
489
+ if (iterations.length === 0) return computeMetrics({
490
+ taskAttempts: [],
491
+ interrupts: []
492
+ });
493
+ let totalTaskAttempts = 0;
494
+ let totalCompletedTasks = 0;
495
+ let totalInterrupts = 0;
496
+ let totalObservedInterruptCategories = 0;
497
+ let totalCoveredInterruptCategories = 0;
498
+ let interruptRateSum = 0;
499
+ let autonomousCompletionRateSum = 0;
500
+ let contextCoverageScoreSum = 0;
501
+ let mttcValues = [];
502
+ let ftdValues = [];
503
+ for (const iter of iterations) {
504
+ const m = iter.metrics;
505
+ totalTaskAttempts += m.totals.taskAttempts;
506
+ totalCompletedTasks += m.totals.completedTasks;
507
+ totalInterrupts += m.totals.interrupts;
508
+ totalObservedInterruptCategories += m.totals.observedInterruptCategories;
509
+ totalCoveredInterruptCategories += m.totals.coveredInterruptCategories;
510
+ interruptRateSum += m.interruptRate;
511
+ autonomousCompletionRateSum += m.autonomousCompletionRate;
512
+ contextCoverageScoreSum += m.contextCoverageScore;
513
+ if (m.meanTimeToCorrectHours !== null) mttcValues.push(m.meanTimeToCorrectHours);
514
+ if (m.feedbackToDemoCycleTimeHours !== null) ftdValues.push(m.feedbackToDemoCycleTimeHours);
515
+ }
516
+ const count = iterations.length;
517
+ const round = (v) => Math.round(v * 1e3) / 1e3;
518
+ const avg = (values) => values.length === 0 ? null : round(values.reduce((s, v) => s + v, 0) / values.length);
519
+ return {
520
+ interruptRate: round(interruptRateSum / count),
521
+ autonomousCompletionRate: round(autonomousCompletionRateSum / count),
522
+ contextCoverageScore: round(contextCoverageScoreSum / count),
523
+ meanTimeToCorrectHours: avg(mttcValues),
524
+ feedbackToDemoCycleTimeHours: avg(ftdValues),
525
+ totals: {
526
+ taskAttempts: totalTaskAttempts,
527
+ completedTasks: totalCompletedTasks,
528
+ interrupts: totalInterrupts,
529
+ observedInterruptCategories: totalObservedInterruptCategories,
530
+ coveredInterruptCategories: totalCoveredInterruptCategories
531
+ }
532
+ };
533
+ }
534
+ function runLoop(options) {
535
+ const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
536
+ const maxWallTimeMs = options.maxWallTimeMs ?? DEFAULT_MAX_WALL_TIME_MS;
537
+ const stopOnPiFailure = options.stopOnPiFailure ?? true;
538
+ const stopOnMayorIdleNoWork = options.stopOnMayorIdleNoWork ?? false;
539
+ const interruptRateThreshold = options.interruptRateThreshold ?? null;
540
+ const loopId = createLoopId();
541
+ const artifactsDir = options.runOptions.artifactsDir;
542
+ const loopDir = join(artifactsDir, "loops", loopId);
543
+ mkdirSync(loopDir, { recursive: true });
544
+ const loopStartedAt = Date.now();
545
+ const iterations = [];
546
+ let finalStopReason = "max-iterations-reached";
547
+ let needsMayorFollowUp = false;
548
+ appendJsonl(join(loopDir, "events.jsonl"), {
549
+ type: "loop_started",
550
+ loopId,
551
+ maxIterations,
552
+ maxWallTimeMs,
553
+ stopOnPiFailure,
554
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
555
+ });
556
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
557
+ if (needsMayorFollowUp) {
558
+ const settled = waitForBackgroundWorkToSettle({
559
+ artifactsDir,
560
+ maxWallTimeMs,
561
+ loopStartedAt
562
+ });
563
+ appendJsonl(join(loopDir, "events.jsonl"), {
564
+ type: "loop_background_work_settled",
565
+ loopId,
566
+ iteration,
567
+ timedOut: settled.timedOut,
568
+ boardSnapshot: settled.board,
569
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
570
+ });
571
+ if (settled.timedOut) {
572
+ finalStopReason = "max-wall-time-reached";
573
+ break;
574
+ }
575
+ needsMayorFollowUp = false;
576
+ }
577
+ const iterationStart = Date.now();
578
+ let controllerResult;
579
+ try {
580
+ controllerResult = runController(options.runOptions);
581
+ } catch (error) {
582
+ appendJsonl(join(loopDir, "events.jsonl"), {
583
+ type: "loop_iteration_error",
584
+ loopId,
585
+ iteration,
586
+ error: error.message,
587
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
588
+ });
589
+ finalStopReason = "pi-exit-nonzero";
590
+ break;
591
+ }
592
+ const iterationElapsedMs = Date.now() - iterationStart;
593
+ const totalElapsedMs = Date.now() - loopStartedAt;
594
+ const board = snapshotBoard(artifactsDir);
595
+ const metrics = controllerResult.metrics;
596
+ const { stopReason, continueReason } = evaluateStopCondition({
597
+ iteration,
598
+ maxIterations,
599
+ elapsedMs: totalElapsedMs,
600
+ maxWallTimeMs,
601
+ piExitCode: controllerResult.piInvocation.exitCode,
602
+ stopOnPiFailure,
603
+ stopOnMayorIdleNoWork,
604
+ board,
605
+ metrics,
606
+ interruptRateThreshold
607
+ });
608
+ const iterationResult = {
609
+ iteration,
610
+ controllerResult,
611
+ boardSnapshot: board,
612
+ metrics,
613
+ elapsedMs: iterationElapsedMs,
614
+ continueReason,
615
+ stopReason
616
+ };
617
+ iterations.push(iterationResult);
618
+ writeJson(join(loopDir, `iteration-${iteration}.json`), {
619
+ iteration,
620
+ runId: controllerResult.runId,
621
+ boardSnapshot: board,
622
+ metrics,
623
+ elapsedMs: iterationElapsedMs,
624
+ continueReason,
625
+ stopReason
626
+ });
627
+ appendJsonl(join(loopDir, "events.jsonl"), {
628
+ type: "loop_iteration_completed",
629
+ loopId,
630
+ iteration,
631
+ runId: controllerResult.runId,
632
+ piExitCode: controllerResult.piInvocation.exitCode,
633
+ stopReason,
634
+ continueReason,
635
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
636
+ });
637
+ if (options.onIterationComplete) options.onIterationComplete(iterationResult);
638
+ if (stopReason !== null) {
639
+ finalStopReason = stopReason;
640
+ break;
641
+ }
642
+ needsMayorFollowUp = hasBackgroundWork(board);
643
+ }
644
+ const totalElapsedMs = Date.now() - loopStartedAt;
645
+ const lastIteration = iterations.at(-1);
646
+ const finalBoard = lastIteration ? lastIteration.boardSnapshot : snapshotBoard(artifactsDir);
647
+ const aggregate = aggregateMetrics(iterations);
648
+ const loopResult = {
649
+ loopId,
650
+ iterations,
651
+ stopReason: finalStopReason,
652
+ totalIterations: iterations.length,
653
+ totalElapsedMs,
654
+ finalBoardSnapshot: finalBoard,
655
+ aggregateMetrics: aggregate
656
+ };
657
+ writeJson(join(loopDir, "loop-summary.json"), {
658
+ loopId,
659
+ stopReason: finalStopReason,
660
+ totalIterations: iterations.length,
661
+ totalElapsedMs,
662
+ finalBoardSnapshot: finalBoard,
663
+ aggregateMetrics: aggregate
664
+ });
665
+ appendJsonl(join(loopDir, "events.jsonl"), {
666
+ type: "loop_finished",
667
+ loopId,
668
+ stopReason: finalStopReason,
669
+ totalIterations: iterations.length,
670
+ totalElapsedMs,
671
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
672
+ });
673
+ return loopResult;
674
+ }
675
+
676
+ //#endregion
677
+ export { runLoop as t };
678
+ //# sourceMappingURL=loop-CocC9qO1.mjs.map