@schilderlabs/pitown 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,27 @@
1
+ # @schilderlabs/pitown
2
+
3
+ The installable Pi Town CLI.
4
+
5
+ Pi Town is an experimental orchestration tool for Pi.
6
+
7
+ For the full project overview, roadmap, and architecture context, see the main repo:
8
+
9
+ - https://github.com/schilderlabs/pitown
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g @schilderlabs/pitown
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ pitown --help
21
+ pitown run --repo /path/to/repo --plan /path/to/private/plans --goal "continue from current scaffold state"
22
+ pitown status
23
+ ```
24
+
25
+ ## Runtime storage
26
+
27
+ By default, Pi Town stores local runtime state under `~/.pi-town` and keeps private plans outside the target repo.
@@ -0,0 +1,182 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { createHash } from "node:crypto";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ //#region ../core/src/shell.ts
8
+ function runCommandSync(command, args, options) {
9
+ const result = spawnSync(command, args, {
10
+ cwd: options?.cwd,
11
+ env: options?.env,
12
+ encoding: "utf-8"
13
+ });
14
+ const errorText = result.error instanceof Error ? `${result.error.message}
15
+ ` : "";
16
+ return {
17
+ stdout: result.stdout ?? "",
18
+ stderr: `${errorText}${result.stderr ?? ""}`,
19
+ exitCode: result.status ?? 1
20
+ };
21
+ }
22
+ function assertSuccess(result, context) {
23
+ if (result.exitCode === 0) return;
24
+ const details = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
25
+ throw new Error(`${context} failed${details ? `\n${details}` : ""}`);
26
+ }
27
+
28
+ //#endregion
29
+ //#region ../core/src/repo.ts
30
+ function gitResult(cwd, args) {
31
+ return runCommandSync("git", args, { cwd });
32
+ }
33
+ function sanitize(value) {
34
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
35
+ }
36
+ function isGitRepo(cwd) {
37
+ const result = gitResult(cwd, ["rev-parse", "--is-inside-work-tree"]);
38
+ return result.exitCode === 0 && result.stdout.trim() === "true";
39
+ }
40
+ function getRepoRoot(cwd) {
41
+ if (!isGitRepo(cwd)) return resolve(cwd);
42
+ const result = gitResult(cwd, ["rev-parse", "--show-toplevel"]);
43
+ assertSuccess(result, "git rev-parse --show-toplevel");
44
+ return resolve(result.stdout.trim());
45
+ }
46
+ function getCurrentBranch(cwd) {
47
+ if (!isGitRepo(cwd)) return null;
48
+ const result = gitResult(cwd, [
49
+ "rev-parse",
50
+ "--abbrev-ref",
51
+ "HEAD"
52
+ ]);
53
+ if (result.exitCode !== 0) return null;
54
+ return result.stdout.trim() || null;
55
+ }
56
+ function getRepoIdentity(cwd) {
57
+ if (!isGitRepo(cwd)) return resolve(cwd);
58
+ const remote = gitResult(cwd, [
59
+ "config",
60
+ "--get",
61
+ "remote.origin.url"
62
+ ]);
63
+ const remoteValue = remote.stdout.trim();
64
+ if (remote.exitCode === 0 && remoteValue) return remoteValue;
65
+ const root = gitResult(cwd, ["rev-parse", "--show-toplevel"]);
66
+ assertSuccess(root, "git rev-parse --show-toplevel");
67
+ const commonDir = gitResult(cwd, ["rev-parse", "--git-common-dir"]);
68
+ assertSuccess(commonDir, "git rev-parse --git-common-dir");
69
+ const rootPath = resolve(root.stdout.trim());
70
+ const commonDirPath = commonDir.stdout.trim();
71
+ return `${basename(rootPath)}:${rootPath}:${existsSync(commonDirPath) ? resolve(commonDirPath) : commonDirPath}`;
72
+ }
73
+ function createRepoSlug(repoId, repoRoot) {
74
+ return `${sanitize(basename(repoRoot))}-${createHash("sha1").update(repoId).digest("hex").slice(0, 8)}`;
75
+ }
76
+
77
+ //#endregion
78
+ //#region src/paths.ts
79
+ function getTownHomeDir() {
80
+ return join(homedir(), ".pi-town");
81
+ }
82
+ function getUserConfigPath() {
83
+ return join(getTownHomeDir(), "config.json");
84
+ }
85
+ function getPlansRootDir() {
86
+ return join(getTownHomeDir(), "plans");
87
+ }
88
+ function getReposRootDir() {
89
+ return join(getTownHomeDir(), "repos");
90
+ }
91
+ function getRepoArtifactsDir(repoSlug) {
92
+ return join(getReposRootDir(), repoSlug);
93
+ }
94
+ function getLatestRunPointerPath() {
95
+ return join(getTownHomeDir(), "latest-run.json");
96
+ }
97
+ function getRepoLatestRunPointerPath(repoSlug) {
98
+ return join(getRepoArtifactsDir(repoSlug), "latest-run.json");
99
+ }
100
+ function getRecommendedPlanDir(repoSlug) {
101
+ return join(getPlansRootDir(), repoSlug);
102
+ }
103
+
104
+ //#endregion
105
+ //#region src/config.ts
106
+ const DEFAULT_GOAL = "continue from current scaffold state";
107
+ function expandHome(value) {
108
+ if (value === "~") return homedir();
109
+ if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
110
+ return value;
111
+ }
112
+ function resolvePathValue(value, baseDir) {
113
+ if (!value) return void 0;
114
+ const expanded = expandHome(value);
115
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded);
116
+ }
117
+ function parseCliFlags(argv) {
118
+ const flags = { help: false };
119
+ for (let index = 0; index < argv.length; index += 1) {
120
+ const arg = argv[index];
121
+ if (arg === "--help" || arg === "-h") {
122
+ flags.help = true;
123
+ continue;
124
+ }
125
+ if (arg.startsWith("--repo=")) {
126
+ flags.repo = arg.slice(7);
127
+ continue;
128
+ }
129
+ if (arg === "--repo") {
130
+ const value = argv[index + 1];
131
+ if (!value) throw new Error("Missing value for --repo");
132
+ flags.repo = value;
133
+ index += 1;
134
+ continue;
135
+ }
136
+ if (arg.startsWith("--plan=")) {
137
+ flags.plan = arg.slice(7);
138
+ continue;
139
+ }
140
+ if (arg === "--plan") {
141
+ const value = argv[index + 1];
142
+ if (!value) throw new Error("Missing value for --plan");
143
+ flags.plan = value;
144
+ index += 1;
145
+ continue;
146
+ }
147
+ if (arg.startsWith("--goal=")) {
148
+ flags.goal = arg.slice(7);
149
+ continue;
150
+ }
151
+ if (arg === "--goal") {
152
+ const value = argv[index + 1];
153
+ if (!value) throw new Error("Missing value for --goal");
154
+ flags.goal = value;
155
+ index += 1;
156
+ continue;
157
+ }
158
+ throw new Error(`Unknown argument: ${arg}`);
159
+ }
160
+ return flags;
161
+ }
162
+ function loadUserConfig() {
163
+ const configPath = getUserConfigPath();
164
+ if (!existsSync(configPath)) return {};
165
+ return JSON.parse(readFileSync(configPath, "utf-8"));
166
+ }
167
+ function resolveRunConfig(argv) {
168
+ const flags = parseCliFlags(argv);
169
+ const configPath = getUserConfigPath();
170
+ const userConfig = loadUserConfig();
171
+ const configDir = dirname(configPath);
172
+ return {
173
+ repo: resolvePathValue(flags.repo, process.cwd()) ?? resolvePathValue(userConfig.repo, configDir) ?? resolve(process.cwd()),
174
+ plan: resolvePathValue(flags.plan, process.cwd()) ?? resolvePathValue(userConfig.plan, configDir) ?? null,
175
+ goal: flags.goal ?? userConfig.goal ?? DEFAULT_GOAL,
176
+ configPath
177
+ };
178
+ }
179
+
180
+ //#endregion
181
+ export { getRepoArtifactsDir as a, createRepoSlug as c, getRepoRoot as d, runCommandSync as f, getRecommendedPlanDir as i, getCurrentBranch as l, resolveRunConfig as n, getRepoLatestRunPointerPath as o, getLatestRunPointerPath as r, getTownHomeDir as s, parseCliFlags as t, getRepoIdentity as u };
182
+ //# sourceMappingURL=config-D67TLZft.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-D67TLZft.mjs","names":[],"sources":["../../core/src/shell.ts","../../core/src/repo.ts","../src/paths.ts","../src/config.ts"],"sourcesContent":["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 { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nexport function getTownHomeDir(): string {\n\treturn join(homedir(), \".pi-town\")\n}\n\nexport function getUserConfigPath(): string {\n\treturn join(getTownHomeDir(), \"config.json\")\n}\n\nexport function getPlansRootDir(): string {\n\treturn join(getTownHomeDir(), \"plans\")\n}\n\nexport function getReposRootDir(): string {\n\treturn join(getTownHomeDir(), \"repos\")\n}\n\nexport function getRepoArtifactsDir(repoSlug: string): string {\n\treturn join(getReposRootDir(), repoSlug)\n}\n\nexport function getLatestRunPointerPath(): string {\n\treturn join(getTownHomeDir(), \"latest-run.json\")\n}\n\nexport function getRepoLatestRunPointerPath(repoSlug: string): string {\n\treturn join(getRepoArtifactsDir(repoSlug), \"latest-run.json\")\n}\n\nexport function getRecommendedPlanDir(repoSlug: string): string {\n\treturn join(getPlansRootDir(), repoSlug)\n}\n","import { existsSync, readFileSync } from \"node:fs\"\nimport { dirname, isAbsolute, resolve } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport { getUserConfigPath } from \"./paths.js\"\n\nconst DEFAULT_GOAL = \"continue from current scaffold state\"\n\nexport interface CliFlags {\n\trepo?: string\n\tplan?: string\n\tgoal?: string\n\thelp: boolean\n}\n\ninterface UserConfig {\n\trepo?: string\n\tplan?: string\n\tgoal?: string\n}\n\nexport interface ResolvedRunConfig {\n\trepo: string\n\tplan: string | null\n\tgoal: string\n\tconfigPath: string\n}\n\nfunction expandHome(value: string): string {\n\tif (value === \"~\") return homedir()\n\tif (value.startsWith(\"~/\")) return resolve(homedir(), value.slice(2))\n\treturn value\n}\n\nfunction resolvePathValue(value: string | undefined, baseDir: string): string | undefined {\n\tif (!value) return undefined\n\tconst expanded = expandHome(value)\n\treturn isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded)\n}\n\nexport function parseCliFlags(argv: string[]): CliFlags {\n\tconst flags: CliFlags = { help: false }\n\n\tfor (let index = 0; index < argv.length; index += 1) {\n\t\tconst arg = argv[index]\n\n\t\tif (arg === \"--help\" || arg === \"-h\") {\n\t\t\tflags.help = true\n\t\t\tcontinue\n\t\t}\n\n\t\tif (arg.startsWith(\"--repo=\")) {\n\t\t\tflags.repo = arg.slice(\"--repo=\".length)\n\t\t\tcontinue\n\t\t}\n\n\t\tif (arg === \"--repo\") {\n\t\t\tconst value = argv[index + 1]\n\t\t\tif (!value) throw new Error(\"Missing value for --repo\")\n\t\t\tflags.repo = value\n\t\t\tindex += 1\n\t\t\tcontinue\n\t\t}\n\n\t\tif (arg.startsWith(\"--plan=\")) {\n\t\t\tflags.plan = arg.slice(\"--plan=\".length)\n\t\t\tcontinue\n\t\t}\n\n\t\tif (arg === \"--plan\") {\n\t\t\tconst value = argv[index + 1]\n\t\t\tif (!value) throw new Error(\"Missing value for --plan\")\n\t\t\tflags.plan = value\n\t\t\tindex += 1\n\t\t\tcontinue\n\t\t}\n\n\t\tif (arg.startsWith(\"--goal=\")) {\n\t\t\tflags.goal = arg.slice(\"--goal=\".length)\n\t\t\tcontinue\n\t\t}\n\n\t\tif (arg === \"--goal\") {\n\t\t\tconst value = argv[index + 1]\n\t\t\tif (!value) throw new Error(\"Missing value for --goal\")\n\t\t\tflags.goal = value\n\t\t\tindex += 1\n\t\t\tcontinue\n\t\t}\n\n\t\tthrow new Error(`Unknown argument: ${arg}`)\n\t}\n\n\treturn flags\n}\n\nexport function loadUserConfig(): UserConfig {\n\tconst configPath = getUserConfigPath()\n\tif (!existsSync(configPath)) return {}\n\treturn JSON.parse(readFileSync(configPath, \"utf-8\")) as UserConfig\n}\n\nexport function resolveRunConfig(argv: string[]): ResolvedRunConfig {\n\tconst flags = parseCliFlags(argv)\n\tconst configPath = getUserConfigPath()\n\tconst userConfig = loadUserConfig()\n\tconst configDir = dirname(configPath)\n\n\tconst repo =\n\t\tresolvePathValue(flags.repo, process.cwd()) ??\n\t\tresolvePathValue(userConfig.repo, configDir) ??\n\t\tresolve(process.cwd())\n\tconst plan = resolvePathValue(flags.plan, process.cwd()) ?? resolvePathValue(userConfig.plan, configDir) ?? null\n\tconst goal = flags.goal ?? userConfig.goal ?? DEFAULT_GOAL\n\n\treturn {\n\t\trepo,\n\t\tplan,\n\t\tgoal,\n\t\tconfigPath,\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,324 @@
1
+ import { c as createRepoSlug, d as getRepoRoot, f as runCommandSync, l as getCurrentBranch, u as getRepoIdentity } from "./config-D67TLZft.mjs";
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir, hostname } from "node:os";
5
+
6
+ //#region ../core/src/events.ts
7
+ function appendJsonl(filePath, value) {
8
+ mkdirSync(dirname(filePath), { recursive: true });
9
+ writeFileSync(filePath, `${JSON.stringify(value)}\n`, {
10
+ encoding: "utf-8",
11
+ flag: "a"
12
+ });
13
+ }
14
+
15
+ //#endregion
16
+ //#region ../core/src/lease.ts
17
+ function sanitize(value) {
18
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
19
+ }
20
+ function processAlive(pid) {
21
+ if (!Number.isFinite(pid) || pid <= 0) return false;
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ function acquireRepoLease(runId, repoId, branch) {
30
+ const locksDir = join(homedir(), ".pi-town", "locks");
31
+ mkdirSync(locksDir, { recursive: true });
32
+ const leasePath = join(locksDir, `pi-town-${sanitize(repoId)}-${sanitize(branch)}.json`);
33
+ const nextData = {
34
+ runId,
35
+ repoId,
36
+ branch,
37
+ pid: process.pid,
38
+ hostname: hostname(),
39
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
40
+ };
41
+ try {
42
+ const current = JSON.parse(readFileSync(leasePath, "utf-8"));
43
+ if (processAlive(current.pid)) throw new Error(`Pi Town lease already held by pid ${current.pid} on ${current.hostname} for run ${current.runId}.`);
44
+ rmSync(leasePath, { force: true });
45
+ } catch (error) {
46
+ if (error.code !== "ENOENT") {
47
+ if (error instanceof Error && error.message.startsWith("Pi Town lease already held")) throw error;
48
+ }
49
+ }
50
+ writeFileSync(leasePath, `${JSON.stringify(nextData, null, 2)}\n`, "utf-8");
51
+ return {
52
+ path: leasePath,
53
+ release: () => {
54
+ try {
55
+ if (JSON.parse(readFileSync(leasePath, "utf-8")).runId === runId) rmSync(leasePath, { force: true });
56
+ } catch {}
57
+ }
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ //#region ../core/src/metrics.ts
63
+ function round(value) {
64
+ return Math.round(value * 1e3) / 1e3;
65
+ }
66
+ function diffHours(start, end) {
67
+ return (Date.parse(end) - Date.parse(start)) / 36e5;
68
+ }
69
+ function average(values) {
70
+ if (values.length === 0) return null;
71
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
72
+ }
73
+ function computeInterruptRate(interrupts, taskAttempts) {
74
+ if (taskAttempts.length === 0) return 0;
75
+ return round(interrupts.length / taskAttempts.length);
76
+ }
77
+ function computeAutonomousCompletionRate(taskAttempts) {
78
+ const completed = taskAttempts.filter((task) => task.status === "completed");
79
+ if (completed.length === 0) return 0;
80
+ return round(completed.filter((task) => !task.interrupted).length / completed.length);
81
+ }
82
+ function computeContextCoverageScore(interrupts) {
83
+ const observed = new Set(interrupts.map((interrupt) => interrupt.category));
84
+ if (observed.size === 0) return 0;
85
+ return round(new Set(interrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category)).size / observed.size);
86
+ }
87
+ function computeMeanTimeToCorrect(interrupts) {
88
+ const value = average(interrupts.filter((interrupt) => interrupt.resolvedAt).map((interrupt) => diffHours(interrupt.createdAt, interrupt.resolvedAt)));
89
+ return value === null ? null : round(value);
90
+ }
91
+ function computeFeedbackToDemoCycleTime(feedbackCycles) {
92
+ const value = average(feedbackCycles.map((cycle) => diffHours(cycle.feedbackAt, cycle.demoReadyAt)));
93
+ return value === null ? null : round(value);
94
+ }
95
+ function computeMetrics(input) {
96
+ const observedCategories = new Set(input.interrupts.map((interrupt) => interrupt.category));
97
+ const coveredCategories = new Set(input.interrupts.filter((interrupt) => interrupt.fixType).map((interrupt) => interrupt.category));
98
+ const completedTasks = input.taskAttempts.filter((task) => task.status === "completed").length;
99
+ return {
100
+ interruptRate: computeInterruptRate(input.interrupts, input.taskAttempts),
101
+ autonomousCompletionRate: computeAutonomousCompletionRate(input.taskAttempts),
102
+ contextCoverageScore: computeContextCoverageScore(input.interrupts),
103
+ meanTimeToCorrectHours: computeMeanTimeToCorrect(input.interrupts),
104
+ feedbackToDemoCycleTimeHours: computeFeedbackToDemoCycleTime(input.feedbackCycles ?? []),
105
+ totals: {
106
+ taskAttempts: input.taskAttempts.length,
107
+ completedTasks,
108
+ interrupts: input.interrupts.length,
109
+ observedInterruptCategories: observedCategories.size,
110
+ coveredInterruptCategories: coveredCategories.size
111
+ }
112
+ };
113
+ }
114
+
115
+ //#endregion
116
+ //#region ../core/src/controller.ts
117
+ function createRunId() {
118
+ return `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
119
+ }
120
+ function writeJson(path, value) {
121
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
122
+ }
123
+ function writeText(path, value) {
124
+ writeFileSync(path, value, "utf-8");
125
+ }
126
+ function createPiPrompt(input) {
127
+ const goal = input.goal ?? "continue from current scaffold state";
128
+ if (input.planPath) return [
129
+ "Read the private plans in:",
130
+ `- ${input.planPath}`,
131
+ "",
132
+ "and the current code in:",
133
+ `- ${input.repoRoot}`,
134
+ "",
135
+ `Goal: ${goal}`,
136
+ "Continue from the current scaffold state.",
137
+ "Keep any persisted run artifacts high-signal and avoid copying private plan contents into them."
138
+ ].join("\n");
139
+ return [
140
+ `Work in the repository at: ${input.repoRoot}`,
141
+ `Goal: ${goal}`,
142
+ "No private plan path is configured for this run.",
143
+ 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.",
144
+ "Continue from the current scaffold state."
145
+ ].join("\n");
146
+ }
147
+ function createManifest(input) {
148
+ return {
149
+ runId: input.runId,
150
+ repoId: input.repoId,
151
+ repoSlug: input.repoSlug,
152
+ repoRoot: input.repoRoot,
153
+ branch: input.branch,
154
+ goal: input.goal,
155
+ planPath: input.planPath,
156
+ recommendedPlanDir: input.recommendedPlanDir,
157
+ mode: input.mode,
158
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
159
+ endedAt: null,
160
+ stopReason: null,
161
+ leasePath: input.leasePath,
162
+ piExitCode: null,
163
+ completedTaskCount: 0,
164
+ blockedTaskCount: 0,
165
+ skippedTaskCount: 0,
166
+ totalCostUsd: 0
167
+ };
168
+ }
169
+ function createSummary(input) {
170
+ const success = input.exitCode === 0;
171
+ const recommendation = input.recommendedPlanDir === null ? "" : ` No plan path was configured. Recommended private plans location: ${input.recommendedPlanDir}.`;
172
+ return {
173
+ runId: input.runId,
174
+ mode: input.mode,
175
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
176
+ success,
177
+ message: success ? `Pi invocation completed.${recommendation}` : `Pi invocation failed.${recommendation}`,
178
+ piExitCode: input.exitCode,
179
+ recommendedPlanDir: input.recommendedPlanDir
180
+ };
181
+ }
182
+ function runController(options) {
183
+ const cwd = options.cwd ?? process.cwd();
184
+ const artifactsDir = options.artifactsDir;
185
+ const repoRoot = getRepoRoot(cwd);
186
+ const repoId = getRepoIdentity(repoRoot);
187
+ const repoSlug = createRepoSlug(repoId, repoRoot);
188
+ const branch = options.branch ?? getCurrentBranch(repoRoot) ?? "workspace";
189
+ const goal = options.goal ?? null;
190
+ const planPath = options.planPath ?? null;
191
+ const recommendedPlanDir = planPath ? null : options.recommendedPlanDir ?? null;
192
+ const mode = options.mode ?? "single-pi";
193
+ const piCommand = options.piCommand ?? "pi";
194
+ const runId = createRunId();
195
+ const runDir = join(artifactsDir, "runs", runId);
196
+ const latestDir = join(artifactsDir, "latest");
197
+ const stdoutPath = join(runDir, "stdout.txt");
198
+ const stderrPath = join(runDir, "stderr.txt");
199
+ const prompt = createPiPrompt({
200
+ repoRoot,
201
+ planPath,
202
+ goal,
203
+ recommendedPlanDir
204
+ });
205
+ mkdirSync(runDir, { recursive: true });
206
+ mkdirSync(latestDir, { recursive: true });
207
+ writeText(join(runDir, "questions.jsonl"), "");
208
+ writeText(join(runDir, "interventions.jsonl"), "");
209
+ writeJson(join(runDir, "agent-state.json"), {
210
+ status: "starting",
211
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
212
+ });
213
+ const lease = acquireRepoLease(runId, repoId, branch);
214
+ try {
215
+ const manifest = createManifest({
216
+ runId,
217
+ repoId,
218
+ repoSlug,
219
+ repoRoot,
220
+ branch,
221
+ goal,
222
+ planPath,
223
+ recommendedPlanDir,
224
+ mode,
225
+ leasePath: lease.path
226
+ });
227
+ appendJsonl(join(runDir, "events.jsonl"), {
228
+ type: "run_started",
229
+ runId,
230
+ repoId,
231
+ repoSlug,
232
+ branch,
233
+ createdAt: manifest.startedAt
234
+ });
235
+ const piStartedAt = (/* @__PURE__ */ new Date()).toISOString();
236
+ appendJsonl(join(runDir, "events.jsonl"), {
237
+ type: "pi_invocation_started",
238
+ runId,
239
+ command: piCommand,
240
+ createdAt: piStartedAt
241
+ });
242
+ const piResult = runCommandSync(piCommand, [
243
+ "--no-session",
244
+ "-p",
245
+ prompt
246
+ ], {
247
+ cwd: repoRoot,
248
+ env: process.env
249
+ });
250
+ const piEndedAt = (/* @__PURE__ */ new Date()).toISOString();
251
+ writeText(stdoutPath, piResult.stdout);
252
+ writeText(stderrPath, piResult.stderr);
253
+ const piInvocation = {
254
+ command: piCommand,
255
+ cwd: repoRoot,
256
+ repoRoot,
257
+ planPath,
258
+ goal,
259
+ startedAt: piStartedAt,
260
+ endedAt: piEndedAt,
261
+ exitCode: piResult.exitCode,
262
+ stdoutPath,
263
+ stderrPath,
264
+ promptSummary: planPath ? "Read private plan path and continue from current scaffold state." : "Continue from current scaffold state without a configured private plan path."
265
+ };
266
+ writeJson(join(runDir, "pi-invocation.json"), piInvocation);
267
+ appendJsonl(join(runDir, "events.jsonl"), {
268
+ type: "pi_invocation_finished",
269
+ runId,
270
+ command: piCommand,
271
+ exitCode: piInvocation.exitCode,
272
+ createdAt: piEndedAt
273
+ });
274
+ const metrics = computeMetrics({
275
+ taskAttempts: [],
276
+ interrupts: []
277
+ });
278
+ const summary = createSummary({
279
+ runId,
280
+ mode,
281
+ exitCode: piInvocation.exitCode,
282
+ recommendedPlanDir
283
+ });
284
+ const finalManifest = {
285
+ ...manifest,
286
+ endedAt: piEndedAt,
287
+ stopReason: piInvocation.exitCode === 0 ? "pi invocation completed" : `pi invocation exited with code ${piInvocation.exitCode}`,
288
+ piExitCode: piInvocation.exitCode
289
+ };
290
+ writeJson(join(runDir, "manifest.json"), finalManifest);
291
+ writeJson(join(runDir, "metrics.json"), metrics);
292
+ writeJson(join(runDir, "run-summary.json"), summary);
293
+ writeJson(join(runDir, "agent-state.json"), {
294
+ status: summary.success ? "completed" : "failed",
295
+ updatedAt: piEndedAt,
296
+ exitCode: piInvocation.exitCode
297
+ });
298
+ writeJson(join(latestDir, "manifest.json"), finalManifest);
299
+ writeJson(join(latestDir, "metrics.json"), metrics);
300
+ writeJson(join(latestDir, "run-summary.json"), summary);
301
+ appendJsonl(join(runDir, "events.jsonl"), {
302
+ type: "run_finished",
303
+ runId,
304
+ createdAt: finalManifest.endedAt,
305
+ stopReason: finalManifest.stopReason,
306
+ metrics
307
+ });
308
+ return {
309
+ runId,
310
+ runDir,
311
+ latestDir,
312
+ manifest: finalManifest,
313
+ metrics,
314
+ summary,
315
+ piInvocation
316
+ };
317
+ } finally {
318
+ lease.release();
319
+ }
320
+ }
321
+
322
+ //#endregion
323
+ export { runController as t };
324
+ //# sourceMappingURL=controller-DJPeTtIy.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller-DJPeTtIy.mjs","names":[],"sources":["../../core/src/events.ts","../../core/src/lease.ts","../../core/src/metrics.ts","../../core/src/controller.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 { 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"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ //#region src/index.d.ts
2
+ declare function printHelp(): void;
3
+ declare function runCli(argv?: string[]): void;
4
+ //#endregion
5
+ export { printHelp, runCli };
6
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { runTown } from "./run.mjs";
3
+ import { showTownStatus } from "./status.mjs";
4
+ import { watchTown } from "./watch.mjs";
5
+
6
+ //#region src/index.ts
7
+ function printHelp() {
8
+ console.log([
9
+ "pitown",
10
+ "",
11
+ "Usage:",
12
+ " pitown run [--repo <path>] [--plan <path>] [--goal <text>]",
13
+ " pitown status [--repo <path>]",
14
+ " pitown watch [--repo <path>]",
15
+ " pitown help",
16
+ " pitown --help"
17
+ ].join("\n"));
18
+ }
19
+ function runCli(argv = process.argv.slice(2)) {
20
+ const [command, ...args] = argv;
21
+ switch (command) {
22
+ case void 0:
23
+ case "help":
24
+ case "--help":
25
+ case "-h":
26
+ printHelp();
27
+ break;
28
+ case "run": {
29
+ const result = runTown(args);
30
+ if (result.piInvocation.exitCode !== 0) process.exitCode = result.piInvocation.exitCode;
31
+ break;
32
+ }
33
+ case "status":
34
+ showTownStatus(args);
35
+ break;
36
+ case "watch":
37
+ watchTown(args);
38
+ break;
39
+ default:
40
+ console.log(`Unknown command: ${command}`);
41
+ printHelp();
42
+ process.exitCode = 1;
43
+ break;
44
+ }
45
+ }
46
+ if (import.meta.url === `file://${process.argv[1]}`) runCli();
47
+
48
+ //#endregion
49
+ export { printHelp, runCli };
50
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { runTown } from \"./run.js\"\nimport { showTownStatus } from \"./status.js\"\nimport { watchTown } from \"./watch.js\"\n\nexport function printHelp() {\n\tconsole.log(\n\t\t[\n\t\t\t\"pitown\",\n\t\t\t\"\",\n\t\t\t\"Usage:\",\n\t\t\t\" pitown run [--repo <path>] [--plan <path>] [--goal <text>]\",\n\t\t\t\" pitown status [--repo <path>]\",\n\t\t\t\" pitown watch [--repo <path>]\",\n\t\t\t\" pitown help\",\n\t\t\t\" pitown --help\",\n\t\t].join(\"\\n\"),\n\t)\n}\n\nexport function runCli(argv = process.argv.slice(2)) {\n\tconst [command, ...args] = argv\n\n\tswitch (command) {\n\t\tcase undefined:\n\t\tcase \"help\":\n\t\tcase \"--help\":\n\t\tcase \"-h\":\n\t\t\tprintHelp()\n\t\t\tbreak\n\t\tcase \"run\": {\n\t\t\tconst result = runTown(args)\n\t\t\tif (result.piInvocation.exitCode !== 0) process.exitCode = result.piInvocation.exitCode\n\t\t\tbreak\n\t\t}\n\t\tcase \"status\":\n\t\t\tshowTownStatus(args)\n\t\t\tbreak\n\t\tcase \"watch\":\n\t\t\twatchTown(args)\n\t\t\tbreak\n\t\tdefault:\n\t\t\tconsole.log(`Unknown command: ${command}`)\n\t\t\tprintHelp()\n\t\t\tprocess.exitCode = 1\n\t\t\tbreak\n\t}\n}\n\nif (import.meta.url === `file://${process.argv[1]}`) {\n\trunCli()\n}\n"],"mappings":";;;;;;AAMA,SAAgB,YAAY;AAC3B,SAAQ,IACP;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,CAAC,KAAK,KAAK,CACZ;;AAGF,SAAgB,OAAO,OAAO,QAAQ,KAAK,MAAM,EAAE,EAAE;CACpD,MAAM,CAAC,SAAS,GAAG,QAAQ;AAE3B,SAAQ,SAAR;EACC,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;AACJ,cAAW;AACX;EACD,KAAK,OAAO;GACX,MAAM,SAAS,QAAQ,KAAK;AAC5B,OAAI,OAAO,aAAa,aAAa,EAAG,SAAQ,WAAW,OAAO,aAAa;AAC/E;;EAED,KAAK;AACJ,kBAAe,KAAK;AACpB;EACD,KAAK;AACJ,aAAU,KAAK;AACf;EACD;AACC,WAAQ,IAAI,oBAAoB,UAAU;AAC1C,cAAW;AACX,WAAQ,WAAW;AACnB;;;AAIH,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAC9C,SAAQ"}
package/dist/run.d.mts ADDED
@@ -0,0 +1,73 @@
1
+ //#region ../core/src/types.d.ts
2
+ type RunMode = "single-pi";
3
+ interface MetricsSnapshot {
4
+ interruptRate: number;
5
+ autonomousCompletionRate: number;
6
+ contextCoverageScore: number;
7
+ meanTimeToCorrectHours: number | null;
8
+ feedbackToDemoCycleTimeHours: number | null;
9
+ totals: {
10
+ taskAttempts: number;
11
+ completedTasks: number;
12
+ interrupts: number;
13
+ observedInterruptCategories: number;
14
+ coveredInterruptCategories: number;
15
+ };
16
+ }
17
+ interface RunManifest {
18
+ runId: string;
19
+ repoId: string;
20
+ repoSlug: string;
21
+ repoRoot: string;
22
+ branch: string;
23
+ goal: string | null;
24
+ planPath: string | null;
25
+ recommendedPlanDir: string | null;
26
+ mode: RunMode;
27
+ startedAt: string;
28
+ endedAt: string | null;
29
+ stopReason: string | null;
30
+ leasePath: string;
31
+ piExitCode: number | null;
32
+ completedTaskCount: number;
33
+ blockedTaskCount: number;
34
+ skippedTaskCount: number;
35
+ totalCostUsd: number;
36
+ }
37
+ interface PiInvocationRecord {
38
+ command: string;
39
+ cwd: string;
40
+ repoRoot: string;
41
+ planPath: string | null;
42
+ goal: string | null;
43
+ startedAt: string;
44
+ endedAt: string;
45
+ exitCode: number;
46
+ stdoutPath: string;
47
+ stderrPath: string;
48
+ promptSummary: string;
49
+ }
50
+ interface RunSummary {
51
+ runId: string;
52
+ mode: RunMode;
53
+ createdAt: string;
54
+ success: boolean;
55
+ message: string;
56
+ piExitCode: number;
57
+ recommendedPlanDir: string | null;
58
+ }
59
+ interface ControllerRunResult {
60
+ runId: string;
61
+ runDir: string;
62
+ latestDir: string;
63
+ manifest: RunManifest;
64
+ metrics: MetricsSnapshot;
65
+ summary: RunSummary;
66
+ piInvocation: PiInvocationRecord;
67
+ }
68
+ //#endregion
69
+ //#region src/run.d.ts
70
+ declare function runTown(argv?: string[]): ControllerRunResult;
71
+ //#endregion
72
+ export { runTown };
73
+ //# sourceMappingURL=run.d.mts.map
package/dist/run.mjs ADDED
@@ -0,0 +1,65 @@
1
+ import { t as runController } from "./controller-DJPeTtIy.mjs";
2
+ import { a as getRepoArtifactsDir, c as createRepoSlug, d as getRepoRoot, i as getRecommendedPlanDir, n as resolveRunConfig, o as getRepoLatestRunPointerPath, r as getLatestRunPointerPath, s as getTownHomeDir, u as getRepoIdentity } from "./config-D67TLZft.mjs";
3
+ import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+
6
+ //#region src/run.ts
7
+ function writeJson(path, value) {
8
+ mkdirSync(dirname(path), { recursive: true });
9
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
10
+ }
11
+ function assertDirectory(path, label) {
12
+ if (!existsSync(path)) throw new Error(`${label} does not exist: ${path}`);
13
+ if (!statSync(path).isDirectory()) throw new Error(`${label} is not a directory: ${path}`);
14
+ }
15
+ function createLatestRunPointer(result, repoSlug, repoRoot) {
16
+ return {
17
+ repoSlug,
18
+ repoRoot,
19
+ runId: result.runId,
20
+ runDir: result.runDir,
21
+ latestDir: result.latestDir,
22
+ manifestPath: join(result.latestDir, "manifest.json"),
23
+ metricsPath: join(result.latestDir, "metrics.json"),
24
+ summaryPath: join(result.latestDir, "run-summary.json"),
25
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
26
+ };
27
+ }
28
+ function runTown(argv = process.argv.slice(2)) {
29
+ const config = resolveRunConfig(argv);
30
+ assertDirectory(config.repo, "Target repo");
31
+ if (config.plan) assertDirectory(config.plan, "Plan path");
32
+ mkdirSync(getTownHomeDir(), { recursive: true });
33
+ const repoRoot = getRepoRoot(config.repo);
34
+ const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
35
+ const recommendedPlanDir = config.plan ? null : getRecommendedPlanDir(repoSlug);
36
+ const result = runController({
37
+ artifactsDir: getRepoArtifactsDir(repoSlug),
38
+ cwd: repoRoot,
39
+ goal: config.goal,
40
+ mode: "single-pi",
41
+ planPath: config.plan,
42
+ recommendedPlanDir
43
+ });
44
+ const latestPointer = createLatestRunPointer(result, repoSlug, repoRoot);
45
+ writeJson(getLatestRunPointerPath(), latestPointer);
46
+ writeJson(getRepoLatestRunPointerPath(repoSlug), latestPointer);
47
+ console.log("[pitown] run written");
48
+ console.log(`- run id: ${result.runId}`);
49
+ console.log(`- repo root: ${result.manifest.repoRoot}`);
50
+ console.log(`- branch: ${result.manifest.branch}`);
51
+ console.log(`- artifacts: ${result.runDir}`);
52
+ console.log(`- latest metrics: ${latestPointer.metricsPath}`);
53
+ console.log(`- pi exit code: ${result.piInvocation.exitCode}`);
54
+ if (result.manifest.planPath) console.log(`- plan path: ${result.manifest.planPath}`);
55
+ if (result.summary.recommendedPlanDir) console.log(`- recommended plans: ${result.summary.recommendedPlanDir}`);
56
+ return result;
57
+ }
58
+ if (import.meta.url === `file://${process.argv[1]}`) {
59
+ const result = runTown();
60
+ if (result.piInvocation.exitCode !== 0) process.exitCode = result.piInvocation.exitCode;
61
+ }
62
+
63
+ //#endregion
64
+ export { runTown };
65
+ //# sourceMappingURL=run.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.mjs","names":[],"sources":["../src/run.ts"],"sourcesContent":["import { existsSync, mkdirSync, statSync, writeFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport {\n\tcreateRepoSlug,\n\tgetRepoIdentity,\n\tgetRepoRoot,\n\trunController,\n\ttype ControllerRunResult,\n} from \"../../core/src/index.js\"\nimport { resolveRunConfig } from \"./config.js\"\nimport {\n\tgetLatestRunPointerPath,\n\tgetRecommendedPlanDir,\n\tgetRepoArtifactsDir,\n\tgetRepoLatestRunPointerPath,\n\tgetTownHomeDir,\n} from \"./paths.js\"\n\ninterface LatestRunPointer {\n\trepoSlug: string\n\trepoRoot: string\n\trunId: string\n\trunDir: string\n\tlatestDir: string\n\tmanifestPath: string\n\tmetricsPath: string\n\tsummaryPath: string\n\tupdatedAt: string\n}\n\nfunction writeJson(path: string, value: unknown) {\n\tmkdirSync(dirname(path), { recursive: true })\n\twriteFileSync(path, `${JSON.stringify(value, null, 2)}\\n`, \"utf-8\")\n}\n\nfunction assertDirectory(path: string, label: string) {\n\tif (!existsSync(path)) throw new Error(`${label} does not exist: ${path}`)\n\tif (!statSync(path).isDirectory()) throw new Error(`${label} is not a directory: ${path}`)\n}\n\nfunction createLatestRunPointer(result: ControllerRunResult, repoSlug: string, repoRoot: string): LatestRunPointer {\n\treturn {\n\t\trepoSlug,\n\t\trepoRoot,\n\t\trunId: result.runId,\n\t\trunDir: result.runDir,\n\t\tlatestDir: result.latestDir,\n\t\tmanifestPath: join(result.latestDir, \"manifest.json\"),\n\t\tmetricsPath: join(result.latestDir, \"metrics.json\"),\n\t\tsummaryPath: join(result.latestDir, \"run-summary.json\"),\n\t\tupdatedAt: new Date().toISOString(),\n\t}\n}\n\nexport function runTown(argv = process.argv.slice(2)): ControllerRunResult {\n\tconst config = resolveRunConfig(argv)\n\tassertDirectory(config.repo, \"Target repo\")\n\tif (config.plan) assertDirectory(config.plan, \"Plan path\")\n\n\tconst townHome = getTownHomeDir()\n\tmkdirSync(townHome, { recursive: true })\n\n\tconst repoRoot = getRepoRoot(config.repo)\n\tconst repoId = getRepoIdentity(repoRoot)\n\tconst repoSlug = createRepoSlug(repoId, repoRoot)\n\tconst recommendedPlanDir = config.plan ? null : getRecommendedPlanDir(repoSlug)\n\tconst artifactsDir = getRepoArtifactsDir(repoSlug)\n\n\tconst result = runController({\n\t\tartifactsDir,\n\t\tcwd: repoRoot,\n\t\tgoal: config.goal,\n\t\tmode: \"single-pi\",\n\t\tplanPath: config.plan,\n\t\trecommendedPlanDir,\n\t})\n\n\tconst latestPointer = createLatestRunPointer(result, repoSlug, repoRoot)\n\twriteJson(getLatestRunPointerPath(), latestPointer)\n\twriteJson(getRepoLatestRunPointerPath(repoSlug), latestPointer)\n\n\tconsole.log(\"[pitown] run written\")\n\tconsole.log(`- run id: ${result.runId}`)\n\tconsole.log(`- repo root: ${result.manifest.repoRoot}`)\n\tconsole.log(`- branch: ${result.manifest.branch}`)\n\tconsole.log(`- artifacts: ${result.runDir}`)\n\tconsole.log(`- latest metrics: ${latestPointer.metricsPath}`)\n\tconsole.log(`- pi exit code: ${result.piInvocation.exitCode}`)\n\tif (result.manifest.planPath) console.log(`- plan path: ${result.manifest.planPath}`)\n\tif (result.summary.recommendedPlanDir) console.log(`- recommended plans: ${result.summary.recommendedPlanDir}`)\n\n\treturn result\n}\n\nif (import.meta.url === `file://${process.argv[1]}`) {\n\tconst result = runTown()\n\tif (result.piInvocation.exitCode !== 0) process.exitCode = result.piInvocation.exitCode\n}\n"],"mappings":";;;;;;AA8BA,SAAS,UAAU,MAAc,OAAgB;AAChD,WAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC7C,eAAc,MAAM,GAAG,KAAK,UAAU,OAAO,MAAM,EAAE,CAAC,KAAK,QAAQ;;AAGpE,SAAS,gBAAgB,MAAc,OAAe;AACrD,KAAI,CAAC,WAAW,KAAK,CAAE,OAAM,IAAI,MAAM,GAAG,MAAM,mBAAmB,OAAO;AAC1E,KAAI,CAAC,SAAS,KAAK,CAAC,aAAa,CAAE,OAAM,IAAI,MAAM,GAAG,MAAM,uBAAuB,OAAO;;AAG3F,SAAS,uBAAuB,QAA6B,UAAkB,UAAoC;AAClH,QAAO;EACN;EACA;EACA,OAAO,OAAO;EACd,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,cAAc,KAAK,OAAO,WAAW,gBAAgB;EACrD,aAAa,KAAK,OAAO,WAAW,eAAe;EACnD,aAAa,KAAK,OAAO,WAAW,mBAAmB;EACvD,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC;;AAGF,SAAgB,QAAQ,OAAO,QAAQ,KAAK,MAAM,EAAE,EAAuB;CAC1E,MAAM,SAAS,iBAAiB,KAAK;AACrC,iBAAgB,OAAO,MAAM,cAAc;AAC3C,KAAI,OAAO,KAAM,iBAAgB,OAAO,MAAM,YAAY;AAG1D,WADiB,gBAAgB,EACb,EAAE,WAAW,MAAM,CAAC;CAExC,MAAM,WAAW,YAAY,OAAO,KAAK;CAEzC,MAAM,WAAW,eADF,gBAAgB,SAAS,EACA,SAAS;CACjD,MAAM,qBAAqB,OAAO,OAAO,OAAO,sBAAsB,SAAS;CAG/E,MAAM,SAAS,cAAc;EAC5B,cAHoB,oBAAoB,SAAS;EAIjD,KAAK;EACL,MAAM,OAAO;EACb,MAAM;EACN,UAAU,OAAO;EACjB;EACA,CAAC;CAEF,MAAM,gBAAgB,uBAAuB,QAAQ,UAAU,SAAS;AACxE,WAAU,yBAAyB,EAAE,cAAc;AACnD,WAAU,4BAA4B,SAAS,EAAE,cAAc;AAE/D,SAAQ,IAAI,uBAAuB;AACnC,SAAQ,IAAI,aAAa,OAAO,QAAQ;AACxC,SAAQ,IAAI,gBAAgB,OAAO,SAAS,WAAW;AACvD,SAAQ,IAAI,aAAa,OAAO,SAAS,SAAS;AAClD,SAAQ,IAAI,gBAAgB,OAAO,SAAS;AAC5C,SAAQ,IAAI,qBAAqB,cAAc,cAAc;AAC7D,SAAQ,IAAI,mBAAmB,OAAO,aAAa,WAAW;AAC9D,KAAI,OAAO,SAAS,SAAU,SAAQ,IAAI,gBAAgB,OAAO,SAAS,WAAW;AACrF,KAAI,OAAO,QAAQ,mBAAoB,SAAQ,IAAI,wBAAwB,OAAO,QAAQ,qBAAqB;AAE/G,QAAO;;AAGR,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,MAAM;CACpD,MAAM,SAAS,SAAS;AACxB,KAAI,OAAO,aAAa,aAAa,EAAG,SAAQ,WAAW,OAAO,aAAa"}
@@ -0,0 +1,17 @@
1
+ //#region src/status.d.ts
2
+ interface LatestRunPointer {
3
+ repoSlug: string;
4
+ repoRoot: string;
5
+ runId?: string;
6
+ runDir?: string;
7
+ latestDir: string;
8
+ manifestPath: string;
9
+ metricsPath: string;
10
+ summaryPath: string;
11
+ updatedAt?: string;
12
+ }
13
+ declare function resolveLatestRunPointer(argv?: string[]): LatestRunPointer | null;
14
+ declare function showTownStatus(argv?: string[]): void;
15
+ //#endregion
16
+ export { resolveLatestRunPointer, showTownStatus };
17
+ //# sourceMappingURL=status.d.mts.map
@@ -0,0 +1,70 @@
1
+ import { a as getRepoArtifactsDir, c as createRepoSlug, d as getRepoRoot, o as getRepoLatestRunPointerPath, r as getLatestRunPointerPath, s as getTownHomeDir, t as parseCliFlags, u as getRepoIdentity } from "./config-D67TLZft.mjs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ //#region src/status.ts
6
+ function readJson(path) {
7
+ return JSON.parse(readFileSync(path, "utf-8"));
8
+ }
9
+ function createFallbackPointer(repoRoot, repoSlug) {
10
+ const latestDir = join(getRepoArtifactsDir(repoSlug), "latest");
11
+ return {
12
+ repoSlug,
13
+ repoRoot,
14
+ latestDir,
15
+ manifestPath: join(latestDir, "manifest.json"),
16
+ metricsPath: join(latestDir, "metrics.json"),
17
+ summaryPath: join(latestDir, "run-summary.json")
18
+ };
19
+ }
20
+ function resolveLatestRunPointer(argv = process.argv.slice(2)) {
21
+ const flags = parseCliFlags(argv);
22
+ if (flags.repo) {
23
+ const repoRoot = getRepoRoot(flags.repo);
24
+ const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
25
+ const repoPointerPath = getRepoLatestRunPointerPath(repoSlug);
26
+ if (existsSync(repoPointerPath)) return readJson(repoPointerPath);
27
+ return createFallbackPointer(repoRoot, repoSlug);
28
+ }
29
+ const latestPointerPath = getLatestRunPointerPath();
30
+ if (!existsSync(latestPointerPath)) return null;
31
+ return readJson(latestPointerPath);
32
+ }
33
+ function showTownStatus(argv = process.argv.slice(2)) {
34
+ const latest = resolveLatestRunPointer(argv);
35
+ console.log("[pitown] status");
36
+ console.log(`- town home: ${getTownHomeDir()}`);
37
+ if (latest === null) {
38
+ console.log("- no local runs found yet");
39
+ console.log(`- expected pointer: ${getLatestRunPointerPath()}`);
40
+ return;
41
+ }
42
+ if (existsSync(latest.summaryPath)) {
43
+ const summary = readJson(latest.summaryPath);
44
+ if (summary.runId) console.log(`- latest run: ${summary.runId}`);
45
+ if (summary.mode) console.log(`- mode: ${summary.mode}`);
46
+ if (summary.piExitCode !== void 0) console.log(`- pi exit code: ${summary.piExitCode}`);
47
+ if (summary.message) console.log(`- note: ${summary.message}`);
48
+ }
49
+ if (existsSync(latest.manifestPath)) {
50
+ const manifest = readJson(latest.manifestPath);
51
+ if (manifest.repoRoot) console.log(`- repo root: ${manifest.repoRoot}`);
52
+ if (manifest.branch) console.log(`- branch: ${manifest.branch}`);
53
+ if (manifest.goal) console.log(`- goal: ${manifest.goal}`);
54
+ if (manifest.planPath) console.log(`- plan path: ${manifest.planPath}`);
55
+ if (!manifest.planPath && manifest.recommendedPlanDir) console.log(`- recommended plans: ${manifest.recommendedPlanDir}`);
56
+ if (manifest.stopReason) console.log(`- stop reason: ${manifest.stopReason}`);
57
+ }
58
+ if (!existsSync(latest.metricsPath)) {
59
+ console.log("- no metrics snapshot found yet");
60
+ console.log(`- expected path: ${latest.metricsPath}`);
61
+ return;
62
+ }
63
+ console.log(`- metrics file: ${latest.metricsPath}`);
64
+ console.log(readFileSync(latest.metricsPath, "utf-8").trim());
65
+ }
66
+ if (import.meta.url === `file://${process.argv[1]}`) showTownStatus();
67
+
68
+ //#endregion
69
+ export { resolveLatestRunPointer, showTownStatus };
70
+ //# sourceMappingURL=status.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.mjs","names":[],"sources":["../src/status.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { createRepoSlug, getRepoIdentity, getRepoRoot } from \"../../core/src/index.js\"\nimport { parseCliFlags } from \"./config.js\"\nimport {\n\tgetLatestRunPointerPath,\n\tgetRepoArtifactsDir,\n\tgetRepoLatestRunPointerPath,\n\tgetTownHomeDir,\n} from \"./paths.js\"\n\ninterface LatestRunPointer {\n\trepoSlug: string\n\trepoRoot: string\n\trunId?: string\n\trunDir?: string\n\tlatestDir: string\n\tmanifestPath: string\n\tmetricsPath: string\n\tsummaryPath: string\n\tupdatedAt?: string\n}\n\nfunction readJson<T>(path: string): T {\n\treturn JSON.parse(readFileSync(path, \"utf-8\")) as T\n}\n\nfunction createFallbackPointer(repoRoot: string, repoSlug: string): LatestRunPointer {\n\tconst latestDir = join(getRepoArtifactsDir(repoSlug), \"latest\")\n\treturn {\n\t\trepoSlug,\n\t\trepoRoot,\n\t\tlatestDir,\n\t\tmanifestPath: join(latestDir, \"manifest.json\"),\n\t\tmetricsPath: join(latestDir, \"metrics.json\"),\n\t\tsummaryPath: join(latestDir, \"run-summary.json\"),\n\t}\n}\n\nexport function resolveLatestRunPointer(argv = process.argv.slice(2)): LatestRunPointer | null {\n\tconst flags = parseCliFlags(argv)\n\n\tif (flags.repo) {\n\t\tconst repoRoot = getRepoRoot(flags.repo)\n\t\tconst repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot)\n\t\tconst repoPointerPath = getRepoLatestRunPointerPath(repoSlug)\n\t\tif (existsSync(repoPointerPath)) return readJson<LatestRunPointer>(repoPointerPath)\n\t\treturn createFallbackPointer(repoRoot, repoSlug)\n\t}\n\n\tconst latestPointerPath = getLatestRunPointerPath()\n\tif (!existsSync(latestPointerPath)) return null\n\treturn readJson<LatestRunPointer>(latestPointerPath)\n}\n\nexport function showTownStatus(argv = process.argv.slice(2)) {\n\tconst latest = resolveLatestRunPointer(argv)\n\tconsole.log(\"[pitown] status\")\n\tconsole.log(`- town home: ${getTownHomeDir()}`)\n\n\tif (latest === null) {\n\t\tconsole.log(\"- no local runs found yet\")\n\t\tconsole.log(`- expected pointer: ${getLatestRunPointerPath()}`)\n\t\treturn\n\t}\n\n\tif (existsSync(latest.summaryPath)) {\n\t\tconst summary = readJson<{\n\t\t\trunId?: string\n\t\t\tmode?: string\n\t\t\tmessage?: string\n\t\t\tpiExitCode?: number\n\t\t}>(latest.summaryPath)\n\t\tif (summary.runId) console.log(`- latest run: ${summary.runId}`)\n\t\tif (summary.mode) console.log(`- mode: ${summary.mode}`)\n\t\tif (summary.piExitCode !== undefined) console.log(`- pi exit code: ${summary.piExitCode}`)\n\t\tif (summary.message) console.log(`- note: ${summary.message}`)\n\t}\n\n\tif (existsSync(latest.manifestPath)) {\n\t\tconst manifest = readJson<{\n\t\t\trepoRoot?: string\n\t\t\tbranch?: string\n\t\t\tgoal?: string | null\n\t\t\tplanPath?: string | null\n\t\t\trecommendedPlanDir?: string | null\n\t\t\tstopReason?: string | null\n\t\t}>(latest.manifestPath)\n\t\tif (manifest.repoRoot) console.log(`- repo root: ${manifest.repoRoot}`)\n\t\tif (manifest.branch) console.log(`- branch: ${manifest.branch}`)\n\t\tif (manifest.goal) console.log(`- goal: ${manifest.goal}`)\n\t\tif (manifest.planPath) console.log(`- plan path: ${manifest.planPath}`)\n\t\tif (!manifest.planPath && manifest.recommendedPlanDir) {\n\t\t\tconsole.log(`- recommended plans: ${manifest.recommendedPlanDir}`)\n\t\t}\n\t\tif (manifest.stopReason) console.log(`- stop reason: ${manifest.stopReason}`)\n\t}\n\n\tif (!existsSync(latest.metricsPath)) {\n\t\tconsole.log(\"- no metrics snapshot found yet\")\n\t\tconsole.log(`- expected path: ${latest.metricsPath}`)\n\t\treturn\n\t}\n\n\tconsole.log(`- metrics file: ${latest.metricsPath}`)\n\tconsole.log(readFileSync(latest.metricsPath, \"utf-8\").trim())\n}\n\nif (import.meta.url === `file://${process.argv[1]}`) {\n\tshowTownStatus()\n}\n"],"mappings":";;;;;AAuBA,SAAS,SAAY,MAAiB;AACrC,QAAO,KAAK,MAAM,aAAa,MAAM,QAAQ,CAAC;;AAG/C,SAAS,sBAAsB,UAAkB,UAAoC;CACpF,MAAM,YAAY,KAAK,oBAAoB,SAAS,EAAE,SAAS;AAC/D,QAAO;EACN;EACA;EACA;EACA,cAAc,KAAK,WAAW,gBAAgB;EAC9C,aAAa,KAAK,WAAW,eAAe;EAC5C,aAAa,KAAK,WAAW,mBAAmB;EAChD;;AAGF,SAAgB,wBAAwB,OAAO,QAAQ,KAAK,MAAM,EAAE,EAA2B;CAC9F,MAAM,QAAQ,cAAc,KAAK;AAEjC,KAAI,MAAM,MAAM;EACf,MAAM,WAAW,YAAY,MAAM,KAAK;EACxC,MAAM,WAAW,eAAe,gBAAgB,SAAS,EAAE,SAAS;EACpE,MAAM,kBAAkB,4BAA4B,SAAS;AAC7D,MAAI,WAAW,gBAAgB,CAAE,QAAO,SAA2B,gBAAgB;AACnF,SAAO,sBAAsB,UAAU,SAAS;;CAGjD,MAAM,oBAAoB,yBAAyB;AACnD,KAAI,CAAC,WAAW,kBAAkB,CAAE,QAAO;AAC3C,QAAO,SAA2B,kBAAkB;;AAGrD,SAAgB,eAAe,OAAO,QAAQ,KAAK,MAAM,EAAE,EAAE;CAC5D,MAAM,SAAS,wBAAwB,KAAK;AAC5C,SAAQ,IAAI,kBAAkB;AAC9B,SAAQ,IAAI,gBAAgB,gBAAgB,GAAG;AAE/C,KAAI,WAAW,MAAM;AACpB,UAAQ,IAAI,4BAA4B;AACxC,UAAQ,IAAI,uBAAuB,yBAAyB,GAAG;AAC/D;;AAGD,KAAI,WAAW,OAAO,YAAY,EAAE;EACnC,MAAM,UAAU,SAKb,OAAO,YAAY;AACtB,MAAI,QAAQ,MAAO,SAAQ,IAAI,iBAAiB,QAAQ,QAAQ;AAChE,MAAI,QAAQ,KAAM,SAAQ,IAAI,WAAW,QAAQ,OAAO;AACxD,MAAI,QAAQ,eAAe,OAAW,SAAQ,IAAI,mBAAmB,QAAQ,aAAa;AAC1F,MAAI,QAAQ,QAAS,SAAQ,IAAI,WAAW,QAAQ,UAAU;;AAG/D,KAAI,WAAW,OAAO,aAAa,EAAE;EACpC,MAAM,WAAW,SAOd,OAAO,aAAa;AACvB,MAAI,SAAS,SAAU,SAAQ,IAAI,gBAAgB,SAAS,WAAW;AACvE,MAAI,SAAS,OAAQ,SAAQ,IAAI,aAAa,SAAS,SAAS;AAChE,MAAI,SAAS,KAAM,SAAQ,IAAI,WAAW,SAAS,OAAO;AAC1D,MAAI,SAAS,SAAU,SAAQ,IAAI,gBAAgB,SAAS,WAAW;AACvE,MAAI,CAAC,SAAS,YAAY,SAAS,mBAClC,SAAQ,IAAI,wBAAwB,SAAS,qBAAqB;AAEnE,MAAI,SAAS,WAAY,SAAQ,IAAI,kBAAkB,SAAS,aAAa;;AAG9E,KAAI,CAAC,WAAW,OAAO,YAAY,EAAE;AACpC,UAAQ,IAAI,kCAAkC;AAC9C,UAAQ,IAAI,oBAAoB,OAAO,cAAc;AACrD;;AAGD,SAAQ,IAAI,mBAAmB,OAAO,cAAc;AACpD,SAAQ,IAAI,aAAa,OAAO,aAAa,QAAQ,CAAC,MAAM,CAAC;;AAG9D,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAC9C,iBAAgB"}
@@ -0,0 +1,5 @@
1
+ //#region src/watch.d.ts
2
+ declare function watchTown(argv?: string[]): void;
3
+ //#endregion
4
+ export { watchTown };
5
+ //# sourceMappingURL=watch.d.mts.map
package/dist/watch.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import { s as getTownHomeDir } from "./config-D67TLZft.mjs";
2
+ import { resolveLatestRunPointer, showTownStatus } from "./status.mjs";
3
+ import { existsSync, watchFile } from "node:fs";
4
+
5
+ //#region src/watch.ts
6
+ function watchTown(argv = process.argv.slice(2)) {
7
+ const metricsPath = resolveLatestRunPointer(argv)?.metricsPath;
8
+ console.log("[pitown] watch");
9
+ console.log(`- town home: ${getTownHomeDir()}`);
10
+ console.log("- press Ctrl+C to stop");
11
+ showTownStatus(argv);
12
+ if (!metricsPath || !existsSync(metricsPath)) {
13
+ console.log("- metrics file does not exist yet; watch will activate after the first run");
14
+ return;
15
+ }
16
+ watchFile(metricsPath, { interval: 1e3 }, () => {
17
+ console.log("\n[pitown] metrics updated");
18
+ showTownStatus(argv);
19
+ });
20
+ }
21
+ if (import.meta.url === `file://${process.argv[1]}`) watchTown();
22
+
23
+ //#endregion
24
+ export { watchTown };
25
+ //# sourceMappingURL=watch.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.mjs","names":[],"sources":["../src/watch.ts"],"sourcesContent":["import { existsSync, watchFile } from \"node:fs\"\nimport { getTownHomeDir } from \"./paths.js\"\nimport { resolveLatestRunPointer, showTownStatus } from \"./status.js\"\n\nexport function watchTown(argv = process.argv.slice(2)) {\n\tconst latest = resolveLatestRunPointer(argv)\n\tconst metricsPath = latest?.metricsPath\n\n\tconsole.log(\"[pitown] watch\")\n\tconsole.log(`- town home: ${getTownHomeDir()}`)\n\tconsole.log(\"- press Ctrl+C to stop\")\n\tshowTownStatus(argv)\n\n\tif (!metricsPath || !existsSync(metricsPath)) {\n\t\tconsole.log(\"- metrics file does not exist yet; watch will activate after the first run\")\n\t\treturn\n\t}\n\n\twatchFile(metricsPath, { interval: 1000 }, () => {\n\t\tconsole.log(\"\\n[pitown] metrics updated\")\n\t\tshowTownStatus(argv)\n\t})\n}\n\nif (import.meta.url === `file://${process.argv[1]}`) {\n\twatchTown()\n}\n"],"mappings":";;;;;AAIA,SAAgB,UAAU,OAAO,QAAQ,KAAK,MAAM,EAAE,EAAE;CAEvD,MAAM,cADS,wBAAwB,KAAK,EAChB;AAE5B,SAAQ,IAAI,iBAAiB;AAC7B,SAAQ,IAAI,gBAAgB,gBAAgB,GAAG;AAC/C,SAAQ,IAAI,yBAAyB;AACrC,gBAAe,KAAK;AAEpB,KAAI,CAAC,eAAe,CAAC,WAAW,YAAY,EAAE;AAC7C,UAAQ,IAAI,6EAA6E;AACzF;;AAGD,WAAU,aAAa,EAAE,UAAU,KAAM,QAAQ;AAChD,UAAQ,IAAI,6BAA6B;AACzC,iBAAe,KAAK;GACnB;;AAGH,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAC9C,YAAW"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@schilderlabs/pitown",
3
+ "version": "0.1.0",
4
+ "description": "Globally installable CLI for Pi Town",
5
+ "type": "module",
6
+ "bin": {
7
+ "pitown": "./dist/index.mjs"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "types": "./dist/index.d.mts"
13
+ }
14
+ },
15
+ "types": "./dist/index.d.mts",
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=24"
22
+ },
23
+ "dependencies": {
24
+ "@schilderlabs/pitown-core": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "eslint": "9.39.3",
28
+ "tsdown": "0.20.3",
29
+ "tsx": "4.21.0",
30
+ "typescript": "5.9.2",
31
+ "vitest": "4.0.18"
32
+ },
33
+ "keywords": [
34
+ "pitown",
35
+ "pi-town",
36
+ "cli",
37
+ "orchestration",
38
+ "automation"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/schilderlabs/pitown.git"
43
+ },
44
+ "homepage": "https://github.com/schilderlabs/pitown#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/schilderlabs/pitown/issues"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "license": "MIT",
52
+ "scripts": {
53
+ "build": "tsdown",
54
+ "lint": "eslint .",
55
+ "test": "vitest run --passWithNoTests",
56
+ "town:run": "tsx src/run.ts",
57
+ "town:status": "tsx src/status.ts",
58
+ "town:watch": "tsx src/watch.ts",
59
+ "typecheck": "tsc --noEmit"
60
+ }
61
+ }