@schilderlabs/pitown-core 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Schilder Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @schilderlabs/pitown-core
2
+
3
+ Core runtime primitives for Pi Town.
4
+
5
+ This package contains the local-first controller, run artifacts model, metrics helpers, repo identity helpers, and other building blocks used by the `pitown` CLI.
@@ -0,0 +1,162 @@
1
+ //#region src/types.d.ts
2
+ type InterruptCategory = "missing-context" | "spec-gap" | "policy-gap" | "validation-gap" | "tooling-failure" | "external-blocker";
3
+ type FixType = "adr" | "docs" | "policy" | "prompt" | "skill" | "tooling" | "validation";
4
+ type RunMode = "single-pi";
5
+ interface InterruptRecord {
6
+ id: string;
7
+ runId: string;
8
+ taskId?: string;
9
+ category: InterruptCategory;
10
+ subtype?: string;
11
+ summary: string;
12
+ requiresHuman: boolean;
13
+ createdAt: string;
14
+ resolvedAt?: string;
15
+ fixType?: FixType;
16
+ }
17
+ interface TaskAttempt {
18
+ taskId: string;
19
+ status: "completed" | "blocked" | "skipped";
20
+ interrupted: boolean;
21
+ startedAt: string;
22
+ endedAt: string;
23
+ }
24
+ interface FeedbackCycle {
25
+ feedbackAt: string;
26
+ demoReadyAt: string;
27
+ }
28
+ interface MetricsSnapshot {
29
+ interruptRate: number;
30
+ autonomousCompletionRate: number;
31
+ contextCoverageScore: number;
32
+ meanTimeToCorrectHours: number | null;
33
+ feedbackToDemoCycleTimeHours: number | null;
34
+ totals: {
35
+ taskAttempts: number;
36
+ completedTasks: number;
37
+ interrupts: number;
38
+ observedInterruptCategories: number;
39
+ coveredInterruptCategories: number;
40
+ };
41
+ }
42
+ interface RunOptions {
43
+ cwd?: string;
44
+ artifactsDir: string;
45
+ branch?: string | null;
46
+ goal?: string | null;
47
+ mode?: RunMode;
48
+ planPath?: string | null;
49
+ recommendedPlanDir?: string | null;
50
+ piCommand?: string;
51
+ }
52
+ interface RunManifest {
53
+ runId: string;
54
+ repoId: string;
55
+ repoSlug: string;
56
+ repoRoot: string;
57
+ branch: string;
58
+ goal: string | null;
59
+ planPath: string | null;
60
+ recommendedPlanDir: string | null;
61
+ mode: RunMode;
62
+ startedAt: string;
63
+ endedAt: string | null;
64
+ stopReason: string | null;
65
+ leasePath: string;
66
+ piExitCode: number | null;
67
+ completedTaskCount: number;
68
+ blockedTaskCount: number;
69
+ skippedTaskCount: number;
70
+ totalCostUsd: number;
71
+ }
72
+ interface PiInvocationRecord {
73
+ command: string;
74
+ cwd: string;
75
+ repoRoot: string;
76
+ planPath: string | null;
77
+ goal: string | null;
78
+ startedAt: string;
79
+ endedAt: string;
80
+ exitCode: number;
81
+ stdoutPath: string;
82
+ stderrPath: string;
83
+ promptSummary: string;
84
+ }
85
+ interface RunSummary {
86
+ runId: string;
87
+ mode: RunMode;
88
+ createdAt: string;
89
+ success: boolean;
90
+ message: string;
91
+ piExitCode: number;
92
+ recommendedPlanDir: string | null;
93
+ }
94
+ interface ControllerRunResult {
95
+ runId: string;
96
+ runDir: string;
97
+ latestDir: string;
98
+ manifest: RunManifest;
99
+ metrics: MetricsSnapshot;
100
+ summary: RunSummary;
101
+ piInvocation: PiInvocationRecord;
102
+ }
103
+ //#endregion
104
+ //#region src/controller.d.ts
105
+ declare function runController(options: RunOptions): ControllerRunResult;
106
+ //#endregion
107
+ //#region src/events.d.ts
108
+ declare function appendJsonl(filePath: string, value: unknown): void;
109
+ declare function readJsonl<T>(filePath: string): T[];
110
+ //#endregion
111
+ //#region src/interrupts.d.ts
112
+ interface CreateInterruptInput {
113
+ runId: string;
114
+ taskId?: string;
115
+ category: InterruptCategory;
116
+ subtype?: string;
117
+ summary: string;
118
+ requiresHuman?: boolean;
119
+ createdAt?: string;
120
+ }
121
+ declare function createInterruptRecord(input: CreateInterruptInput): InterruptRecord;
122
+ declare function resolveInterrupt(interrupt: InterruptRecord, options: Pick<InterruptRecord, "resolvedAt" | "fixType">): InterruptRecord;
123
+ //#endregion
124
+ //#region src/lease.d.ts
125
+ declare function acquireRepoLease(runId: string, repoId: string, branch: string): {
126
+ path: string;
127
+ release: () => void;
128
+ };
129
+ //#endregion
130
+ //#region src/metrics.d.ts
131
+ declare function computeInterruptRate(interrupts: InterruptRecord[], taskAttempts: TaskAttempt[]): number;
132
+ declare function computeAutonomousCompletionRate(taskAttempts: TaskAttempt[]): number;
133
+ declare function computeContextCoverageScore(interrupts: InterruptRecord[]): number;
134
+ declare function computeMeanTimeToCorrect(interrupts: InterruptRecord[]): number | null;
135
+ declare function computeFeedbackToDemoCycleTime(feedbackCycles: FeedbackCycle[]): number | null;
136
+ declare function computeMetrics(input: {
137
+ taskAttempts: TaskAttempt[];
138
+ interrupts: InterruptRecord[];
139
+ feedbackCycles?: FeedbackCycle[];
140
+ }): MetricsSnapshot;
141
+ //#endregion
142
+ //#region src/repo.d.ts
143
+ declare function isGitRepo(cwd: string): boolean;
144
+ declare function getRepoRoot(cwd: string): string;
145
+ declare function getCurrentBranch(cwd: string): string | null;
146
+ declare function getRepoIdentity(cwd: string): string;
147
+ declare function createRepoSlug(repoId: string, repoRoot: string): string;
148
+ //#endregion
149
+ //#region src/shell.d.ts
150
+ interface CommandResult {
151
+ stdout: string;
152
+ stderr: string;
153
+ exitCode: number;
154
+ }
155
+ declare function runCommandSync(command: string, args: string[], options?: {
156
+ cwd?: string;
157
+ env?: NodeJS.ProcessEnv;
158
+ }): CommandResult;
159
+ declare function assertSuccess(result: CommandResult, context: string): void;
160
+ //#endregion
161
+ export { CommandResult, ControllerRunResult, CreateInterruptInput, FeedbackCycle, FixType, InterruptCategory, InterruptRecord, MetricsSnapshot, PiInvocationRecord, RunManifest, RunMode, RunOptions, RunSummary, TaskAttempt, acquireRepoLease, appendJsonl, assertSuccess, computeAutonomousCompletionRate, computeContextCoverageScore, computeFeedbackToDemoCycleTime, computeInterruptRate, computeMeanTimeToCorrect, computeMetrics, createInterruptRecord, createRepoSlug, getCurrentBranch, getRepoIdentity, getRepoRoot, isGitRepo, readJsonl, resolveInterrupt, runCommandSync, runController };
162
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,425 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { homedir, hostname } from "node:os";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { spawnSync } from "node:child_process";
6
+
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/lease.ts
25
+ function sanitize$1(value) {
26
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
27
+ }
28
+ function processAlive(pid) {
29
+ if (!Number.isFinite(pid) || pid <= 0) return false;
30
+ try {
31
+ process.kill(pid, 0);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ function acquireRepoLease(runId, repoId, branch) {
38
+ const locksDir = join(homedir(), ".pi-town", "locks");
39
+ mkdirSync(locksDir, { recursive: true });
40
+ const leasePath = join(locksDir, `pi-town-${sanitize$1(repoId)}-${sanitize$1(branch)}.json`);
41
+ const nextData = {
42
+ runId,
43
+ repoId,
44
+ branch,
45
+ pid: process.pid,
46
+ hostname: hostname(),
47
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
48
+ };
49
+ try {
50
+ const current = JSON.parse(readFileSync(leasePath, "utf-8"));
51
+ if (processAlive(current.pid)) throw new Error(`Pi Town lease already held by pid ${current.pid} on ${current.hostname} for run ${current.runId}.`);
52
+ rmSync(leasePath, { force: true });
53
+ } catch (error) {
54
+ if (error.code !== "ENOENT") {
55
+ if (error instanceof Error && error.message.startsWith("Pi Town lease already held")) throw error;
56
+ }
57
+ }
58
+ writeFileSync(leasePath, `${JSON.stringify(nextData, null, 2)}\n`, "utf-8");
59
+ return {
60
+ path: leasePath,
61
+ release: () => {
62
+ try {
63
+ if (JSON.parse(readFileSync(leasePath, "utf-8")).runId === runId) rmSync(leasePath, { force: true });
64
+ } catch {}
65
+ }
66
+ };
67
+ }
68
+
69
+ //#endregion
70
+ //#region src/metrics.ts
71
+ function round(value) {
72
+ return Math.round(value * 1e3) / 1e3;
73
+ }
74
+ function diffHours(start, end) {
75
+ return (Date.parse(end) - Date.parse(start)) / 36e5;
76
+ }
77
+ function average(values) {
78
+ if (values.length === 0) return null;
79
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
80
+ }
81
+ function computeInterruptRate(interrupts, taskAttempts) {
82
+ if (taskAttempts.length === 0) return 0;
83
+ return round(interrupts.length / taskAttempts.length);
84
+ }
85
+ function computeAutonomousCompletionRate(taskAttempts) {
86
+ const completed = taskAttempts.filter((task) => task.status === "completed");
87
+ if (completed.length === 0) return 0;
88
+ return round(completed.filter((task) => !task.interrupted).length / completed.length);
89
+ }
90
+ function computeContextCoverageScore(interrupts) {
91
+ const observed = new Set(interrupts.map((interrupt) => interrupt.category));
92
+ if (observed.size === 0) return 0;
93
+ return round(new Set(interrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category)).size / observed.size);
94
+ }
95
+ function computeMeanTimeToCorrect(interrupts) {
96
+ const value = average(interrupts.filter((interrupt) => interrupt.resolvedAt).map((interrupt) => diffHours(interrupt.createdAt, interrupt.resolvedAt)));
97
+ return value === null ? null : round(value);
98
+ }
99
+ function computeFeedbackToDemoCycleTime(feedbackCycles) {
100
+ const value = average(feedbackCycles.map((cycle) => diffHours(cycle.feedbackAt, cycle.demoReadyAt)));
101
+ return value === null ? null : round(value);
102
+ }
103
+ function computeMetrics(input) {
104
+ const observedCategories = new Set(input.interrupts.map((interrupt) => interrupt.category));
105
+ const coveredCategories = new Set(input.interrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category));
106
+ const completedTasks = input.taskAttempts.filter((task) => task.status === "completed").length;
107
+ return {
108
+ interruptRate: computeInterruptRate(input.interrupts, input.taskAttempts),
109
+ autonomousCompletionRate: computeAutonomousCompletionRate(input.taskAttempts),
110
+ contextCoverageScore: computeContextCoverageScore(input.interrupts),
111
+ meanTimeToCorrectHours: computeMeanTimeToCorrect(input.interrupts),
112
+ feedbackToDemoCycleTimeHours: computeFeedbackToDemoCycleTime(input.feedbackCycles ?? []),
113
+ totals: {
114
+ taskAttempts: input.taskAttempts.length,
115
+ completedTasks,
116
+ interrupts: input.interrupts.length,
117
+ observedInterruptCategories: observedCategories.size,
118
+ coveredInterruptCategories: coveredCategories.size
119
+ }
120
+ };
121
+ }
122
+
123
+ //#endregion
124
+ //#region src/shell.ts
125
+ function runCommandSync(command, args, options) {
126
+ const result = spawnSync(command, args, {
127
+ cwd: options?.cwd,
128
+ env: options?.env,
129
+ encoding: "utf-8"
130
+ });
131
+ const errorText = result.error instanceof Error ? `${result.error.message}
132
+ ` : "";
133
+ return {
134
+ stdout: result.stdout ?? "",
135
+ stderr: `${errorText}${result.stderr ?? ""}`,
136
+ exitCode: result.status ?? 1
137
+ };
138
+ }
139
+ function assertSuccess(result, context) {
140
+ if (result.exitCode === 0) return;
141
+ const details = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
142
+ throw new Error(`${context} failed${details ? `\n${details}` : ""}`);
143
+ }
144
+
145
+ //#endregion
146
+ //#region src/repo.ts
147
+ function gitResult(cwd, args) {
148
+ return runCommandSync("git", args, { cwd });
149
+ }
150
+ function sanitize(value) {
151
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
152
+ }
153
+ function isGitRepo(cwd) {
154
+ const result = gitResult(cwd, ["rev-parse", "--is-inside-work-tree"]);
155
+ return result.exitCode === 0 && result.stdout.trim() === "true";
156
+ }
157
+ function getRepoRoot(cwd) {
158
+ if (!isGitRepo(cwd)) return resolve(cwd);
159
+ const result = gitResult(cwd, ["rev-parse", "--show-toplevel"]);
160
+ assertSuccess(result, "git rev-parse --show-toplevel");
161
+ return resolve(result.stdout.trim());
162
+ }
163
+ function getCurrentBranch(cwd) {
164
+ if (!isGitRepo(cwd)) return null;
165
+ const result = gitResult(cwd, [
166
+ "rev-parse",
167
+ "--abbrev-ref",
168
+ "HEAD"
169
+ ]);
170
+ if (result.exitCode !== 0) return null;
171
+ return result.stdout.trim() || null;
172
+ }
173
+ function getRepoIdentity(cwd) {
174
+ if (!isGitRepo(cwd)) return resolve(cwd);
175
+ const remote = gitResult(cwd, [
176
+ "config",
177
+ "--get",
178
+ "remote.origin.url"
179
+ ]);
180
+ const remoteValue = remote.stdout.trim();
181
+ if (remote.exitCode === 0 && remoteValue) return remoteValue;
182
+ const root = gitResult(cwd, ["rev-parse", "--show-toplevel"]);
183
+ assertSuccess(root, "git rev-parse --show-toplevel");
184
+ const commonDir = gitResult(cwd, ["rev-parse", "--git-common-dir"]);
185
+ assertSuccess(commonDir, "git rev-parse --git-common-dir");
186
+ const rootPath = resolve(root.stdout.trim());
187
+ const commonDirPath = commonDir.stdout.trim();
188
+ return `${basename(rootPath)}:${rootPath}:${existsSync(commonDirPath) ? resolve(commonDirPath) : commonDirPath}`;
189
+ }
190
+ function createRepoSlug(repoId, repoRoot) {
191
+ return `${sanitize(basename(repoRoot))}-${createHash("sha1").update(repoId).digest("hex").slice(0, 8)}`;
192
+ }
193
+
194
+ //#endregion
195
+ //#region src/controller.ts
196
+ function createRunId() {
197
+ return `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
198
+ }
199
+ function writeJson(path, value) {
200
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
201
+ }
202
+ function writeText(path, value) {
203
+ writeFileSync(path, value, "utf-8");
204
+ }
205
+ function createPiPrompt(input) {
206
+ const goal = input.goal ?? "continue from current scaffold state";
207
+ if (input.planPath) return [
208
+ "Read the private plans in:",
209
+ `- ${input.planPath}`,
210
+ "",
211
+ "and the current code in:",
212
+ `- ${input.repoRoot}`,
213
+ "",
214
+ `Goal: ${goal}`,
215
+ "Continue from the current scaffold state.",
216
+ "Keep any persisted run artifacts high-signal and avoid copying private plan contents into them."
217
+ ].join("\n");
218
+ return [
219
+ `Work in the repository at: ${input.repoRoot}`,
220
+ `Goal: ${goal}`,
221
+ "No private plan path is configured for this run.",
222
+ 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.",
223
+ "Continue from the current scaffold state."
224
+ ].join("\n");
225
+ }
226
+ function createManifest(input) {
227
+ return {
228
+ runId: input.runId,
229
+ repoId: input.repoId,
230
+ repoSlug: input.repoSlug,
231
+ repoRoot: input.repoRoot,
232
+ branch: input.branch,
233
+ goal: input.goal,
234
+ planPath: input.planPath,
235
+ recommendedPlanDir: input.recommendedPlanDir,
236
+ mode: input.mode,
237
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
238
+ endedAt: null,
239
+ stopReason: null,
240
+ leasePath: input.leasePath,
241
+ piExitCode: null,
242
+ completedTaskCount: 0,
243
+ blockedTaskCount: 0,
244
+ skippedTaskCount: 0,
245
+ totalCostUsd: 0
246
+ };
247
+ }
248
+ function createSummary(input) {
249
+ const success = input.exitCode === 0;
250
+ const recommendation = input.recommendedPlanDir === null ? "" : ` No plan path was configured. Recommended private plans location: ${input.recommendedPlanDir}.`;
251
+ return {
252
+ runId: input.runId,
253
+ mode: input.mode,
254
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
255
+ success,
256
+ message: success ? `Pi invocation completed.${recommendation}` : `Pi invocation failed.${recommendation}`,
257
+ piExitCode: input.exitCode,
258
+ recommendedPlanDir: input.recommendedPlanDir
259
+ };
260
+ }
261
+ function runController(options) {
262
+ const cwd = options.cwd ?? process.cwd();
263
+ const artifactsDir = options.artifactsDir;
264
+ const repoRoot = getRepoRoot(cwd);
265
+ const repoId = getRepoIdentity(repoRoot);
266
+ const repoSlug = createRepoSlug(repoId, repoRoot);
267
+ const branch = options.branch ?? getCurrentBranch(repoRoot) ?? "workspace";
268
+ const goal = options.goal ?? null;
269
+ const planPath = options.planPath ?? null;
270
+ const recommendedPlanDir = planPath ? null : options.recommendedPlanDir ?? null;
271
+ const mode = options.mode ?? "single-pi";
272
+ const piCommand = options.piCommand ?? "pi";
273
+ const runId = createRunId();
274
+ const runDir = join(artifactsDir, "runs", runId);
275
+ const latestDir = join(artifactsDir, "latest");
276
+ const stdoutPath = join(runDir, "stdout.txt");
277
+ const stderrPath = join(runDir, "stderr.txt");
278
+ const prompt = createPiPrompt({
279
+ repoRoot,
280
+ planPath,
281
+ goal,
282
+ recommendedPlanDir
283
+ });
284
+ mkdirSync(runDir, { recursive: true });
285
+ mkdirSync(latestDir, { recursive: true });
286
+ writeText(join(runDir, "questions.jsonl"), "");
287
+ writeText(join(runDir, "interventions.jsonl"), "");
288
+ writeJson(join(runDir, "agent-state.json"), {
289
+ status: "starting",
290
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
291
+ });
292
+ const lease = acquireRepoLease(runId, repoId, branch);
293
+ try {
294
+ const manifest = createManifest({
295
+ runId,
296
+ repoId,
297
+ repoSlug,
298
+ repoRoot,
299
+ branch,
300
+ goal,
301
+ planPath,
302
+ recommendedPlanDir,
303
+ mode,
304
+ leasePath: lease.path
305
+ });
306
+ appendJsonl(join(runDir, "events.jsonl"), {
307
+ type: "run_started",
308
+ runId,
309
+ repoId,
310
+ repoSlug,
311
+ branch,
312
+ createdAt: manifest.startedAt
313
+ });
314
+ const piStartedAt = (/* @__PURE__ */ new Date()).toISOString();
315
+ appendJsonl(join(runDir, "events.jsonl"), {
316
+ type: "pi_invocation_started",
317
+ runId,
318
+ command: piCommand,
319
+ createdAt: piStartedAt
320
+ });
321
+ const piResult = runCommandSync(piCommand, [
322
+ "--no-session",
323
+ "-p",
324
+ prompt
325
+ ], {
326
+ cwd: repoRoot,
327
+ env: process.env
328
+ });
329
+ const piEndedAt = (/* @__PURE__ */ new Date()).toISOString();
330
+ writeText(stdoutPath, piResult.stdout);
331
+ writeText(stderrPath, piResult.stderr);
332
+ const piInvocation = {
333
+ command: piCommand,
334
+ cwd: repoRoot,
335
+ repoRoot,
336
+ planPath,
337
+ goal,
338
+ startedAt: piStartedAt,
339
+ endedAt: piEndedAt,
340
+ exitCode: piResult.exitCode,
341
+ stdoutPath,
342
+ stderrPath,
343
+ promptSummary: planPath ? "Read private plan path and continue from current scaffold state." : "Continue from current scaffold state without a configured private plan path."
344
+ };
345
+ writeJson(join(runDir, "pi-invocation.json"), piInvocation);
346
+ appendJsonl(join(runDir, "events.jsonl"), {
347
+ type: "pi_invocation_finished",
348
+ runId,
349
+ command: piCommand,
350
+ exitCode: piInvocation.exitCode,
351
+ createdAt: piEndedAt
352
+ });
353
+ const metrics = computeMetrics({
354
+ taskAttempts: [],
355
+ interrupts: []
356
+ });
357
+ const summary = createSummary({
358
+ runId,
359
+ mode,
360
+ exitCode: piInvocation.exitCode,
361
+ recommendedPlanDir
362
+ });
363
+ const finalManifest = {
364
+ ...manifest,
365
+ endedAt: piEndedAt,
366
+ stopReason: piInvocation.exitCode === 0 ? "pi invocation completed" : `pi invocation exited with code ${piInvocation.exitCode}`,
367
+ piExitCode: piInvocation.exitCode
368
+ };
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"), {
373
+ status: summary.success ? "completed" : "failed",
374
+ updatedAt: piEndedAt,
375
+ exitCode: piInvocation.exitCode
376
+ });
377
+ writeJson(join(latestDir, "manifest.json"), finalManifest);
378
+ writeJson(join(latestDir, "metrics.json"), metrics);
379
+ writeJson(join(latestDir, "run-summary.json"), summary);
380
+ appendJsonl(join(runDir, "events.jsonl"), {
381
+ type: "run_finished",
382
+ runId,
383
+ createdAt: finalManifest.endedAt,
384
+ stopReason: finalManifest.stopReason,
385
+ metrics
386
+ });
387
+ return {
388
+ runId,
389
+ runDir,
390
+ latestDir,
391
+ manifest: finalManifest,
392
+ metrics,
393
+ summary,
394
+ piInvocation
395
+ };
396
+ } finally {
397
+ lease.release();
398
+ }
399
+ }
400
+
401
+ //#endregion
402
+ //#region src/interrupts.ts
403
+ function createInterruptRecord(input) {
404
+ return {
405
+ id: randomUUID(),
406
+ runId: input.runId,
407
+ category: input.category,
408
+ summary: input.summary,
409
+ requiresHuman: input.requiresHuman ?? true,
410
+ createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
411
+ ...input.taskId ? { taskId: input.taskId } : {},
412
+ ...input.subtype ? { subtype: input.subtype } : {}
413
+ };
414
+ }
415
+ function resolveInterrupt(interrupt, options) {
416
+ return {
417
+ ...interrupt,
418
+ ...options.resolvedAt ? { resolvedAt: options.resolvedAt } : {},
419
+ ...options.fixType ? { fixType: options.fixType } : {}
420
+ };
421
+ }
422
+
423
+ //#endregion
424
+ export { acquireRepoLease, appendJsonl, assertSuccess, computeAutonomousCompletionRate, computeContextCoverageScore, computeFeedbackToDemoCycleTime, computeInterruptRate, computeMeanTimeToCorrect, computeMetrics, createInterruptRecord, createRepoSlug, getCurrentBranch, getRepoIdentity, getRepoRoot, isGitRepo, readJsonl, resolveInterrupt, runCommandSync, runController };
425
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["sanitize"],"sources":["../src/events.ts","../src/lease.ts","../src/metrics.ts","../src/shell.ts","../src/repo.ts","../src/controller.ts","../src/interrupts.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\n\nexport function appendJsonl(filePath: string, value: unknown) {\n\tmkdirSync(dirname(filePath), { recursive: true })\n\twriteFileSync(filePath, `${JSON.stringify(value)}\\n`, { encoding: \"utf-8\", flag: \"a\" })\n}\n\nexport function readJsonl<T>(filePath: string): T[] {\n\ttry {\n\t\tconst raw = readFileSync(filePath, \"utf-8\")\n\t\treturn raw\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((line) => line.trim())\n\t\t\t.filter(Boolean)\n\t\t\t.map((line) => JSON.parse(line) as T)\n\t} catch {\n\t\treturn []\n\t}\n}\n","import { mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\"\nimport { homedir, hostname } from \"node:os\"\nimport { join } from \"node:path\"\n\ninterface LeaseData {\n\trunId: string\n\trepoId: string\n\tbranch: string\n\tpid: number\n\thostname: string\n\tstartedAt: string\n}\n\nfunction sanitize(value: string): string {\n\treturn value.replace(/[^a-zA-Z0-9._-]+/g, \"_\")\n}\n\nfunction processAlive(pid: number): boolean {\n\tif (!Number.isFinite(pid) || pid <= 0) return false\n\ttry {\n\t\tprocess.kill(pid, 0)\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\nexport function acquireRepoLease(runId: string, repoId: string, branch: string): { path: string; release: () => void } {\n\tconst locksDir = join(homedir(), \".pi-town\", \"locks\")\n\tmkdirSync(locksDir, { recursive: true })\n\n\tconst leasePath = join(locksDir, `pi-town-${sanitize(repoId)}-${sanitize(branch)}.json`)\n\tconst nextData: LeaseData = {\n\t\trunId,\n\t\trepoId,\n\t\tbranch,\n\t\tpid: process.pid,\n\t\thostname: hostname(),\n\t\tstartedAt: new Date().toISOString(),\n\t}\n\n\ttry {\n\t\tconst current = JSON.parse(readFileSync(leasePath, \"utf-8\")) as LeaseData\n\t\tif (processAlive(current.pid)) {\n\t\t\tthrow new Error(`Pi Town lease already held by pid ${current.pid} on ${current.hostname} for run ${current.runId}.`)\n\t\t}\n\t\trmSync(leasePath, { force: true })\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n\t\t\tif (error instanceof Error && error.message.startsWith(\"Pi Town lease already held\")) throw error\n\t\t}\n\t}\n\n\twriteFileSync(leasePath, `${JSON.stringify(nextData, null, 2)}\\n`, \"utf-8\")\n\n\treturn {\n\t\tpath: leasePath,\n\t\trelease: () => {\n\t\t\ttry {\n\t\t\t\tconst current = JSON.parse(readFileSync(leasePath, \"utf-8\")) as LeaseData\n\t\t\t\tif (current.runId === runId) rmSync(leasePath, { force: true })\n\t\t\t} catch {\n\t\t\t\t// ignore cleanup failures\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { FeedbackCycle, InterruptRecord, MetricsSnapshot, TaskAttempt } from \"./types.js\"\n\nfunction round(value: number): number {\n\treturn Math.round(value * 1000) / 1000\n}\n\nfunction diffHours(start: string, end: string): number {\n\treturn (Date.parse(end) - Date.parse(start)) / 3_600_000\n}\n\nfunction average(values: number[]): number | null {\n\tif (values.length === 0) return null\n\treturn values.reduce((sum, value) => sum + value, 0) / values.length\n}\n\nexport function computeInterruptRate(interrupts: InterruptRecord[], taskAttempts: TaskAttempt[]): number {\n\tif (taskAttempts.length === 0) return 0\n\treturn round(interrupts.length / taskAttempts.length)\n}\n\nexport function computeAutonomousCompletionRate(taskAttempts: TaskAttempt[]): number {\n\tconst completed = taskAttempts.filter((task) => task.status === \"completed\")\n\tif (completed.length === 0) return 0\n\tconst autonomous = completed.filter((task) => !task.interrupted)\n\treturn round(autonomous.length / completed.length)\n}\n\nexport function computeContextCoverageScore(interrupts: InterruptRecord[]): number {\n\tconst observed = new Set(interrupts.map((interrupt) => interrupt.category))\n\tif (observed.size === 0) return 0\n\n\tconst covered = new Set(\n\t\tinterrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category),\n\t)\n\n\treturn round(covered.size / observed.size)\n}\n\nexport function computeMeanTimeToCorrect(interrupts: InterruptRecord[]): number | null {\n\tconst resolved = interrupts.filter((interrupt) => interrupt.resolvedAt)\n\tconst hours = resolved.map((interrupt) => diffHours(interrupt.createdAt, interrupt.resolvedAt!))\n\tconst value = average(hours)\n\treturn value === null ? null : round(value)\n}\n\nexport function computeFeedbackToDemoCycleTime(feedbackCycles: FeedbackCycle[]): number | null {\n\tconst hours = feedbackCycles.map((cycle) => diffHours(cycle.feedbackAt, cycle.demoReadyAt))\n\tconst value = average(hours)\n\treturn value === null ? null : round(value)\n}\n\nexport function computeMetrics(input: {\n\ttaskAttempts: TaskAttempt[]\n\tinterrupts: InterruptRecord[]\n\tfeedbackCycles?: FeedbackCycle[]\n}): MetricsSnapshot {\n\tconst observedCategories = new Set(input.interrupts.map((interrupt) => interrupt.category))\n\tconst coveredCategories = new Set(\n\t\tinput.interrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category),\n\t)\n\tconst completedTasks = input.taskAttempts.filter((task) => task.status === \"completed\").length\n\n\treturn {\n\t\tinterruptRate: computeInterruptRate(input.interrupts, input.taskAttempts),\n\t\tautonomousCompletionRate: computeAutonomousCompletionRate(input.taskAttempts),\n\t\tcontextCoverageScore: computeContextCoverageScore(input.interrupts),\n\t\tmeanTimeToCorrectHours: computeMeanTimeToCorrect(input.interrupts),\n\t\tfeedbackToDemoCycleTimeHours: computeFeedbackToDemoCycleTime(input.feedbackCycles ?? []),\n\t\ttotals: {\n\t\t\ttaskAttempts: input.taskAttempts.length,\n\t\t\tcompletedTasks,\n\t\t\tinterrupts: input.interrupts.length,\n\t\t\tobservedInterruptCategories: observedCategories.size,\n\t\t\tcoveredInterruptCategories: coveredCategories.size,\n\t\t},\n\t}\n}\n","import { spawnSync } from \"node:child_process\"\n\nexport interface CommandResult {\n\tstdout: string\n\tstderr: string\n\texitCode: number\n}\n\nexport function runCommandSync(\n\tcommand: string,\n\targs: string[],\n\toptions?: { cwd?: string; env?: NodeJS.ProcessEnv },\n): CommandResult {\n\tconst result = spawnSync(command, args, {\n\t\tcwd: options?.cwd,\n\t\tenv: options?.env,\n\t\tencoding: \"utf-8\",\n\t})\n\tconst errorText = result.error instanceof Error ? `${result.error.message}\n` : \"\"\n\n\treturn {\n\t\tstdout: result.stdout ?? \"\",\n\t\tstderr: `${errorText}${result.stderr ?? \"\"}`,\n\t\texitCode: result.status ?? 1,\n\t}\n}\n\nexport function assertSuccess(result: CommandResult, context: string) {\n\tif (result.exitCode === 0) return\n\tconst details = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join(\"\\n\")\n\tthrow new Error(`${context} failed${details ? `\\n${details}` : \"\"}`)\n}\n","import { createHash } from \"node:crypto\"\nimport { existsSync } from \"node:fs\"\nimport { basename, resolve } from \"node:path\"\nimport { assertSuccess, runCommandSync } from \"./shell.js\"\n\nfunction gitResult(cwd: string, args: string[]) {\n\treturn runCommandSync(\"git\", args, { cwd })\n}\n\nfunction sanitize(value: string): string {\n\treturn value.replace(/[^a-zA-Z0-9._-]+/g, \"-\").replace(/^-+|-+$/g, \"\") || \"repo\"\n}\n\nexport function isGitRepo(cwd: string): boolean {\n\tconst result = gitResult(cwd, [\"rev-parse\", \"--is-inside-work-tree\"])\n\treturn result.exitCode === 0 && result.stdout.trim() === \"true\"\n}\n\nexport function getRepoRoot(cwd: string): string {\n\tif (!isGitRepo(cwd)) return resolve(cwd)\n\tconst result = gitResult(cwd, [\"rev-parse\", \"--show-toplevel\"])\n\tassertSuccess(result, \"git rev-parse --show-toplevel\")\n\treturn resolve(result.stdout.trim())\n}\n\nexport function getCurrentBranch(cwd: string): string | null {\n\tif (!isGitRepo(cwd)) return null\n\tconst result = gitResult(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"])\n\tif (result.exitCode !== 0) return null\n\tconst branch = result.stdout.trim()\n\treturn branch || null\n}\n\nexport function getRepoIdentity(cwd: string): string {\n\tif (!isGitRepo(cwd)) return resolve(cwd)\n\n\tconst remote = gitResult(cwd, [\"config\", \"--get\", \"remote.origin.url\"])\n\tconst remoteValue = remote.stdout.trim()\n\tif (remote.exitCode === 0 && remoteValue) return remoteValue\n\n\tconst root = gitResult(cwd, [\"rev-parse\", \"--show-toplevel\"])\n\tassertSuccess(root, \"git rev-parse --show-toplevel\")\n\tconst commonDir = gitResult(cwd, [\"rev-parse\", \"--git-common-dir\"])\n\tassertSuccess(commonDir, \"git rev-parse --git-common-dir\")\n\n\tconst rootPath = resolve(root.stdout.trim())\n\tconst commonDirPath = commonDir.stdout.trim()\n\treturn `${basename(rootPath)}:${rootPath}:${existsSync(commonDirPath) ? resolve(commonDirPath) : commonDirPath}`\n}\n\nexport function createRepoSlug(repoId: string, repoRoot: string): string {\n\tconst name = sanitize(basename(repoRoot))\n\tconst digest = createHash(\"sha1\").update(repoId).digest(\"hex\").slice(0, 8)\n\treturn `${name}-${digest}`\n}\n","import { mkdirSync, writeFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { appendJsonl } from \"./events.js\"\nimport { acquireRepoLease } from \"./lease.js\"\nimport { computeMetrics } from \"./metrics.js\"\nimport { createRepoSlug, getCurrentBranch, getRepoIdentity, getRepoRoot } from \"./repo.js\"\nimport { runCommandSync } from \"./shell.js\"\nimport type { ControllerRunResult, PiInvocationRecord, RunManifest, RunOptions, RunSummary } from \"./types.js\"\n\nfunction createRunId(): string {\n\treturn `run-${new Date().toISOString().replace(/[:.]/g, \"-\")}`\n}\n\nfunction writeJson(path: string, value: unknown) {\n\twriteFileSync(path, `${JSON.stringify(value, null, 2)}\\n`, \"utf-8\")\n}\n\nfunction writeText(path: string, value: string) {\n\twriteFileSync(path, value, \"utf-8\")\n}\n\nfunction createPiPrompt(input: {\n\trepoRoot: string\n\tplanPath: string | null\n\tgoal: string | null\n\trecommendedPlanDir: string | null\n}): string {\n\tconst goal = input.goal ?? \"continue from current scaffold state\"\n\n\tif (input.planPath) {\n\t\treturn [\n\t\t\t\"Read the private plans in:\",\n\t\t\t`- ${input.planPath}`,\n\t\t\t\"\",\n\t\t\t\"and the current code in:\",\n\t\t\t`- ${input.repoRoot}`,\n\t\t\t\"\",\n\t\t\t`Goal: ${goal}`,\n\t\t\t\"Continue from the current scaffold state.\",\n\t\t\t\"Keep any persisted run artifacts high-signal and avoid copying private plan contents into them.\",\n\t\t].join(\"\\n\")\n\t}\n\n\treturn [\n\t\t`Work in the repository at: ${input.repoRoot}`,\n\t\t`Goal: ${goal}`,\n\t\t\"No private plan path is configured for this run.\",\n\t\tinput.recommendedPlanDir\n\t\t\t? `If you need private plans, use a user-owned location such as: ${input.recommendedPlanDir}`\n\t\t\t: \"If you need private plans, keep them in a user-owned location outside the repo.\",\n\t\t\"Continue from the current scaffold state.\",\n\t].join(\"\\n\")\n}\n\nfunction createManifest(input: {\n\trunId: string\n\trepoId: string\n\trepoSlug: string\n\trepoRoot: string\n\tbranch: string\n\tgoal: string | null\n\tplanPath: string | null\n\trecommendedPlanDir: string | null\n\tmode: \"single-pi\"\n\tleasePath: string\n}): RunManifest {\n\treturn {\n\t\trunId: input.runId,\n\t\trepoId: input.repoId,\n\t\trepoSlug: input.repoSlug,\n\t\trepoRoot: input.repoRoot,\n\t\tbranch: input.branch,\n\t\tgoal: input.goal,\n\t\tplanPath: input.planPath,\n\t\trecommendedPlanDir: input.recommendedPlanDir,\n\t\tmode: input.mode,\n\t\tstartedAt: new Date().toISOString(),\n\t\tendedAt: null,\n\t\tstopReason: null,\n\t\tleasePath: input.leasePath,\n\t\tpiExitCode: null,\n\t\tcompletedTaskCount: 0,\n\t\tblockedTaskCount: 0,\n\t\tskippedTaskCount: 0,\n\t\ttotalCostUsd: 0,\n\t}\n}\n\nfunction createSummary(input: {\n\trunId: string\n\tmode: \"single-pi\"\n\texitCode: number\n\trecommendedPlanDir: string | null\n}): RunSummary {\n\tconst success = input.exitCode === 0\n\tconst recommendation =\n\t\tinput.recommendedPlanDir === null\n\t\t\t? \"\"\n\t\t\t: ` No plan path was configured. Recommended private plans location: ${input.recommendedPlanDir}.`\n\n\treturn {\n\t\trunId: input.runId,\n\t\tmode: input.mode,\n\t\tcreatedAt: new Date().toISOString(),\n\t\tsuccess,\n\t\tmessage: success ? `Pi invocation completed.${recommendation}` : `Pi invocation failed.${recommendation}`,\n\t\tpiExitCode: input.exitCode,\n\t\trecommendedPlanDir: input.recommendedPlanDir,\n\t}\n}\n\nexport function runController(options: RunOptions): ControllerRunResult {\n\tconst cwd = options.cwd ?? process.cwd()\n\tconst artifactsDir = options.artifactsDir\n\tconst repoRoot = getRepoRoot(cwd)\n\tconst repoId = getRepoIdentity(repoRoot)\n\tconst repoSlug = createRepoSlug(repoId, repoRoot)\n\tconst branch = options.branch ?? getCurrentBranch(repoRoot) ?? \"workspace\"\n\tconst goal = options.goal ?? null\n\tconst planPath = options.planPath ?? null\n\tconst recommendedPlanDir = planPath ? null : (options.recommendedPlanDir ?? null)\n\tconst mode = options.mode ?? \"single-pi\"\n\tconst piCommand = options.piCommand ?? \"pi\"\n\tconst runId = createRunId()\n\tconst runDir = join(artifactsDir, \"runs\", runId)\n\tconst latestDir = join(artifactsDir, \"latest\")\n\tconst stdoutPath = join(runDir, \"stdout.txt\")\n\tconst stderrPath = join(runDir, \"stderr.txt\")\n\tconst prompt = createPiPrompt({ repoRoot, planPath, goal, recommendedPlanDir })\n\n\tmkdirSync(runDir, { recursive: true })\n\tmkdirSync(latestDir, { recursive: true })\n\n\twriteText(join(runDir, \"questions.jsonl\"), \"\")\n\twriteText(join(runDir, \"interventions.jsonl\"), \"\")\n\twriteJson(join(runDir, \"agent-state.json\"), {\n\t\tstatus: \"starting\",\n\t\tupdatedAt: new Date().toISOString(),\n\t})\n\n\tconst lease = acquireRepoLease(runId, repoId, branch)\n\n\ttry {\n\t\tconst manifest = createManifest({\n\t\t\trunId,\n\t\t\trepoId,\n\t\t\trepoSlug,\n\t\t\trepoRoot,\n\t\t\tbranch,\n\t\t\tgoal,\n\t\t\tplanPath,\n\t\t\trecommendedPlanDir,\n\t\t\tmode,\n\t\t\tleasePath: lease.path,\n\t\t})\n\n\t\tappendJsonl(join(runDir, \"events.jsonl\"), {\n\t\t\ttype: \"run_started\",\n\t\t\trunId,\n\t\t\trepoId,\n\t\t\trepoSlug,\n\t\t\tbranch,\n\t\t\tcreatedAt: manifest.startedAt,\n\t\t})\n\n\t\tconst piStartedAt = new Date().toISOString()\n\t\tappendJsonl(join(runDir, \"events.jsonl\"), {\n\t\t\ttype: \"pi_invocation_started\",\n\t\t\trunId,\n\t\t\tcommand: piCommand,\n\t\t\tcreatedAt: piStartedAt,\n\t\t})\n\n\t\tconst piResult = runCommandSync(piCommand, [\"--no-session\", \"-p\", prompt], {\n\t\t\tcwd: repoRoot,\n\t\t\tenv: process.env,\n\t\t})\n\t\tconst piEndedAt = new Date().toISOString()\n\n\t\twriteText(stdoutPath, piResult.stdout)\n\t\twriteText(stderrPath, piResult.stderr)\n\n\t\tconst piInvocation: PiInvocationRecord = {\n\t\t\tcommand: piCommand,\n\t\t\tcwd: repoRoot,\n\t\t\trepoRoot,\n\t\t\tplanPath,\n\t\t\tgoal,\n\t\t\tstartedAt: piStartedAt,\n\t\t\tendedAt: piEndedAt,\n\t\t\texitCode: piResult.exitCode,\n\t\t\tstdoutPath,\n\t\t\tstderrPath,\n\t\t\tpromptSummary: planPath\n\t\t\t\t? \"Read private plan path and continue from current scaffold state.\"\n\t\t\t\t: \"Continue from current scaffold state without a configured private plan path.\",\n\t\t}\n\t\twriteJson(join(runDir, \"pi-invocation.json\"), piInvocation)\n\n\t\tappendJsonl(join(runDir, \"events.jsonl\"), {\n\t\t\ttype: \"pi_invocation_finished\",\n\t\t\trunId,\n\t\t\tcommand: piCommand,\n\t\t\texitCode: piInvocation.exitCode,\n\t\t\tcreatedAt: piEndedAt,\n\t\t})\n\n\t\tconst metrics = computeMetrics({\n\t\t\ttaskAttempts: [],\n\t\t\tinterrupts: [],\n\t\t})\n\t\tconst summary = createSummary({\n\t\t\trunId,\n\t\t\tmode,\n\t\t\texitCode: piInvocation.exitCode,\n\t\t\trecommendedPlanDir,\n\t\t})\n\t\tconst finalManifest: RunManifest = {\n\t\t\t...manifest,\n\t\t\tendedAt: piEndedAt,\n\t\t\tstopReason:\n\t\t\t\tpiInvocation.exitCode === 0\n\t\t\t\t\t? \"pi invocation completed\"\n\t\t\t\t\t: `pi invocation exited with code ${piInvocation.exitCode}`,\n\t\t\tpiExitCode: piInvocation.exitCode,\n\t\t}\n\n\t\twriteJson(join(runDir, \"manifest.json\"), finalManifest)\n\t\twriteJson(join(runDir, \"metrics.json\"), metrics)\n\t\twriteJson(join(runDir, \"run-summary.json\"), summary)\n\t\twriteJson(join(runDir, \"agent-state.json\"), {\n\t\t\tstatus: summary.success ? \"completed\" : \"failed\",\n\t\t\tupdatedAt: piEndedAt,\n\t\t\texitCode: piInvocation.exitCode,\n\t\t})\n\t\twriteJson(join(latestDir, \"manifest.json\"), finalManifest)\n\t\twriteJson(join(latestDir, \"metrics.json\"), metrics)\n\t\twriteJson(join(latestDir, \"run-summary.json\"), summary)\n\n\t\tappendJsonl(join(runDir, \"events.jsonl\"), {\n\t\t\ttype: \"run_finished\",\n\t\t\trunId,\n\t\t\tcreatedAt: finalManifest.endedAt,\n\t\t\tstopReason: finalManifest.stopReason,\n\t\t\tmetrics,\n\t\t})\n\n\t\treturn {\n\t\t\trunId,\n\t\t\trunDir,\n\t\t\tlatestDir,\n\t\t\tmanifest: finalManifest,\n\t\t\tmetrics,\n\t\t\tsummary,\n\t\t\tpiInvocation,\n\t\t}\n\t} finally {\n\t\tlease.release()\n\t}\n}\n","import { randomUUID } from \"node:crypto\"\nimport type { InterruptCategory, InterruptRecord } from \"./types.js\"\n\nexport interface CreateInterruptInput {\n\trunId: string\n\ttaskId?: string\n\tcategory: InterruptCategory\n\tsubtype?: string\n\tsummary: string\n\trequiresHuman?: boolean\n\tcreatedAt?: string\n}\n\nexport function createInterruptRecord(input: CreateInterruptInput): InterruptRecord {\n\treturn {\n\t\tid: randomUUID(),\n\t\trunId: input.runId,\n\t\tcategory: input.category,\n\t\tsummary: input.summary,\n\t\trequiresHuman: input.requiresHuman ?? true,\n\t\tcreatedAt: input.createdAt ?? new Date().toISOString(),\n\t\t...(input.taskId ? { taskId: input.taskId } : {}),\n\t\t...(input.subtype ? { subtype: input.subtype } : {}),\n\t}\n}\n\nexport function resolveInterrupt(\n\tinterrupt: InterruptRecord,\n\toptions: Pick<InterruptRecord, \"resolvedAt\" | \"fixType\">,\n): InterruptRecord {\n\treturn {\n\t\t...interrupt,\n\t\t...(options.resolvedAt ? { resolvedAt: options.resolvedAt } : {}),\n\t\t...(options.fixType ? { fixType: options.fixType } : {}),\n\t}\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@schilderlabs/pitown-core",
3
+ "version": "0.1.0",
4
+ "description": "Core runtime primitives for Pi Town",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.mjs",
9
+ "types": "./dist/index.d.mts"
10
+ }
11
+ },
12
+ "types": "./dist/index.d.mts",
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=24"
19
+ },
20
+ "devDependencies": {
21
+ "eslint": "9.39.3",
22
+ "tsdown": "0.20.3",
23
+ "typescript": "5.9.2",
24
+ "vitest": "4.0.18"
25
+ },
26
+ "keywords": [
27
+ "pitown",
28
+ "orchestration",
29
+ "runtime",
30
+ "night-shift"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/schilderlabs/pitown.git"
35
+ },
36
+ "homepage": "https://github.com/schilderlabs/pitown#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/schilderlabs/pitown/issues"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "license": "MIT",
44
+ "scripts": {
45
+ "build": "tsdown",
46
+ "lint": "eslint .",
47
+ "test": "vitest run --passWithNoTests",
48
+ "typecheck": "tsc --noEmit"
49
+ }
50
+ }