@schilderlabs/pitown 0.1.2 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/{config-Bw-mNdF5.mjs → config-BG1v4iIi.mjs} +29 -50
- package/dist/config-BG1v4iIi.mjs.map +1 -0
- package/dist/doctor.d.mts +8 -0
- package/dist/doctor.mjs +42 -0
- package/dist/doctor.mjs.map +1 -0
- package/dist/entrypoint-WBAQmFbT.mjs +61 -0
- package/dist/entrypoint-WBAQmFbT.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1265 -8
- package/dist/index.mjs.map +1 -1
- package/dist/loop-CocC9qO1.mjs +678 -0
- package/dist/loop-CocC9qO1.mjs.map +1 -0
- package/dist/pi-C7HRNjBG.mjs +12 -0
- package/dist/pi-C7HRNjBG.mjs.map +1 -0
- package/dist/repo-context-BuA2JqPm.mjs +45 -0
- package/dist/repo-context-BuA2JqPm.mjs.map +1 -0
- package/dist/run.d.mts +3 -69
- package/dist/run.mjs +39 -19
- package/dist/run.mjs.map +1 -1
- package/dist/status.mjs +2 -1
- package/dist/status.mjs.map +1 -1
- package/dist/tasks-De4IAy3x.mjs +195 -0
- package/dist/tasks-De4IAy3x.mjs.map +1 -0
- package/dist/types-COGNGvsY.d.mts +142 -0
- package/dist/watch.d.mts +35 -1
- package/dist/watch.mjs +129 -16
- package/dist/watch.mjs.map +1 -1
- package/package.json +21 -23
- package/dist/config-Bw-mNdF5.mjs.map +0 -1
- package/dist/controller-D7lezZjg.mjs +0 -342
- package/dist/controller-D7lezZjg.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,42 +1,1299 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { m as
|
|
2
|
+
import { a as appendAgentMessage, c as getAgentDir, d as listAgentStates, f as readAgentMessages, i as writeTaskRecord, l as getAgentSessionsDir, m as writeAgentState, n as listTaskRecords, o as createAgentSessionRecord, p as readAgentState, r as updateTaskRecordStatus, s as createAgentState, t as createTaskRecord, u as getLatestAgentSession } from "./tasks-De4IAy3x.mjs";
|
|
3
|
+
import { t as runLoop } from "./loop-CocC9qO1.mjs";
|
|
4
|
+
import { a as runCommandSync, i as runCommandInteractive, n as assertCommandAvailable, t as isDirectExecution } from "./entrypoint-WBAQmFbT.mjs";
|
|
5
|
+
import { a as getRecommendedPlanDir, c as getReposRootDir, d as createRepoSlug, f as getCurrentBranch, l as getTownHomeDir, m as getRepoRoot, n as parseOptionalRepoFlag, o as getRepoArtifactsDir, p as getRepoIdentity, u as getUserConfigPath } from "./config-BG1v4iIi.mjs";
|
|
6
|
+
import { t as resolveRepoContext } from "./repo-context-BuA2JqPm.mjs";
|
|
7
|
+
import { runDoctor } from "./doctor.mjs";
|
|
3
8
|
import { runTown } from "./run.mjs";
|
|
4
9
|
import { showTownStatus } from "./status.mjs";
|
|
5
10
|
import { watchTown } from "./watch.mjs";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { readPiTownMayorPrompt, resolvePiTownExtensionPath } from "@schilderlabs/pitown-package";
|
|
6
18
|
|
|
19
|
+
//#region ../core/src/orchestration.ts
|
|
20
|
+
function createPiInvocationArgs(input) {
|
|
21
|
+
const args = [];
|
|
22
|
+
if (input.extensionPath) args.push("--extension", input.extensionPath);
|
|
23
|
+
if (input.appendedSystemPrompt) args.push("--append-system-prompt", input.appendedSystemPrompt);
|
|
24
|
+
if (input.sessionPath) args.push("--session", input.sessionPath);
|
|
25
|
+
else if (input.sessionDir) args.push("--session-dir", input.sessionDir);
|
|
26
|
+
else throw new Error("Pi invocation requires a session path or session directory");
|
|
27
|
+
if (input.prompt) args.push("-p", input.prompt);
|
|
28
|
+
return args;
|
|
29
|
+
}
|
|
30
|
+
function createDetachedRunnerInvocation(encodedPayload) {
|
|
31
|
+
if (fileURLToPath(import.meta.url).endsWith(".ts")) {
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
return {
|
|
34
|
+
command: process.execPath,
|
|
35
|
+
args: [
|
|
36
|
+
"--import",
|
|
37
|
+
require.resolve("tsx"),
|
|
38
|
+
fileURLToPath(new URL("./agent-runner.ts", import.meta.url)),
|
|
39
|
+
encodedPayload
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
command: process.execPath,
|
|
45
|
+
args: [fileURLToPath(new URL("./agent-runner.mjs", import.meta.url)), encodedPayload]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createRolePrompt(input) {
|
|
49
|
+
const task = input.task ?? "pick the next bounded task from the current repo context";
|
|
50
|
+
switch (input.role) {
|
|
51
|
+
case "mayor": return [
|
|
52
|
+
"You are the Pi Town mayor.",
|
|
53
|
+
"You coordinate work for this repository and act as the primary human-facing agent.",
|
|
54
|
+
"",
|
|
55
|
+
`Repository: ${input.repoRoot}`,
|
|
56
|
+
`Task: ${task}`,
|
|
57
|
+
"Keep updates concise, choose bounded next steps, and leave a durable artifact trail."
|
|
58
|
+
].join("\n");
|
|
59
|
+
case "reviewer": return [
|
|
60
|
+
"You are the Pi Town reviewer.",
|
|
61
|
+
"You review work for correctness, safety, and completeness.",
|
|
62
|
+
"",
|
|
63
|
+
`Repository: ${input.repoRoot}`,
|
|
64
|
+
`Task: ${task}`,
|
|
65
|
+
"Focus on validation confidence, regressions, and whether the output is ready for a human handoff."
|
|
66
|
+
].join("\n");
|
|
67
|
+
case "docs-keeper": return [
|
|
68
|
+
"You are the Pi Town docs keeper.",
|
|
69
|
+
"You summarize outcomes, blockers, and continuity in compact factual language.",
|
|
70
|
+
"",
|
|
71
|
+
`Repository: ${input.repoRoot}`,
|
|
72
|
+
`Task: ${task}`,
|
|
73
|
+
"Keep the output concise and useful for the next run or human review."
|
|
74
|
+
].join("\n");
|
|
75
|
+
default: return [
|
|
76
|
+
"You are the Pi Town worker.",
|
|
77
|
+
"You implement one bounded task at a time.",
|
|
78
|
+
"",
|
|
79
|
+
`Repository: ${input.repoRoot}`,
|
|
80
|
+
`Task: ${task}`,
|
|
81
|
+
"Keep scope tight, prefer explicit validations, and summarize what changed and what still needs follow-up."
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function resolveAgentSession(agentId, artifactsDir) {
|
|
86
|
+
const state = readAgentState(artifactsDir, agentId);
|
|
87
|
+
if (state === null) throw new Error(`Unknown agent: ${agentId}`);
|
|
88
|
+
const latestSession = getLatestAgentSession(artifactsDir, agentId);
|
|
89
|
+
const sessionPath = state.session.sessionPath ?? latestSession.sessionPath;
|
|
90
|
+
const sessionId = state.session.sessionId ?? latestSession.sessionId;
|
|
91
|
+
const sessionDir = state.session.sessionDir ?? latestSession.sessionDir ?? getAgentSessionsDir(artifactsDir, agentId);
|
|
92
|
+
if (sessionPath === null) throw new Error(`Agent ${agentId} does not have a persisted Pi session yet.`);
|
|
93
|
+
return {
|
|
94
|
+
state,
|
|
95
|
+
session: createAgentSessionRecord({
|
|
96
|
+
sessionDir,
|
|
97
|
+
sessionId,
|
|
98
|
+
sessionPath,
|
|
99
|
+
processId: state.session.processId,
|
|
100
|
+
lastAttachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
101
|
+
})
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function queueAgentMessage(input) {
|
|
105
|
+
const state = readAgentState(input.artifactsDir, input.agentId);
|
|
106
|
+
if (state === null) throw new Error(`Unknown agent: ${input.agentId}`);
|
|
107
|
+
appendAgentMessage({
|
|
108
|
+
artifactsDir: input.artifactsDir,
|
|
109
|
+
agentId: input.agentId,
|
|
110
|
+
box: "inbox",
|
|
111
|
+
from: input.from,
|
|
112
|
+
body: input.body
|
|
113
|
+
});
|
|
114
|
+
writeAgentState(input.artifactsDir, createAgentState({
|
|
115
|
+
...state,
|
|
116
|
+
status: state.status === "idle" ? "queued" : state.status,
|
|
117
|
+
lastMessage: input.body,
|
|
118
|
+
waitingOn: null,
|
|
119
|
+
blocked: false,
|
|
120
|
+
session: createAgentSessionRecord({
|
|
121
|
+
sessionDir: state.session.sessionDir ?? getAgentSessionsDir(input.artifactsDir, input.agentId),
|
|
122
|
+
sessionId: state.session.sessionId,
|
|
123
|
+
sessionPath: state.session.sessionPath,
|
|
124
|
+
processId: state.session.processId,
|
|
125
|
+
lastAttachedAt: state.session.lastAttachedAt
|
|
126
|
+
})
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
function spawnAgentRun(options) {
|
|
130
|
+
const sessionDir = getAgentSessionsDir(options.artifactsDir, options.agentId);
|
|
131
|
+
if (readAgentState(options.artifactsDir, options.agentId) !== null) throw new Error(`Agent already exists: ${options.agentId}`);
|
|
132
|
+
assertCommandAvailable("pi");
|
|
133
|
+
const state = createAgentState({
|
|
134
|
+
agentId: options.agentId,
|
|
135
|
+
role: options.role,
|
|
136
|
+
status: "queued",
|
|
137
|
+
taskId: options.taskId ?? null,
|
|
138
|
+
task: options.task,
|
|
139
|
+
lastMessage: options.task ? `Spawned with task: ${options.task}` : `Spawned ${options.role} agent`,
|
|
140
|
+
session: createAgentSessionRecord({ sessionDir })
|
|
141
|
+
});
|
|
142
|
+
writeAgentState(options.artifactsDir, state);
|
|
143
|
+
if (options.task) appendAgentMessage({
|
|
144
|
+
artifactsDir: options.artifactsDir,
|
|
145
|
+
agentId: options.agentId,
|
|
146
|
+
box: "inbox",
|
|
147
|
+
from: "system",
|
|
148
|
+
body: options.task
|
|
149
|
+
});
|
|
150
|
+
writeAgentState(options.artifactsDir, createAgentState({
|
|
151
|
+
...state,
|
|
152
|
+
status: "running",
|
|
153
|
+
lastMessage: options.task ? `Running ${options.role} task: ${options.task}` : `Running ${options.role} agent`
|
|
154
|
+
}));
|
|
155
|
+
const piArgs = createPiInvocationArgs({
|
|
156
|
+
sessionDir,
|
|
157
|
+
prompt: createRolePrompt({
|
|
158
|
+
role: options.role,
|
|
159
|
+
task: options.task,
|
|
160
|
+
repoRoot: options.repoRoot
|
|
161
|
+
}),
|
|
162
|
+
appendedSystemPrompt: options.appendedSystemPrompt,
|
|
163
|
+
extensionPath: options.extensionPath
|
|
164
|
+
});
|
|
165
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
166
|
+
const runner = createDetachedRunnerInvocation(Buffer.from(JSON.stringify({
|
|
167
|
+
repoRoot: options.repoRoot,
|
|
168
|
+
artifactsDir: options.artifactsDir,
|
|
169
|
+
agentId: options.agentId,
|
|
170
|
+
role: options.role,
|
|
171
|
+
task: options.task,
|
|
172
|
+
taskId: options.taskId ?? null,
|
|
173
|
+
sessionDir,
|
|
174
|
+
piArgs
|
|
175
|
+
}), "utf-8").toString("base64url"));
|
|
176
|
+
const child = spawn(runner.command, runner.args, {
|
|
177
|
+
cwd: options.repoRoot,
|
|
178
|
+
detached: true,
|
|
179
|
+
env: process.env,
|
|
180
|
+
stdio: "ignore"
|
|
181
|
+
});
|
|
182
|
+
child.unref();
|
|
183
|
+
if (!child.pid) throw new Error(`Failed to launch detached ${options.role} run for ${options.agentId}`);
|
|
184
|
+
writeFileSync(`${getAgentDir(options.artifactsDir, options.agentId)}/latest-invocation.json`, `${JSON.stringify({
|
|
185
|
+
command: "pi",
|
|
186
|
+
args: piArgs,
|
|
187
|
+
exitCode: null,
|
|
188
|
+
sessionDir,
|
|
189
|
+
sessionPath: null,
|
|
190
|
+
sessionId: null,
|
|
191
|
+
processId: child.pid,
|
|
192
|
+
startedAt
|
|
193
|
+
}, null, 2)}\n`, "utf-8");
|
|
194
|
+
return {
|
|
195
|
+
launch: {
|
|
196
|
+
processId: child.pid,
|
|
197
|
+
startedAt
|
|
198
|
+
},
|
|
199
|
+
latestSession: createAgentSessionRecord({ sessionDir })
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function runAgentTurn(options) {
|
|
203
|
+
assertCommandAvailable("pi");
|
|
204
|
+
const resolved = resolveAgentSession(options.agentId, options.artifactsDir);
|
|
205
|
+
const messageSource = options.from ?? "human";
|
|
206
|
+
writeAgentState(options.artifactsDir, createAgentState({
|
|
207
|
+
...resolved.state,
|
|
208
|
+
status: "running",
|
|
209
|
+
lastMessage: `Responding to ${messageSource}: ${options.message}`,
|
|
210
|
+
waitingOn: null,
|
|
211
|
+
blocked: false,
|
|
212
|
+
session: resolved.session
|
|
213
|
+
}));
|
|
214
|
+
const piArgs = options.runtimeArgs && options.runtimeArgs.length > 0 ? options.runtimeArgs : createPiInvocationArgs({
|
|
215
|
+
sessionPath: resolved.session.sessionPath,
|
|
216
|
+
prompt: options.message
|
|
217
|
+
});
|
|
218
|
+
const piResult = runCommandSync("pi", piArgs, {
|
|
219
|
+
cwd: options.repoRoot,
|
|
220
|
+
env: process.env
|
|
221
|
+
});
|
|
222
|
+
const latestSession = getLatestAgentSession(options.artifactsDir, options.agentId);
|
|
223
|
+
const agentArtifactsDir = getAgentDir(options.artifactsDir, options.agentId);
|
|
224
|
+
writeFileSync(`${agentArtifactsDir}/latest-stdout.txt`, piResult.stdout, "utf-8");
|
|
225
|
+
writeFileSync(`${agentArtifactsDir}/latest-stderr.txt`, piResult.stderr, "utf-8");
|
|
226
|
+
writeFileSync(`${agentArtifactsDir}/latest-invocation.json`, `${JSON.stringify({
|
|
227
|
+
command: "pi",
|
|
228
|
+
args: piArgs,
|
|
229
|
+
exitCode: piResult.exitCode,
|
|
230
|
+
sessionDir: latestSession.sessionDir,
|
|
231
|
+
sessionPath: latestSession.sessionPath,
|
|
232
|
+
sessionId: latestSession.sessionId
|
|
233
|
+
}, null, 2)}\n`, "utf-8");
|
|
234
|
+
const completionMessage = piResult.stdout.trim() || (piResult.exitCode === 0 ? `${resolved.state.role} turn completed` : `${resolved.state.role} turn exited with code ${piResult.exitCode}`);
|
|
235
|
+
appendAgentMessage({
|
|
236
|
+
artifactsDir: options.artifactsDir,
|
|
237
|
+
agentId: options.agentId,
|
|
238
|
+
box: "outbox",
|
|
239
|
+
from: options.agentId,
|
|
240
|
+
body: completionMessage
|
|
241
|
+
});
|
|
242
|
+
writeAgentState(options.artifactsDir, createAgentState({
|
|
243
|
+
...resolved.state,
|
|
244
|
+
status: piResult.exitCode === 0 ? "idle" : "blocked",
|
|
245
|
+
lastMessage: completionMessage,
|
|
246
|
+
waitingOn: piResult.exitCode === 0 ? null : "human-or-follow-up-run",
|
|
247
|
+
blocked: piResult.exitCode !== 0,
|
|
248
|
+
session: createAgentSessionRecord({
|
|
249
|
+
sessionDir: latestSession.sessionDir,
|
|
250
|
+
sessionId: latestSession.sessionId,
|
|
251
|
+
sessionPath: latestSession.sessionPath,
|
|
252
|
+
processId: null
|
|
253
|
+
})
|
|
254
|
+
}));
|
|
255
|
+
return {
|
|
256
|
+
piResult,
|
|
257
|
+
latestSession,
|
|
258
|
+
completionMessage
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function delegateTask(options) {
|
|
262
|
+
if (readAgentState(options.artifactsDir, options.fromAgentId) === null) throw new Error(`Unknown delegating agent: ${options.fromAgentId}`);
|
|
263
|
+
const agentId = options.agentId ?? `${options.role}-${Date.now()}`;
|
|
264
|
+
const task = createTaskRecord({
|
|
265
|
+
taskId: `task-${Date.now()}`,
|
|
266
|
+
title: options.task,
|
|
267
|
+
status: "queued",
|
|
268
|
+
role: options.role,
|
|
269
|
+
assignedAgentId: agentId,
|
|
270
|
+
createdBy: options.fromAgentId
|
|
271
|
+
});
|
|
272
|
+
writeTaskRecord(options.artifactsDir, task);
|
|
273
|
+
appendAgentMessage({
|
|
274
|
+
artifactsDir: options.artifactsDir,
|
|
275
|
+
agentId: options.fromAgentId,
|
|
276
|
+
box: "outbox",
|
|
277
|
+
from: options.fromAgentId,
|
|
278
|
+
body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
|
|
279
|
+
});
|
|
280
|
+
const { launch, latestSession } = spawnAgentRun({
|
|
281
|
+
repoRoot: options.repoRoot,
|
|
282
|
+
artifactsDir: options.artifactsDir,
|
|
283
|
+
role: options.role,
|
|
284
|
+
agentId,
|
|
285
|
+
appendedSystemPrompt: options.appendedSystemPrompt,
|
|
286
|
+
extensionPath: options.extensionPath,
|
|
287
|
+
task: options.task,
|
|
288
|
+
taskId: task.taskId
|
|
289
|
+
});
|
|
290
|
+
appendAgentMessage({
|
|
291
|
+
artifactsDir: options.artifactsDir,
|
|
292
|
+
agentId,
|
|
293
|
+
box: "inbox",
|
|
294
|
+
from: options.fromAgentId,
|
|
295
|
+
body: `Delegated by ${options.fromAgentId} as ${task.taskId}: ${options.task}`
|
|
296
|
+
});
|
|
297
|
+
writeTaskRecord(options.artifactsDir, {
|
|
298
|
+
...task,
|
|
299
|
+
status: "running",
|
|
300
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
task: {
|
|
304
|
+
...task,
|
|
305
|
+
status: "running",
|
|
306
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
307
|
+
},
|
|
308
|
+
agentId,
|
|
309
|
+
launch,
|
|
310
|
+
latestSession
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region ../core/src/stop.ts
|
|
316
|
+
const DEFAULT_GRACE_MS = 750;
|
|
317
|
+
function sleepMs(ms) {
|
|
318
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
319
|
+
}
|
|
320
|
+
function getLocksDir() {
|
|
321
|
+
return join(homedir(), ".pi-town", "locks");
|
|
322
|
+
}
|
|
323
|
+
function processAlive(pid) {
|
|
324
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
325
|
+
try {
|
|
326
|
+
process.kill(pid, 0);
|
|
327
|
+
return true;
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function terminateProcess(pid, options) {
|
|
333
|
+
if (!processAlive(pid)) return {
|
|
334
|
+
signal: null,
|
|
335
|
+
exited: true
|
|
336
|
+
};
|
|
337
|
+
try {
|
|
338
|
+
process.kill(pid, "SIGTERM");
|
|
339
|
+
} catch {
|
|
340
|
+
return {
|
|
341
|
+
signal: null,
|
|
342
|
+
exited: !processAlive(pid)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
|
|
346
|
+
const deadline = Date.now() + graceMs;
|
|
347
|
+
while (Date.now() < deadline) {
|
|
348
|
+
if (!processAlive(pid)) return {
|
|
349
|
+
signal: "SIGTERM",
|
|
350
|
+
exited: true
|
|
351
|
+
};
|
|
352
|
+
sleepMs(25);
|
|
353
|
+
}
|
|
354
|
+
if (!options.force) return {
|
|
355
|
+
signal: "SIGTERM",
|
|
356
|
+
exited: !processAlive(pid)
|
|
357
|
+
};
|
|
358
|
+
try {
|
|
359
|
+
process.kill(pid, "SIGKILL");
|
|
360
|
+
} catch {
|
|
361
|
+
return {
|
|
362
|
+
signal: "SIGTERM",
|
|
363
|
+
exited: !processAlive(pid)
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
signal: "SIGKILL",
|
|
368
|
+
exited: !processAlive(pid)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function readLease(path) {
|
|
372
|
+
try {
|
|
373
|
+
return {
|
|
374
|
+
...JSON.parse(readFileSync(path, "utf-8")),
|
|
375
|
+
path
|
|
376
|
+
};
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function createStopMessage(options) {
|
|
382
|
+
if (options.reason) return options.reason;
|
|
383
|
+
if (options.actorId) return `Stopped by ${options.actorId}`;
|
|
384
|
+
return "Stopped by operator";
|
|
385
|
+
}
|
|
386
|
+
function createStopMessageInput(options) {
|
|
387
|
+
return {
|
|
388
|
+
...options.actorId === void 0 ? {} : { actorId: options.actorId },
|
|
389
|
+
...options.reason === void 0 ? {} : { reason: options.reason }
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function createTerminateOptions(options) {
|
|
393
|
+
return {
|
|
394
|
+
...options.force === void 0 ? {} : { force: options.force },
|
|
395
|
+
...options.graceMs === void 0 ? {} : { graceMs: options.graceMs }
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function listRepoLeases(repoId) {
|
|
399
|
+
let entries;
|
|
400
|
+
try {
|
|
401
|
+
entries = readdirSync(getLocksDir());
|
|
402
|
+
} catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
return entries.filter((entry) => entry.endsWith(".json")).map((entry) => readLease(join(getLocksDir(), entry))).filter((record) => record !== null).filter((record) => repoId === void 0 || repoId === null || record.repoId === repoId);
|
|
406
|
+
}
|
|
407
|
+
function stopRepoLeases(options) {
|
|
408
|
+
const results = listRepoLeases(options.repoId).map((lease) => {
|
|
409
|
+
const termination = terminateProcess(lease.pid, options);
|
|
410
|
+
if (termination.exited) rmSync(lease.path, { force: true });
|
|
411
|
+
return {
|
|
412
|
+
path: lease.path,
|
|
413
|
+
runId: lease.runId,
|
|
414
|
+
repoId: lease.repoId,
|
|
415
|
+
branch: lease.branch,
|
|
416
|
+
processId: lease.pid,
|
|
417
|
+
signal: termination.signal,
|
|
418
|
+
exited: termination.exited
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
return {
|
|
422
|
+
results,
|
|
423
|
+
signaledProcesses: results.filter((result) => result.signal !== null).length
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function stopManagedAgents(options) {
|
|
427
|
+
const reason = createStopMessage(createStopMessageInput(options));
|
|
428
|
+
const excluded = new Set(options.excludeAgentIds ?? []);
|
|
429
|
+
const results = listAgentStates(options.artifactsDir).filter((agent) => {
|
|
430
|
+
if (excluded.has(agent.agentId)) return false;
|
|
431
|
+
if (options.agentId && agent.agentId !== options.agentId) return false;
|
|
432
|
+
return ![
|
|
433
|
+
"completed",
|
|
434
|
+
"failed",
|
|
435
|
+
"stopped"
|
|
436
|
+
].includes(agent.status);
|
|
437
|
+
}).map((state) => {
|
|
438
|
+
const processId = state.session.processId;
|
|
439
|
+
const termination = processId === null ? {
|
|
440
|
+
signal: null,
|
|
441
|
+
exited: true
|
|
442
|
+
} : terminateProcess(processId, createTerminateOptions(options));
|
|
443
|
+
if (state.taskId) updateTaskRecordStatus(options.artifactsDir, state.taskId, "aborted");
|
|
444
|
+
appendAgentMessage({
|
|
445
|
+
artifactsDir: options.artifactsDir,
|
|
446
|
+
agentId: state.agentId,
|
|
447
|
+
box: "outbox",
|
|
448
|
+
from: options.actorId ?? "system",
|
|
449
|
+
body: reason
|
|
450
|
+
});
|
|
451
|
+
writeAgentState(options.artifactsDir, createAgentState({
|
|
452
|
+
...state,
|
|
453
|
+
status: "stopped",
|
|
454
|
+
lastMessage: reason,
|
|
455
|
+
waitingOn: "stopped",
|
|
456
|
+
blocked: true,
|
|
457
|
+
session: createAgentSessionRecord({
|
|
458
|
+
sessionDir: state.session.sessionDir,
|
|
459
|
+
sessionId: state.session.sessionId,
|
|
460
|
+
sessionPath: state.session.sessionPath,
|
|
461
|
+
processId: null,
|
|
462
|
+
lastAttachedAt: state.session.lastAttachedAt
|
|
463
|
+
})
|
|
464
|
+
}));
|
|
465
|
+
return {
|
|
466
|
+
agentId: state.agentId,
|
|
467
|
+
previousStatus: state.status,
|
|
468
|
+
nextStatus: "stopped",
|
|
469
|
+
processId,
|
|
470
|
+
signal: termination.signal,
|
|
471
|
+
exited: termination.exited
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
results,
|
|
476
|
+
stoppedAgents: results.length,
|
|
477
|
+
signaledProcesses: results.filter((result) => result.signal !== null).length
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/agent-id.ts
|
|
483
|
+
function normalizeAgentId(agentId) {
|
|
484
|
+
return agentId;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
//#endregion
|
|
488
|
+
//#region src/pi-runtime.ts
|
|
489
|
+
function isMayorAgent(agentId) {
|
|
490
|
+
return agentId === "mayor";
|
|
491
|
+
}
|
|
492
|
+
function createPiTownRuntimeArgs(options) {
|
|
493
|
+
const args = ["--extension", resolvePiTownExtensionPath()];
|
|
494
|
+
if (isMayorAgent(options.agentId)) args.push("--append-system-prompt", readPiTownMayorPrompt());
|
|
495
|
+
if (options.sessionPath) args.push("--session", options.sessionPath);
|
|
496
|
+
else if (options.sessionDir) args.push("--session-dir", options.sessionDir);
|
|
497
|
+
else throw new Error("Pi Town runtime requires either a session path or a session directory");
|
|
498
|
+
if (options.prompt) args.push("-p", options.prompt);
|
|
499
|
+
if (options.message) args.push(options.message);
|
|
500
|
+
return args;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/attach.ts
|
|
505
|
+
function attachTownAgent(argv = process.argv.slice(2)) {
|
|
506
|
+
const repo = resolveRepoContext(argv);
|
|
507
|
+
const [agentArg] = repo.rest;
|
|
508
|
+
if (!agentArg) throw new Error("Usage: pitown attach [--repo <path>] <agent>");
|
|
509
|
+
const agentId = normalizeAgentId(agentArg);
|
|
510
|
+
assertCommandAvailable("pi");
|
|
511
|
+
const resolved = resolveAgentSession(agentId, repo.artifactsDir);
|
|
512
|
+
writeAgentState(repo.artifactsDir, {
|
|
513
|
+
...resolved.state,
|
|
514
|
+
session: resolved.session
|
|
515
|
+
});
|
|
516
|
+
console.log("[pitown] attach");
|
|
517
|
+
console.log(`- repo root: ${repo.repoRoot}`);
|
|
518
|
+
console.log(`- agent: ${agentId}`);
|
|
519
|
+
console.log(`- session: ${resolved.session.sessionPath}`);
|
|
520
|
+
const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
|
|
521
|
+
agentId,
|
|
522
|
+
sessionPath: resolved.session.sessionPath
|
|
523
|
+
}), {
|
|
524
|
+
cwd: repo.repoRoot,
|
|
525
|
+
env: process.env
|
|
526
|
+
});
|
|
527
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/board.ts
|
|
532
|
+
function truncate(text, max) {
|
|
533
|
+
if (!text) return "—";
|
|
534
|
+
const single = text.replace(/\n/g, " ").trim();
|
|
535
|
+
if (single.length <= max) return single;
|
|
536
|
+
return `${single.slice(0, max - 1)}…`;
|
|
537
|
+
}
|
|
538
|
+
function showTownBoard(argv = process.argv.slice(2)) {
|
|
539
|
+
const repo = resolveRepoContext(argv);
|
|
540
|
+
const agents = listAgentStates(repo.artifactsDir);
|
|
541
|
+
const tasks = listTaskRecords(repo.artifactsDir);
|
|
542
|
+
const repoName = basename(repo.repoRoot);
|
|
543
|
+
const branch = getCurrentBranch(repo.repoRoot);
|
|
544
|
+
const branchLabel = branch ? ` (${branch})` : "";
|
|
545
|
+
const mayor = agents.find((a) => a.agentId === "mayor");
|
|
546
|
+
const workers = agents.filter((a) => a.agentId !== "mayor");
|
|
547
|
+
const workersByStatus = (status) => workers.filter((a) => a.status === status).length;
|
|
548
|
+
console.log(`[pitown] board — ${repoName}${branchLabel}`);
|
|
549
|
+
if (mayor) {
|
|
550
|
+
const spawned = workers.length;
|
|
551
|
+
const running = workersByStatus("running") + workersByStatus("starting");
|
|
552
|
+
const completed = workersByStatus("completed") + workersByStatus("idle");
|
|
553
|
+
const blocked = workersByStatus("blocked") + workersByStatus("failed");
|
|
554
|
+
const parts = [`${spawned} spawned`];
|
|
555
|
+
if (running > 0) parts.push(`${running} running`);
|
|
556
|
+
if (completed > 0) parts.push(`${completed} done`);
|
|
557
|
+
if (blocked > 0) parts.push(`${blocked} blocked`);
|
|
558
|
+
console.log(`Mayor workers: ${parts.join(", ")}`);
|
|
559
|
+
}
|
|
560
|
+
console.log("");
|
|
561
|
+
console.log("Agents:");
|
|
562
|
+
if (agents.length === 0) console.log(" (no agents)");
|
|
563
|
+
else for (const agent of agents) {
|
|
564
|
+
const id = agent.agentId.padEnd(14);
|
|
565
|
+
const role = agent.role.padEnd(10);
|
|
566
|
+
const status = agent.status.padEnd(10);
|
|
567
|
+
const task = truncate(agent.task, 60);
|
|
568
|
+
const msg = agent.lastMessage ? ` | ${truncate(agent.lastMessage, 40)}` : "";
|
|
569
|
+
const waiting = agent.waitingOn ? ` | waiting on: ${agent.waitingOn}` : "";
|
|
570
|
+
console.log(` ${id}${role}${status}${task}${msg}${waiting}`);
|
|
571
|
+
}
|
|
572
|
+
console.log("");
|
|
573
|
+
console.log("Tasks:");
|
|
574
|
+
if (tasks.length === 0) console.log(" (no tasks)");
|
|
575
|
+
else for (const task of tasks) {
|
|
576
|
+
const id = task.taskId.padEnd(14);
|
|
577
|
+
const status = task.status.padEnd(12);
|
|
578
|
+
const assignee = (task.assignedAgentId ?? "—").padEnd(14);
|
|
579
|
+
const title = truncate(task.title, 60);
|
|
580
|
+
console.log(` ${id}${status}${assignee}${title}`);
|
|
581
|
+
}
|
|
582
|
+
const metricsPath = join(repo.artifactsDir, "latest", "metrics.json");
|
|
583
|
+
if (existsSync(metricsPath)) try {
|
|
584
|
+
const metrics = JSON.parse(readFileSync(metricsPath, "utf-8"));
|
|
585
|
+
console.log("");
|
|
586
|
+
console.log("Metrics (latest run):");
|
|
587
|
+
console.log(` Interrupt Rate: ${fmt(metrics.interruptRate)}`);
|
|
588
|
+
console.log(` Autonomous Completion Rate: ${fmt(metrics.autonomousCompletionRate)}`);
|
|
589
|
+
console.log(` Context Coverage Score: ${fmt(metrics.contextCoverageScore)}`);
|
|
590
|
+
console.log(` MTTC: ${metrics.meanTimeToCorrectHours != null ? `${metrics.meanTimeToCorrectHours}h` : "—"}`);
|
|
591
|
+
console.log(` Feedback-to-Demo: ${metrics.feedbackToDemoCycleTimeHours != null ? `${metrics.feedbackToDemoCycleTimeHours}h` : "—"}`);
|
|
592
|
+
} catch {}
|
|
593
|
+
}
|
|
594
|
+
function fmt(value) {
|
|
595
|
+
if (value == null) return "—";
|
|
596
|
+
return String(value);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/continue.ts
|
|
601
|
+
function continueTownAgent(argv = process.argv.slice(2)) {
|
|
602
|
+
const repo = resolveRepoContext(argv);
|
|
603
|
+
const [agentArg, ...messageParts] = repo.rest;
|
|
604
|
+
if (!agentArg) throw new Error("Usage: pitown continue [--repo <path>] <agent> [\"message\"]");
|
|
605
|
+
const agentId = normalizeAgentId(agentArg);
|
|
606
|
+
assertCommandAvailable("pi");
|
|
607
|
+
const resolved = resolveAgentSession(agentId, repo.artifactsDir);
|
|
608
|
+
writeAgentState(repo.artifactsDir, {
|
|
609
|
+
...resolved.state,
|
|
610
|
+
session: resolved.session
|
|
611
|
+
});
|
|
612
|
+
const message = messageParts.join(" ").trim();
|
|
613
|
+
const args = createPiTownRuntimeArgs({
|
|
614
|
+
agentId,
|
|
615
|
+
sessionPath: resolved.session.sessionPath,
|
|
616
|
+
message: message || null
|
|
617
|
+
});
|
|
618
|
+
console.log("[pitown] continue");
|
|
619
|
+
console.log(`- repo root: ${repo.repoRoot}`);
|
|
620
|
+
console.log(`- agent: ${agentId}`);
|
|
621
|
+
console.log(`- session: ${resolved.session.sessionPath}`);
|
|
622
|
+
if (message) console.log(`- message: ${message}`);
|
|
623
|
+
const exitCode = runCommandInteractive("pi", args, {
|
|
624
|
+
cwd: repo.repoRoot,
|
|
625
|
+
env: process.env
|
|
626
|
+
});
|
|
627
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/delegate.ts
|
|
632
|
+
function parseDelegateFlags(argv) {
|
|
633
|
+
let from = "mayor";
|
|
634
|
+
let role = "worker";
|
|
635
|
+
let agentId = null;
|
|
636
|
+
let task = null;
|
|
637
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
638
|
+
const arg = argv[index];
|
|
639
|
+
if (arg === void 0) continue;
|
|
640
|
+
if (arg.startsWith("--from=")) {
|
|
641
|
+
from = arg.slice(7);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (arg === "--from") {
|
|
645
|
+
from = argv[index + 1] ?? from;
|
|
646
|
+
index += 1;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (arg.startsWith("--role=")) {
|
|
650
|
+
role = arg.slice(7);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (arg === "--role") {
|
|
654
|
+
role = argv[index + 1] ?? role;
|
|
655
|
+
index += 1;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (arg.startsWith("--agent=")) {
|
|
659
|
+
agentId = arg.slice(8);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (arg === "--agent") {
|
|
663
|
+
agentId = argv[index + 1] ?? null;
|
|
664
|
+
index += 1;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (arg.startsWith("--task=")) {
|
|
668
|
+
task = arg.slice(7);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (arg === "--task") {
|
|
672
|
+
task = argv[index + 1] ?? null;
|
|
673
|
+
index += 1;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
677
|
+
}
|
|
678
|
+
if (!task) throw new Error("Usage: pitown delegate [--repo <path>] [--from <agent>] [--role <role>] [--agent <id>] --task <text>");
|
|
679
|
+
return {
|
|
680
|
+
from,
|
|
681
|
+
role,
|
|
682
|
+
agentId,
|
|
683
|
+
task
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function delegateTownTask(argv = process.argv.slice(2)) {
|
|
687
|
+
const repo = resolveRepoContext(argv);
|
|
688
|
+
const flags = parseDelegateFlags(repo.rest);
|
|
689
|
+
const fromAgentId = normalizeAgentId(flags.from);
|
|
690
|
+
if (readAgentState(repo.artifactsDir, fromAgentId) === null) throw new Error(`Unknown delegating agent: ${fromAgentId}`);
|
|
691
|
+
const { agentId, latestSession, launch, task } = delegateTask({
|
|
692
|
+
repoRoot: repo.repoRoot,
|
|
693
|
+
artifactsDir: repo.artifactsDir,
|
|
694
|
+
fromAgentId,
|
|
695
|
+
role: flags.role,
|
|
696
|
+
agentId: flags.agentId,
|
|
697
|
+
task: flags.task,
|
|
698
|
+
extensionPath: resolvePiTownExtensionPath()
|
|
699
|
+
});
|
|
700
|
+
console.log("[pitown] delegate");
|
|
701
|
+
console.log(`- repo root: ${repo.repoRoot}`);
|
|
702
|
+
console.log(`- from: ${fromAgentId}`);
|
|
703
|
+
console.log(`- task id: ${task.taskId}`);
|
|
704
|
+
console.log(`- agent: ${agentId}`);
|
|
705
|
+
console.log(`- role: ${flags.role}`);
|
|
706
|
+
console.log(`- status: ${task.status}`);
|
|
707
|
+
console.log(`- launch pid: ${launch.processId}`);
|
|
708
|
+
if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
|
|
709
|
+
else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/loop-config.ts
|
|
714
|
+
const DEFAULT_GOAL = "continue from current scaffold state";
|
|
715
|
+
const DEFAULT_MAX_ITERATIONS = 10;
|
|
716
|
+
const DEFAULT_MAX_TIME_MINUTES = 60;
|
|
717
|
+
function expandHome(value) {
|
|
718
|
+
if (value === "~") return homedir();
|
|
719
|
+
if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
|
|
720
|
+
return value;
|
|
721
|
+
}
|
|
722
|
+
function resolvePathValue(value, baseDir) {
|
|
723
|
+
if (!value) return void 0;
|
|
724
|
+
const expanded = expandHome(value);
|
|
725
|
+
return isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded);
|
|
726
|
+
}
|
|
727
|
+
function parseLoopCliFlags(argv) {
|
|
728
|
+
const flags = { noStopOnFailure: false };
|
|
729
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
730
|
+
const arg = argv[index];
|
|
731
|
+
if (arg === void 0) continue;
|
|
732
|
+
if (arg.startsWith("--repo=")) {
|
|
733
|
+
flags.repo = arg.slice(7);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (arg === "--repo") {
|
|
737
|
+
const value = argv[index + 1];
|
|
738
|
+
if (!value) throw new Error("Missing value for --repo");
|
|
739
|
+
flags.repo = value;
|
|
740
|
+
index += 1;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (arg.startsWith("--plan=")) {
|
|
744
|
+
flags.plan = arg.slice(7);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (arg === "--plan") {
|
|
748
|
+
const value = argv[index + 1];
|
|
749
|
+
if (!value) throw new Error("Missing value for --plan");
|
|
750
|
+
flags.plan = value;
|
|
751
|
+
index += 1;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (arg.startsWith("--goal=")) {
|
|
755
|
+
flags.goal = arg.slice(7);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (arg === "--goal") {
|
|
759
|
+
const value = argv[index + 1];
|
|
760
|
+
if (!value) throw new Error("Missing value for --goal");
|
|
761
|
+
flags.goal = value;
|
|
762
|
+
index += 1;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (arg.startsWith("--max-iterations=")) {
|
|
766
|
+
flags.maxIterations = Number.parseInt(arg.slice(17), 10);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (arg === "--max-iterations") {
|
|
770
|
+
const value = argv[index + 1];
|
|
771
|
+
if (!value) throw new Error("Missing value for --max-iterations");
|
|
772
|
+
flags.maxIterations = Number.parseInt(value, 10);
|
|
773
|
+
index += 1;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (arg.startsWith("--max-time=")) {
|
|
777
|
+
flags.maxTime = Number.parseInt(arg.slice(11), 10);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (arg === "--max-time") {
|
|
781
|
+
const value = argv[index + 1];
|
|
782
|
+
if (!value) throw new Error("Missing value for --max-time");
|
|
783
|
+
flags.maxTime = Number.parseInt(value, 10);
|
|
784
|
+
index += 1;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (arg === "--no-stop-on-failure") {
|
|
788
|
+
flags.noStopOnFailure = true;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
792
|
+
}
|
|
793
|
+
return flags;
|
|
794
|
+
}
|
|
795
|
+
function loadUserConfig() {
|
|
796
|
+
const configPath = getUserConfigPath();
|
|
797
|
+
if (!existsSync(configPath)) return {};
|
|
798
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
799
|
+
}
|
|
800
|
+
function resolveLoopConfig(argv) {
|
|
801
|
+
const flags = parseLoopCliFlags(argv);
|
|
802
|
+
const configPath = getUserConfigPath();
|
|
803
|
+
const userConfig = loadUserConfig();
|
|
804
|
+
const configDir = dirname(configPath);
|
|
805
|
+
return {
|
|
806
|
+
repo: resolvePathValue(flags.repo, process.cwd()) ?? resolvePathValue(userConfig.repo, configDir) ?? resolve(process.cwd()),
|
|
807
|
+
plan: resolvePathValue(flags.plan, process.cwd()) ?? resolvePathValue(userConfig.plan, configDir) ?? null,
|
|
808
|
+
goal: flags.goal ?? userConfig.goal ?? DEFAULT_GOAL,
|
|
809
|
+
maxIterations: flags.maxIterations ?? DEFAULT_MAX_ITERATIONS,
|
|
810
|
+
maxTimeMinutes: flags.maxTime ?? DEFAULT_MAX_TIME_MINUTES,
|
|
811
|
+
stopOnPiFailure: !flags.noStopOnFailure
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/loop.ts
|
|
817
|
+
function assertDirectory(path, label) {
|
|
818
|
+
if (!existsSync(path)) throw new Error(`${label} does not exist: ${path}`);
|
|
819
|
+
if (!statSync(path).isDirectory()) throw new Error(`${label} is not a directory: ${path}`);
|
|
820
|
+
}
|
|
821
|
+
function formatMs(ms) {
|
|
822
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
823
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
824
|
+
}
|
|
825
|
+
function loopTown(argv = process.argv.slice(2)) {
|
|
826
|
+
const config = resolveLoopConfig(argv);
|
|
827
|
+
assertDirectory(config.repo, "Target repo");
|
|
828
|
+
if (config.plan) assertDirectory(config.plan, "Plan path");
|
|
829
|
+
mkdirSync(getTownHomeDir(), { recursive: true });
|
|
830
|
+
const repoRoot = getRepoRoot(config.repo);
|
|
831
|
+
const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
|
|
832
|
+
const recommendedPlanDir = config.plan ? null : getRecommendedPlanDir(repoSlug);
|
|
833
|
+
const artifactsDir = getRepoArtifactsDir(repoSlug);
|
|
834
|
+
console.log(`[pitown-loop] starting loop (max ${config.maxIterations} iterations, ${config.maxTimeMinutes}min wall time)`);
|
|
835
|
+
const result = runLoop({
|
|
836
|
+
runOptions: {
|
|
837
|
+
artifactsDir,
|
|
838
|
+
cwd: repoRoot,
|
|
839
|
+
goal: config.goal,
|
|
840
|
+
mode: "single-pi",
|
|
841
|
+
planPath: config.plan,
|
|
842
|
+
recommendedPlanDir,
|
|
843
|
+
appendedSystemPrompt: readPiTownMayorPrompt(),
|
|
844
|
+
extensionPath: resolvePiTownExtensionPath()
|
|
845
|
+
},
|
|
846
|
+
maxIterations: config.maxIterations,
|
|
847
|
+
maxWallTimeMs: config.maxTimeMinutes * 6e4,
|
|
848
|
+
stopOnPiFailure: config.stopOnPiFailure,
|
|
849
|
+
onIterationComplete(iteration) {
|
|
850
|
+
const board = iteration.boardSnapshot;
|
|
851
|
+
const taskSummary = board.tasks.length > 0 ? `${board.tasks.length} tasks (${board.tasks.filter((t) => t.status === "completed").length} completed, ${board.tasks.filter((t) => t.status === "running").length} running)` : "no tasks tracked";
|
|
852
|
+
const mayorStatus = board.agents.find((a) => a.agentId === "mayor")?.status ?? "unknown";
|
|
853
|
+
console.log(`[pitown-loop] iteration ${iteration.iteration}/${config.maxIterations} completed (${formatMs(iteration.elapsedMs)})`);
|
|
854
|
+
console.log(` - pi exit code: ${iteration.controllerResult.piInvocation.exitCode}`);
|
|
855
|
+
console.log(` - run: ${iteration.controllerResult.runId}`);
|
|
856
|
+
console.log(` - board: ${taskSummary}, mayor ${mayorStatus}`);
|
|
857
|
+
console.log(` - metrics: interrupt rate ${iteration.metrics.interruptRate}, autonomous completion ${iteration.metrics.autonomousCompletionRate}`);
|
|
858
|
+
if (iteration.stopReason) console.log(` - stopping: ${iteration.stopReason}`);
|
|
859
|
+
else console.log(` - continuing: ${iteration.continueReason}`);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
console.log(`[pitown-loop] stopped after ${result.totalIterations} iteration${result.totalIterations === 1 ? "" : "s"} (${formatMs(result.totalElapsedMs)} total)`);
|
|
863
|
+
console.log(` - reason: ${result.stopReason}`);
|
|
864
|
+
console.log(` - aggregate metrics: interrupt rate ${result.aggregateMetrics.interruptRate}, autonomous completion ${result.aggregateMetrics.autonomousCompletionRate}`);
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
//#endregion
|
|
869
|
+
//#region src/mayor.ts
|
|
870
|
+
function startFreshMayorSession(repoRoot, artifactsDir) {
|
|
871
|
+
const sessionDir = getAgentSessionsDir(artifactsDir, "mayor");
|
|
872
|
+
writeAgentState(artifactsDir, createAgentState({
|
|
873
|
+
agentId: "mayor",
|
|
874
|
+
role: "mayor",
|
|
875
|
+
status: "running",
|
|
876
|
+
task: "open the mayor session and plan the next steps for this repository",
|
|
877
|
+
lastMessage: "Mayor session opened",
|
|
878
|
+
session: createAgentSessionRecord({ sessionDir })
|
|
879
|
+
}));
|
|
880
|
+
console.log("[pitown] mayor");
|
|
881
|
+
console.log(`- repo root: ${repoRoot}`);
|
|
882
|
+
console.log("- starting a new mayor session");
|
|
883
|
+
const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
|
|
884
|
+
agentId: "mayor",
|
|
885
|
+
sessionDir
|
|
886
|
+
}), {
|
|
887
|
+
cwd: repoRoot,
|
|
888
|
+
env: process.env
|
|
889
|
+
});
|
|
890
|
+
const latestSession = getLatestAgentSession(artifactsDir, "mayor");
|
|
891
|
+
const previousState = readAgentState(artifactsDir, "mayor");
|
|
892
|
+
if (previousState !== null) writeAgentState(artifactsDir, createAgentState({
|
|
893
|
+
...previousState,
|
|
894
|
+
status: exitCode === 0 ? "idle" : "blocked",
|
|
895
|
+
lastMessage: exitCode === 0 ? "Mayor session closed" : `Mayor session exited with code ${exitCode}`,
|
|
896
|
+
blocked: exitCode !== 0,
|
|
897
|
+
waitingOn: exitCode === 0 ? null : "human-or-follow-up-run",
|
|
898
|
+
session: createAgentSessionRecord({
|
|
899
|
+
sessionDir: latestSession.sessionDir,
|
|
900
|
+
sessionId: latestSession.sessionId,
|
|
901
|
+
sessionPath: latestSession.sessionPath
|
|
902
|
+
})
|
|
903
|
+
}));
|
|
904
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
905
|
+
}
|
|
906
|
+
function openTownMayor(argv = process.argv.slice(2)) {
|
|
907
|
+
const repo = resolveRepoContext(argv);
|
|
908
|
+
const message = repo.rest.join(" ").trim();
|
|
909
|
+
if (readAgentState(repo.artifactsDir, "mayor") === null) {
|
|
910
|
+
assertCommandAvailable("pi");
|
|
911
|
+
if (message) {
|
|
912
|
+
runTown([
|
|
913
|
+
"--repo",
|
|
914
|
+
repo.repoRoot,
|
|
915
|
+
"--goal",
|
|
916
|
+
message
|
|
917
|
+
]);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
startFreshMayorSession(repo.repoRoot, repo.artifactsDir);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (message) {
|
|
924
|
+
continueTownAgent([
|
|
925
|
+
"--repo",
|
|
926
|
+
repo.repoRoot,
|
|
927
|
+
"mayor",
|
|
928
|
+
message
|
|
929
|
+
]);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
attachTownAgent([
|
|
933
|
+
"--repo",
|
|
934
|
+
repo.repoRoot,
|
|
935
|
+
"mayor"
|
|
936
|
+
]);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region src/msg.ts
|
|
941
|
+
function messageTownAgent(argv = process.argv.slice(2)) {
|
|
942
|
+
const repo = resolveRepoContext(argv);
|
|
943
|
+
const [agentArg, ...messageParts] = repo.rest;
|
|
944
|
+
if (!agentArg || messageParts.length === 0) throw new Error("Usage: pitown msg [--repo <path>] <agent> \"message\"");
|
|
945
|
+
const agentId = normalizeAgentId(agentArg);
|
|
946
|
+
const state = readAgentState(repo.artifactsDir, agentId);
|
|
947
|
+
if (state === null) throw new Error(`Unknown agent: ${agentId}`);
|
|
948
|
+
const body = messageParts.join(" ").trim();
|
|
949
|
+
queueAgentMessage({
|
|
950
|
+
artifactsDir: repo.artifactsDir,
|
|
951
|
+
agentId,
|
|
952
|
+
from: "human",
|
|
953
|
+
body
|
|
954
|
+
});
|
|
955
|
+
const deliveredResult = state.role === "mayor" ? runAgentTurn({
|
|
956
|
+
repoRoot: repo.repoRoot,
|
|
957
|
+
artifactsDir: repo.artifactsDir,
|
|
958
|
+
agentId,
|
|
959
|
+
message: body,
|
|
960
|
+
from: "human",
|
|
961
|
+
runtimeArgs: createPiTownRuntimeArgs({
|
|
962
|
+
agentId,
|
|
963
|
+
sessionPath: state.session.sessionPath,
|
|
964
|
+
prompt: body
|
|
965
|
+
})
|
|
966
|
+
}) : null;
|
|
967
|
+
console.log("[pitown] msg");
|
|
968
|
+
console.log(`- repo root: ${repo.repoRoot}`);
|
|
969
|
+
console.log(`- agent: ${agentId}`);
|
|
970
|
+
console.log(`- queued message: ${body}`);
|
|
971
|
+
if (deliveredResult) {
|
|
972
|
+
console.log(`- delivered to session: ${deliveredResult.latestSession.sessionPath}`);
|
|
973
|
+
console.log(`- mayor response: ${deliveredResult.completionMessage}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
//#endregion
|
|
978
|
+
//#region src/peek.ts
|
|
979
|
+
function printMessages(label, lines) {
|
|
980
|
+
console.log(`- ${label}:`);
|
|
981
|
+
if (lines.length === 0) {
|
|
982
|
+
console.log(" (empty)");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
for (const line of lines) console.log(` ${line.createdAt} ${line.from}: ${line.body}`);
|
|
986
|
+
}
|
|
987
|
+
function peekTownAgent(argv = process.argv.slice(2)) {
|
|
988
|
+
const repo = resolveRepoContext(argv);
|
|
989
|
+
const [agentArg] = repo.rest;
|
|
990
|
+
const agentId = normalizeAgentId(agentArg ?? "mayor");
|
|
991
|
+
const state = readAgentState(repo.artifactsDir, agentId);
|
|
992
|
+
if (state === null) throw new Error(`Unknown agent: ${agentId}`);
|
|
993
|
+
console.log("[pitown] peek");
|
|
994
|
+
console.log(`- repo root: ${repo.repoRoot}`);
|
|
995
|
+
console.log(`- agent: ${state.agentId}`);
|
|
996
|
+
console.log(`- role: ${state.role}`);
|
|
997
|
+
console.log(`- status: ${state.status}`);
|
|
998
|
+
if (state.taskId) console.log(`- task id: ${state.taskId}`);
|
|
999
|
+
if (state.task) console.log(`- task: ${state.task}`);
|
|
1000
|
+
if (state.branch) console.log(`- branch: ${state.branch}`);
|
|
1001
|
+
console.log(`- blocked: ${state.blocked}`);
|
|
1002
|
+
if (state.waitingOn) console.log(`- waiting on: ${state.waitingOn}`);
|
|
1003
|
+
if (state.lastMessage) console.log(`- last message: ${state.lastMessage}`);
|
|
1004
|
+
if (state.session.sessionId) console.log(`- session id: ${state.session.sessionId}`);
|
|
1005
|
+
if (state.session.sessionPath) console.log(`- session path: ${state.session.sessionPath}`);
|
|
1006
|
+
console.log(`- updated at: ${state.updatedAt}`);
|
|
1007
|
+
printMessages("recent inbox", readAgentMessages(repo.artifactsDir, agentId, "inbox").slice(-5));
|
|
1008
|
+
printMessages("recent outbox", readAgentMessages(repo.artifactsDir, agentId, "outbox").slice(-5));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
//#endregion
|
|
1012
|
+
//#region src/spawn.ts
|
|
1013
|
+
function parseSpawnFlags(argv) {
|
|
1014
|
+
let role = null;
|
|
1015
|
+
let agentId = null;
|
|
1016
|
+
let task = null;
|
|
1017
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1018
|
+
const arg = argv[index];
|
|
1019
|
+
if (arg === void 0) continue;
|
|
1020
|
+
if (arg.startsWith("--role=")) {
|
|
1021
|
+
role = arg.slice(7);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (arg === "--role") {
|
|
1025
|
+
role = argv[index + 1] ?? null;
|
|
1026
|
+
index += 1;
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (arg.startsWith("--agent=")) {
|
|
1030
|
+
agentId = arg.slice(8);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (arg === "--agent") {
|
|
1034
|
+
agentId = argv[index + 1] ?? null;
|
|
1035
|
+
index += 1;
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (arg.startsWith("--task=")) {
|
|
1039
|
+
task = arg.slice(7);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (arg === "--task") {
|
|
1043
|
+
task = argv[index + 1] ?? null;
|
|
1044
|
+
index += 1;
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1048
|
+
}
|
|
1049
|
+
if (!role) throw new Error("Usage: pitown spawn [--repo <path>] --role <role> [--agent <id>] [--task <text>]");
|
|
1050
|
+
return {
|
|
1051
|
+
role,
|
|
1052
|
+
agentId,
|
|
1053
|
+
task
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function spawnAgent(options) {
|
|
1057
|
+
return spawnAgentRun(options);
|
|
1058
|
+
}
|
|
1059
|
+
function spawnTownAgent(argv = process.argv.slice(2)) {
|
|
1060
|
+
const repo = resolveRepoContext(argv);
|
|
1061
|
+
const flags = parseSpawnFlags(repo.rest);
|
|
1062
|
+
const agentId = flags.agentId ?? `${flags.role}-${Date.now()}`;
|
|
1063
|
+
const task = flags.task;
|
|
1064
|
+
const { launch, latestSession } = spawnAgent({
|
|
1065
|
+
repoRoot: repo.repoRoot,
|
|
1066
|
+
artifactsDir: repo.artifactsDir,
|
|
1067
|
+
role: flags.role,
|
|
1068
|
+
agentId,
|
|
1069
|
+
task,
|
|
1070
|
+
extensionPath: resolvePiTownExtensionPath()
|
|
1071
|
+
});
|
|
1072
|
+
console.log("[pitown] spawn");
|
|
1073
|
+
console.log(`- repo root: ${repo.repoRoot}`);
|
|
1074
|
+
console.log(`- agent: ${agentId}`);
|
|
1075
|
+
console.log(`- role: ${flags.role}`);
|
|
1076
|
+
console.log(`- status: running`);
|
|
1077
|
+
console.log(`- launch pid: ${launch.processId}`);
|
|
1078
|
+
if (task) console.log(`- task: ${task}`);
|
|
1079
|
+
if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
|
|
1080
|
+
else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
//#endregion
|
|
1084
|
+
//#region src/stop.ts
|
|
1085
|
+
function parseStopFlags(argv) {
|
|
1086
|
+
let all = false;
|
|
1087
|
+
let agentId = null;
|
|
1088
|
+
let force = false;
|
|
1089
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1090
|
+
const arg = argv[index];
|
|
1091
|
+
if (arg === void 0) continue;
|
|
1092
|
+
if (arg === "--all") {
|
|
1093
|
+
all = true;
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (arg === "--force") {
|
|
1097
|
+
force = true;
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (arg.startsWith("--agent=")) {
|
|
1101
|
+
agentId = normalizeAgentId(arg.slice(8));
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (arg === "--agent") {
|
|
1105
|
+
const value = argv[index + 1];
|
|
1106
|
+
if (!value) throw new Error("Missing value for --agent");
|
|
1107
|
+
agentId = normalizeAgentId(value);
|
|
1108
|
+
index += 1;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
all,
|
|
1115
|
+
agentId,
|
|
1116
|
+
force
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function stopRepo(repoRoot, artifactsDir, flags) {
|
|
1120
|
+
const repoId = getRepoIdentity(repoRoot);
|
|
1121
|
+
const leaseResult = flags.agentId ? { signaledProcesses: 0 } : stopRepoLeases({
|
|
1122
|
+
repoId,
|
|
1123
|
+
force: flags.force
|
|
1124
|
+
});
|
|
1125
|
+
const agentResult = stopManagedAgents({
|
|
1126
|
+
artifactsDir,
|
|
1127
|
+
agentId: flags.agentId,
|
|
1128
|
+
actorId: "human",
|
|
1129
|
+
reason: flags.agentId ? `Stopped ${flags.agentId} via pitown stop` : "Stopped via pitown stop",
|
|
1130
|
+
force: flags.force
|
|
1131
|
+
});
|
|
1132
|
+
return {
|
|
1133
|
+
repoLabel: repoRoot,
|
|
1134
|
+
stoppedAgents: agentResult.stoppedAgents,
|
|
1135
|
+
signaledAgentProcesses: agentResult.signaledProcesses,
|
|
1136
|
+
signaledLeaseProcesses: leaseResult.signaledProcesses
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function listTrackedArtifactsDirs() {
|
|
1140
|
+
const reposRoot = getReposRootDir();
|
|
1141
|
+
if (!existsSync(reposRoot)) return [];
|
|
1142
|
+
return readdirSync(reposRoot).map((entry) => getRepoArtifactsDir(entry)).filter((path) => existsSync(path));
|
|
1143
|
+
}
|
|
1144
|
+
function stopTown(argv = process.argv.slice(2)) {
|
|
1145
|
+
const { repo, rest } = parseOptionalRepoFlag(argv);
|
|
1146
|
+
const flags = parseStopFlags(rest);
|
|
1147
|
+
if (flags.all && repo) throw new Error("Do not combine --all with --repo");
|
|
1148
|
+
if (flags.all && flags.agentId) throw new Error("Do not combine --all with --agent");
|
|
1149
|
+
if (flags.all) {
|
|
1150
|
+
const repoSummaries = listTrackedArtifactsDirs().map((artifactsDir) => {
|
|
1151
|
+
const result = stopManagedAgents({
|
|
1152
|
+
artifactsDir,
|
|
1153
|
+
actorId: "human",
|
|
1154
|
+
reason: "Stopped via pitown stop --all",
|
|
1155
|
+
force: flags.force
|
|
1156
|
+
});
|
|
1157
|
+
return {
|
|
1158
|
+
repoLabel: artifactsDir,
|
|
1159
|
+
stoppedAgents: result.stoppedAgents,
|
|
1160
|
+
signaledAgentProcesses: result.signaledProcesses,
|
|
1161
|
+
signaledLeaseProcesses: 0
|
|
1162
|
+
};
|
|
1163
|
+
});
|
|
1164
|
+
const leaseResult = stopRepoLeases({ force: flags.force });
|
|
1165
|
+
const totalAgents = repoSummaries.reduce((sum, result) => sum + result.stoppedAgents, 0);
|
|
1166
|
+
const totalAgentProcesses = repoSummaries.reduce((sum, result) => sum + result.signaledAgentProcesses, 0);
|
|
1167
|
+
const totalLeaseProcesses = leaseResult.signaledProcesses + repoSummaries.reduce((sum, result) => sum + result.signaledLeaseProcesses, 0);
|
|
1168
|
+
console.log("[pitown] stop");
|
|
1169
|
+
console.log("- scope: all repos");
|
|
1170
|
+
console.log(`- stopped agents: ${totalAgents}`);
|
|
1171
|
+
console.log(`- signaled agent processes: ${totalAgentProcesses}`);
|
|
1172
|
+
console.log(`- signaled lease processes: ${totalLeaseProcesses}`);
|
|
1173
|
+
if (repoSummaries.length === 0 && leaseResult.results.length === 0) console.log("- nothing was running");
|
|
1174
|
+
return repoSummaries;
|
|
1175
|
+
}
|
|
1176
|
+
const resolved = repo ? resolveRepoContext(["--repo", repo]) : resolveRepoContext([]);
|
|
1177
|
+
const result = stopRepo(resolved.repoRoot, resolved.artifactsDir, flags);
|
|
1178
|
+
console.log("[pitown] stop");
|
|
1179
|
+
console.log(`- repo root: ${result.repoLabel}`);
|
|
1180
|
+
if (flags.agentId) console.log(`- agent: ${flags.agentId}`);
|
|
1181
|
+
console.log(`- stopped agents: ${result.stoppedAgents}`);
|
|
1182
|
+
console.log(`- signaled agent processes: ${result.signaledAgentProcesses}`);
|
|
1183
|
+
if (!flags.agentId) console.log(`- signaled lease processes: ${result.signaledLeaseProcesses}`);
|
|
1184
|
+
if (result.stoppedAgents === 0 && result.signaledAgentProcesses === 0 && result.signaledLeaseProcesses === 0) console.log("- nothing was running");
|
|
1185
|
+
return [result];
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
//#endregion
|
|
1189
|
+
//#region src/version.ts
|
|
1190
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1191
|
+
const CLI_VERSION = packageJson.version;
|
|
1192
|
+
|
|
1193
|
+
//#endregion
|
|
7
1194
|
//#region src/index.ts
|
|
8
|
-
function printHelp() {
|
|
1195
|
+
function printHelp(showAdvanced = false) {
|
|
9
1196
|
console.log([
|
|
10
1197
|
"pitown",
|
|
11
1198
|
"",
|
|
12
1199
|
"Usage:",
|
|
13
|
-
" pitown
|
|
1200
|
+
" pitown [--repo <path>] [\"message\"]",
|
|
1201
|
+
" pitown board [--repo <path>]",
|
|
1202
|
+
" pitown peek [--repo <path>] [agent]",
|
|
1203
|
+
" pitown msg [--repo <path>] <agent> \"message\"",
|
|
14
1204
|
" pitown status [--repo <path>]",
|
|
15
|
-
" pitown
|
|
16
|
-
" pitown
|
|
17
|
-
" pitown --
|
|
1205
|
+
" pitown stop [--repo <path>] [--agent <id>] [--all] [--force]",
|
|
1206
|
+
" pitown doctor",
|
|
1207
|
+
" pitown --version",
|
|
1208
|
+
"",
|
|
1209
|
+
"Mayor workflow:",
|
|
1210
|
+
" pitown",
|
|
1211
|
+
" pitown \"plan the next milestones\"",
|
|
1212
|
+
" /plan",
|
|
1213
|
+
" /todos",
|
|
1214
|
+
"",
|
|
1215
|
+
"Inside the mayor session, `/plan` toggles read-only planning mode and `/todos` shows the captured plan.",
|
|
1216
|
+
"Aliases still work: `pitown mayor`, `pitown help`, `pitown --help`, `pitown -v`.",
|
|
1217
|
+
"",
|
|
1218
|
+
"If --repo is omitted, Pi Town uses the repo for the current working directory when possible.",
|
|
1219
|
+
...showAdvanced ? [
|
|
1220
|
+
"",
|
|
1221
|
+
"Advanced commands:",
|
|
1222
|
+
" pitown run [--repo <path>] [--plan <path>] [--goal <text>]",
|
|
1223
|
+
" pitown loop [--repo <path>] [--plan <path>] [--goal <text>] [--max-iterations N] [--max-time M] [--no-stop-on-failure]",
|
|
1224
|
+
" pitown attach [--repo <path>] <agent>",
|
|
1225
|
+
" pitown continue [--repo <path>] <agent> [\"message\"]",
|
|
1226
|
+
" pitown delegate [--repo <path>] [--from <agent>] [--role <role>] [--agent <id>] --task <text>",
|
|
1227
|
+
" pitown spawn [--repo <path>] --role <role> [--agent <id>] [--task <text>]",
|
|
1228
|
+
" pitown watch [--repo <path>]"
|
|
1229
|
+
] : []
|
|
18
1230
|
].join("\n"));
|
|
19
1231
|
}
|
|
20
1232
|
function runCli(argv = process.argv.slice(2)) {
|
|
21
1233
|
const [command, ...args] = argv;
|
|
1234
|
+
const showAdvancedHelp = args.includes("--all");
|
|
22
1235
|
switch (command) {
|
|
23
1236
|
case void 0:
|
|
1237
|
+
openTownMayor([]);
|
|
1238
|
+
break;
|
|
24
1239
|
case "help":
|
|
25
1240
|
case "--help":
|
|
26
1241
|
case "-h":
|
|
27
|
-
printHelp();
|
|
1242
|
+
printHelp(showAdvancedHelp);
|
|
1243
|
+
break;
|
|
1244
|
+
case "-v":
|
|
1245
|
+
case "--version":
|
|
1246
|
+
case "version":
|
|
1247
|
+
console.log(CLI_VERSION);
|
|
28
1248
|
break;
|
|
29
1249
|
case "run": {
|
|
30
1250
|
const result = runTown(args);
|
|
31
|
-
|
|
1251
|
+
const latestIteration = result.iterations[result.iterations.length - 1];
|
|
1252
|
+
if (latestIteration && latestIteration.controllerResult.piInvocation.exitCode !== 0) process.exitCode = latestIteration.controllerResult.piInvocation.exitCode;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
case "loop": {
|
|
1256
|
+
const result = loopTown(args);
|
|
1257
|
+
const lastIteration = result.iterations[result.iterations.length - 1];
|
|
1258
|
+
if (lastIteration && lastIteration.controllerResult.piInvocation.exitCode !== 0) process.exitCode = lastIteration.controllerResult.piInvocation.exitCode;
|
|
32
1259
|
break;
|
|
33
1260
|
}
|
|
1261
|
+
case "attach":
|
|
1262
|
+
attachTownAgent(args);
|
|
1263
|
+
break;
|
|
1264
|
+
case "board":
|
|
1265
|
+
showTownBoard(args);
|
|
1266
|
+
break;
|
|
1267
|
+
case "continue":
|
|
1268
|
+
continueTownAgent(args);
|
|
1269
|
+
break;
|
|
1270
|
+
case "delegate":
|
|
1271
|
+
delegateTownTask(args);
|
|
1272
|
+
break;
|
|
1273
|
+
case "mayor":
|
|
1274
|
+
openTownMayor(args);
|
|
1275
|
+
break;
|
|
1276
|
+
case "msg":
|
|
1277
|
+
messageTownAgent(args);
|
|
1278
|
+
break;
|
|
1279
|
+
case "peek":
|
|
1280
|
+
peekTownAgent(args);
|
|
1281
|
+
break;
|
|
1282
|
+
case "spawn":
|
|
1283
|
+
spawnTownAgent(args);
|
|
1284
|
+
break;
|
|
34
1285
|
case "status":
|
|
35
1286
|
showTownStatus(args);
|
|
36
1287
|
break;
|
|
1288
|
+
case "stop":
|
|
1289
|
+
stopTown(args);
|
|
1290
|
+
break;
|
|
37
1291
|
case "watch":
|
|
38
1292
|
watchTown(args);
|
|
39
1293
|
break;
|
|
1294
|
+
case "doctor":
|
|
1295
|
+
if (!runDoctor().ok) process.exitCode = 1;
|
|
1296
|
+
break;
|
|
40
1297
|
default:
|
|
41
1298
|
console.log(`Unknown command: ${command}`);
|
|
42
1299
|
printHelp();
|