@schilderlabs/pitown 0.2.1 → 0.2.7
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 +6 -0
- package/dist/{config-CUpe9o0x.mjs → config-BG1v4iIi.mjs} +6 -7
- package/dist/config-BG1v4iIi.mjs.map +1 -0
- package/dist/doctor.mjs +2 -2
- package/dist/{entrypoint-CyJDLudQ.mjs → entrypoint-WBAQmFbT.mjs} +1 -1
- package/dist/{entrypoint-CyJDLudQ.mjs.map → entrypoint-WBAQmFbT.mjs.map} +1 -1
- package/dist/index.mjs +443 -407
- package/dist/index.mjs.map +1 -1
- package/dist/{controller-9ihAZj3V.mjs → loop-CocC9qO1.mjs} +327 -174
- package/dist/loop-CocC9qO1.mjs.map +1 -0
- package/dist/{pi-C0fURZj7.mjs → pi-C7HRNjBG.mjs} +1 -1
- package/dist/{pi-C0fURZj7.mjs.map → pi-C7HRNjBG.mjs.map} +1 -1
- package/dist/repo-context-BuA2JqPm.mjs +45 -0
- package/dist/repo-context-BuA2JqPm.mjs.map +1 -0
- package/dist/run.d.mts +3 -72
- package/dist/run.mjs +38 -23
- package/dist/run.mjs.map +1 -1
- package/dist/status.mjs +2 -2
- 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 -17
- package/dist/watch.mjs.map +1 -1
- package/package.json +20 -24
- package/dist/config-CUpe9o0x.mjs.map +0 -1
- package/dist/controller-9ihAZj3V.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,293 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
4
|
-
import { a 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";
|
|
5
7
|
import { runDoctor } from "./doctor.mjs";
|
|
6
8
|
import { runTown } from "./run.mjs";
|
|
7
9
|
import { showTownStatus } from "./status.mjs";
|
|
8
10
|
import { watchTown } from "./watch.mjs";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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, hostname } from "node:os";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
12
17
|
import { readPiTownMayorPrompt, resolvePiTownExtensionPath } from "@schilderlabs/pitown-package";
|
|
13
18
|
|
|
14
|
-
//#region ../core/src/tasks.ts
|
|
15
|
-
function writeJson$1(path, value) {
|
|
16
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
17
|
-
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
18
|
-
}
|
|
19
|
-
function getTasksDir(artifactsDir) {
|
|
20
|
-
return join(artifactsDir, "tasks");
|
|
21
|
-
}
|
|
22
|
-
function getTaskPath(artifactsDir, taskId) {
|
|
23
|
-
return join(getTasksDir(artifactsDir), `${taskId}.json`);
|
|
24
|
-
}
|
|
25
|
-
function createTaskRecord(input) {
|
|
26
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
27
|
-
return {
|
|
28
|
-
taskId: input.taskId,
|
|
29
|
-
title: input.title,
|
|
30
|
-
status: input.status,
|
|
31
|
-
role: input.role,
|
|
32
|
-
assignedAgentId: input.assignedAgentId,
|
|
33
|
-
createdBy: input.createdBy,
|
|
34
|
-
createdAt: now,
|
|
35
|
-
updatedAt: now
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function writeTaskRecord(artifactsDir, task) {
|
|
39
|
-
writeJson$1(getTaskPath(artifactsDir, task.taskId), task);
|
|
40
|
-
}
|
|
41
|
-
function readTaskRecord(artifactsDir, taskId) {
|
|
42
|
-
const path = getTaskPath(artifactsDir, taskId);
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function listTaskRecords(artifactsDir) {
|
|
50
|
-
const tasksDir = getTasksDir(artifactsDir);
|
|
51
|
-
let entries;
|
|
52
|
-
try {
|
|
53
|
-
entries = readdirSync(tasksDir);
|
|
54
|
-
} catch {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
return entries.filter((entry) => entry.endsWith(".json")).map((entry) => readTaskRecord(artifactsDir, entry.replace(/\.json$/, ""))).filter((task) => task !== null).sort((left, right) => left.taskId.localeCompare(right.taskId));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region ../core/src/loop.ts
|
|
62
|
-
const DEFAULT_MAX_ITERATIONS$1 = 10;
|
|
63
|
-
const DEFAULT_MAX_WALL_TIME_MS = 36e5;
|
|
64
|
-
function createLoopId() {
|
|
65
|
-
return `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
66
|
-
}
|
|
67
|
-
function writeJson(path, value) {
|
|
68
|
-
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
69
|
-
}
|
|
70
|
-
function snapshotBoard(artifactsDir) {
|
|
71
|
-
const tasks = listTaskRecords(artifactsDir);
|
|
72
|
-
const agents = listAgentStates(artifactsDir);
|
|
73
|
-
return {
|
|
74
|
-
tasks: tasks.map((task) => ({
|
|
75
|
-
taskId: task.taskId,
|
|
76
|
-
status: task.status
|
|
77
|
-
})),
|
|
78
|
-
agents: agents.map((agent) => ({
|
|
79
|
-
agentId: agent.agentId,
|
|
80
|
-
status: agent.status,
|
|
81
|
-
blocked: agent.blocked
|
|
82
|
-
})),
|
|
83
|
-
allTasksCompleted: tasks.length > 0 && tasks.every((task) => task.status === "completed"),
|
|
84
|
-
allRemainingTasksBlocked: tasks.length > 0 && tasks.every((task) => task.status === "completed" || task.status === "blocked"),
|
|
85
|
-
leaderBlocked: agents.find((agent) => agent.agentId === "leader")?.blocked === true,
|
|
86
|
-
hasQueuedOrRunningWork: agents.some((agent) => agent.status === "queued" || agent.status === "running" || agent.status === "starting") || tasks.some((task) => task.status === "queued" || task.status === "running")
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function evaluateStopCondition(input) {
|
|
90
|
-
if (input.iteration >= input.maxIterations) return {
|
|
91
|
-
stopReason: "max-iterations-reached",
|
|
92
|
-
continueReason: null
|
|
93
|
-
};
|
|
94
|
-
if (input.elapsedMs >= input.maxWallTimeMs) return {
|
|
95
|
-
stopReason: "max-wall-time-reached",
|
|
96
|
-
continueReason: null
|
|
97
|
-
};
|
|
98
|
-
if (input.stopOnPiFailure && input.piExitCode !== 0) return {
|
|
99
|
-
stopReason: "pi-exit-nonzero",
|
|
100
|
-
continueReason: null
|
|
101
|
-
};
|
|
102
|
-
if (input.board.allTasksCompleted) return {
|
|
103
|
-
stopReason: "all-tasks-completed",
|
|
104
|
-
continueReason: null
|
|
105
|
-
};
|
|
106
|
-
if (input.board.leaderBlocked) return {
|
|
107
|
-
stopReason: "leader-blocked",
|
|
108
|
-
continueReason: null
|
|
109
|
-
};
|
|
110
|
-
if (input.board.allRemainingTasksBlocked) return {
|
|
111
|
-
stopReason: "all-remaining-tasks-blocked",
|
|
112
|
-
continueReason: null
|
|
113
|
-
};
|
|
114
|
-
if (input.interruptRateThreshold !== null && input.metrics.interruptRate > input.interruptRateThreshold) return {
|
|
115
|
-
stopReason: "high-interrupt-rate",
|
|
116
|
-
continueReason: null
|
|
117
|
-
};
|
|
118
|
-
const reasons = [];
|
|
119
|
-
if (input.board.hasQueuedOrRunningWork) reasons.push("queued or running work remains");
|
|
120
|
-
if (input.board.tasks.length === 0) reasons.push("no tasks tracked yet");
|
|
121
|
-
if (reasons.length === 0) reasons.push("leader idle, no stop condition met");
|
|
122
|
-
return {
|
|
123
|
-
stopReason: null,
|
|
124
|
-
continueReason: reasons.join("; ")
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function aggregateMetrics(iterations) {
|
|
128
|
-
if (iterations.length === 0) return computeMetrics({
|
|
129
|
-
taskAttempts: [],
|
|
130
|
-
interrupts: []
|
|
131
|
-
});
|
|
132
|
-
let totalTaskAttempts = 0;
|
|
133
|
-
let totalCompletedTasks = 0;
|
|
134
|
-
let totalInterrupts = 0;
|
|
135
|
-
let totalObservedInterruptCategories = 0;
|
|
136
|
-
let totalCoveredInterruptCategories = 0;
|
|
137
|
-
let interruptRateSum = 0;
|
|
138
|
-
let autonomousCompletionRateSum = 0;
|
|
139
|
-
let contextCoverageScoreSum = 0;
|
|
140
|
-
let mttcValues = [];
|
|
141
|
-
let ftdValues = [];
|
|
142
|
-
for (const iter of iterations) {
|
|
143
|
-
const m = iter.metrics;
|
|
144
|
-
totalTaskAttempts += m.totals.taskAttempts;
|
|
145
|
-
totalCompletedTasks += m.totals.completedTasks;
|
|
146
|
-
totalInterrupts += m.totals.interrupts;
|
|
147
|
-
totalObservedInterruptCategories += m.totals.observedInterruptCategories;
|
|
148
|
-
totalCoveredInterruptCategories += m.totals.coveredInterruptCategories;
|
|
149
|
-
interruptRateSum += m.interruptRate;
|
|
150
|
-
autonomousCompletionRateSum += m.autonomousCompletionRate;
|
|
151
|
-
contextCoverageScoreSum += m.contextCoverageScore;
|
|
152
|
-
if (m.meanTimeToCorrectHours !== null) mttcValues.push(m.meanTimeToCorrectHours);
|
|
153
|
-
if (m.feedbackToDemoCycleTimeHours !== null) ftdValues.push(m.feedbackToDemoCycleTimeHours);
|
|
154
|
-
}
|
|
155
|
-
const count = iterations.length;
|
|
156
|
-
const round = (v) => Math.round(v * 1e3) / 1e3;
|
|
157
|
-
const avg = (values) => values.length === 0 ? null : round(values.reduce((s, v) => s + v, 0) / values.length);
|
|
158
|
-
return {
|
|
159
|
-
interruptRate: round(interruptRateSum / count),
|
|
160
|
-
autonomousCompletionRate: round(autonomousCompletionRateSum / count),
|
|
161
|
-
contextCoverageScore: round(contextCoverageScoreSum / count),
|
|
162
|
-
meanTimeToCorrectHours: avg(mttcValues),
|
|
163
|
-
feedbackToDemoCycleTimeHours: avg(ftdValues),
|
|
164
|
-
totals: {
|
|
165
|
-
taskAttempts: totalTaskAttempts,
|
|
166
|
-
completedTasks: totalCompletedTasks,
|
|
167
|
-
interrupts: totalInterrupts,
|
|
168
|
-
observedInterruptCategories: totalObservedInterruptCategories,
|
|
169
|
-
coveredInterruptCategories: totalCoveredInterruptCategories
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
function runLoop(options) {
|
|
174
|
-
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS$1;
|
|
175
|
-
const maxWallTimeMs = options.maxWallTimeMs ?? DEFAULT_MAX_WALL_TIME_MS;
|
|
176
|
-
const stopOnPiFailure = options.stopOnPiFailure ?? true;
|
|
177
|
-
const interruptRateThreshold = options.interruptRateThreshold ?? null;
|
|
178
|
-
const loopId = createLoopId();
|
|
179
|
-
const artifactsDir = options.runOptions.artifactsDir;
|
|
180
|
-
const loopDir = join(artifactsDir, "loops", loopId);
|
|
181
|
-
mkdirSync(loopDir, { recursive: true });
|
|
182
|
-
const loopStartedAt = Date.now();
|
|
183
|
-
const iterations = [];
|
|
184
|
-
let finalStopReason = "max-iterations-reached";
|
|
185
|
-
appendJsonl(join(loopDir, "events.jsonl"), {
|
|
186
|
-
type: "loop_started",
|
|
187
|
-
loopId,
|
|
188
|
-
maxIterations,
|
|
189
|
-
maxWallTimeMs,
|
|
190
|
-
stopOnPiFailure,
|
|
191
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
192
|
-
});
|
|
193
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
194
|
-
const iterationStart = Date.now();
|
|
195
|
-
let controllerResult;
|
|
196
|
-
try {
|
|
197
|
-
controllerResult = runController(options.runOptions);
|
|
198
|
-
} catch (error) {
|
|
199
|
-
appendJsonl(join(loopDir, "events.jsonl"), {
|
|
200
|
-
type: "loop_iteration_error",
|
|
201
|
-
loopId,
|
|
202
|
-
iteration,
|
|
203
|
-
error: error.message,
|
|
204
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
205
|
-
});
|
|
206
|
-
finalStopReason = "pi-exit-nonzero";
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
const iterationElapsedMs = Date.now() - iterationStart;
|
|
210
|
-
const totalElapsedMs = Date.now() - loopStartedAt;
|
|
211
|
-
const board = snapshotBoard(artifactsDir);
|
|
212
|
-
const metrics = controllerResult.metrics;
|
|
213
|
-
const { stopReason, continueReason } = evaluateStopCondition({
|
|
214
|
-
iteration,
|
|
215
|
-
maxIterations,
|
|
216
|
-
elapsedMs: totalElapsedMs,
|
|
217
|
-
maxWallTimeMs,
|
|
218
|
-
piExitCode: controllerResult.piInvocation.exitCode,
|
|
219
|
-
stopOnPiFailure,
|
|
220
|
-
board,
|
|
221
|
-
metrics,
|
|
222
|
-
interruptRateThreshold
|
|
223
|
-
});
|
|
224
|
-
const iterationResult = {
|
|
225
|
-
iteration,
|
|
226
|
-
controllerResult,
|
|
227
|
-
boardSnapshot: board,
|
|
228
|
-
metrics,
|
|
229
|
-
elapsedMs: iterationElapsedMs,
|
|
230
|
-
continueReason,
|
|
231
|
-
stopReason
|
|
232
|
-
};
|
|
233
|
-
iterations.push(iterationResult);
|
|
234
|
-
writeJson(join(loopDir, `iteration-${iteration}.json`), {
|
|
235
|
-
iteration,
|
|
236
|
-
runId: controllerResult.runId,
|
|
237
|
-
boardSnapshot: board,
|
|
238
|
-
metrics,
|
|
239
|
-
elapsedMs: iterationElapsedMs,
|
|
240
|
-
continueReason,
|
|
241
|
-
stopReason
|
|
242
|
-
});
|
|
243
|
-
appendJsonl(join(loopDir, "events.jsonl"), {
|
|
244
|
-
type: "loop_iteration_completed",
|
|
245
|
-
loopId,
|
|
246
|
-
iteration,
|
|
247
|
-
runId: controllerResult.runId,
|
|
248
|
-
piExitCode: controllerResult.piInvocation.exitCode,
|
|
249
|
-
stopReason,
|
|
250
|
-
continueReason,
|
|
251
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
252
|
-
});
|
|
253
|
-
if (options.onIterationComplete) options.onIterationComplete(iterationResult);
|
|
254
|
-
if (stopReason !== null) {
|
|
255
|
-
finalStopReason = stopReason;
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
const totalElapsedMs = Date.now() - loopStartedAt;
|
|
260
|
-
const finalBoard = iterations.length > 0 ? iterations[iterations.length - 1].boardSnapshot : snapshotBoard(artifactsDir);
|
|
261
|
-
const aggregate = aggregateMetrics(iterations);
|
|
262
|
-
const loopResult = {
|
|
263
|
-
loopId,
|
|
264
|
-
iterations,
|
|
265
|
-
stopReason: finalStopReason,
|
|
266
|
-
totalIterations: iterations.length,
|
|
267
|
-
totalElapsedMs,
|
|
268
|
-
finalBoardSnapshot: finalBoard,
|
|
269
|
-
aggregateMetrics: aggregate
|
|
270
|
-
};
|
|
271
|
-
writeJson(join(loopDir, "loop-summary.json"), {
|
|
272
|
-
loopId,
|
|
273
|
-
stopReason: finalStopReason,
|
|
274
|
-
totalIterations: iterations.length,
|
|
275
|
-
totalElapsedMs,
|
|
276
|
-
finalBoardSnapshot: finalBoard,
|
|
277
|
-
aggregateMetrics: aggregate
|
|
278
|
-
});
|
|
279
|
-
appendJsonl(join(loopDir, "events.jsonl"), {
|
|
280
|
-
type: "loop_finished",
|
|
281
|
-
loopId,
|
|
282
|
-
stopReason: finalStopReason,
|
|
283
|
-
totalIterations: iterations.length,
|
|
284
|
-
totalElapsedMs,
|
|
285
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
286
|
-
});
|
|
287
|
-
return loopResult;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
//#endregion
|
|
291
19
|
//#region ../core/src/orchestration.ts
|
|
292
20
|
function createPiInvocationArgs(input) {
|
|
293
21
|
const args = [];
|
|
@@ -299,11 +27,29 @@ function createPiInvocationArgs(input) {
|
|
|
299
27
|
if (input.prompt) args.push("-p", input.prompt);
|
|
300
28
|
return args;
|
|
301
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
|
+
}
|
|
302
48
|
function createRolePrompt(input) {
|
|
303
49
|
const task = input.task ?? "pick the next bounded task from the current repo context";
|
|
304
50
|
switch (input.role) {
|
|
305
|
-
case "
|
|
306
|
-
"You are the Pi Town
|
|
51
|
+
case "mayor": return [
|
|
52
|
+
"You are the Pi Town mayor.",
|
|
307
53
|
"You coordinate work for this repository and act as the primary human-facing agent.",
|
|
308
54
|
"",
|
|
309
55
|
`Repository: ${input.repoRoot}`,
|
|
@@ -350,6 +96,7 @@ function resolveAgentSession(agentId, artifactsDir) {
|
|
|
350
96
|
sessionDir,
|
|
351
97
|
sessionId,
|
|
352
98
|
sessionPath,
|
|
99
|
+
processId: state.session.processId,
|
|
353
100
|
lastAttachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
354
101
|
})
|
|
355
102
|
};
|
|
@@ -374,6 +121,7 @@ function queueAgentMessage(input) {
|
|
|
374
121
|
sessionDir: state.session.sessionDir ?? getAgentSessionsDir(input.artifactsDir, input.agentId),
|
|
375
122
|
sessionId: state.session.sessionId,
|
|
376
123
|
sessionPath: state.session.sessionPath,
|
|
124
|
+
processId: state.session.processId,
|
|
377
125
|
lastAttachedAt: state.session.lastAttachedAt
|
|
378
126
|
})
|
|
379
127
|
}));
|
|
@@ -414,46 +162,42 @@ function spawnAgentRun(options) {
|
|
|
414
162
|
appendedSystemPrompt: options.appendedSystemPrompt,
|
|
415
163
|
extensionPath: options.extensionPath
|
|
416
164
|
});
|
|
417
|
-
const
|
|
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
|
+
autoResumeTarget: options.autoResumeTarget ?? null
|
|
176
|
+
}), "utf-8").toString("base64url"));
|
|
177
|
+
const child = spawn(runner.command, runner.args, {
|
|
418
178
|
cwd: options.repoRoot,
|
|
419
|
-
|
|
179
|
+
detached: true,
|
|
180
|
+
env: process.env,
|
|
181
|
+
stdio: "ignore"
|
|
420
182
|
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
writeFileSync(`${
|
|
424
|
-
writeFileSync(`${agentArtifactsDir}/latest-stderr.txt`, piResult.stderr, "utf-8");
|
|
425
|
-
writeFileSync(`${agentArtifactsDir}/latest-invocation.json`, `${JSON.stringify({
|
|
183
|
+
child.unref();
|
|
184
|
+
if (!child.pid) throw new Error(`Failed to launch detached ${options.role} run for ${options.agentId}`);
|
|
185
|
+
writeFileSync(`${getAgentDir(options.artifactsDir, options.agentId)}/latest-invocation.json`, `${JSON.stringify({
|
|
426
186
|
command: "pi",
|
|
427
187
|
args: piArgs,
|
|
428
|
-
exitCode:
|
|
188
|
+
exitCode: null,
|
|
429
189
|
sessionDir,
|
|
430
|
-
sessionPath:
|
|
431
|
-
sessionId:
|
|
190
|
+
sessionPath: null,
|
|
191
|
+
sessionId: null,
|
|
192
|
+
processId: child.pid,
|
|
193
|
+
startedAt
|
|
432
194
|
}, null, 2)}\n`, "utf-8");
|
|
433
|
-
const completionMessage = piResult.stdout.trim() || (piResult.exitCode === 0 ? `${options.role} run completed` : `${options.role} run exited with code ${piResult.exitCode}`);
|
|
434
|
-
appendAgentMessage({
|
|
435
|
-
artifactsDir: options.artifactsDir,
|
|
436
|
-
agentId: options.agentId,
|
|
437
|
-
box: "outbox",
|
|
438
|
-
from: options.agentId,
|
|
439
|
-
body: completionMessage
|
|
440
|
-
});
|
|
441
|
-
writeAgentState(options.artifactsDir, createAgentState({
|
|
442
|
-
...state,
|
|
443
|
-
status: piResult.exitCode === 0 ? "idle" : "blocked",
|
|
444
|
-
lastMessage: completionMessage,
|
|
445
|
-
blocked: piResult.exitCode !== 0,
|
|
446
|
-
waitingOn: piResult.exitCode === 0 ? null : "human-or-follow-up-run",
|
|
447
|
-
session: createAgentSessionRecord({
|
|
448
|
-
sessionDir: latestSession.sessionDir,
|
|
449
|
-
sessionId: latestSession.sessionId,
|
|
450
|
-
sessionPath: latestSession.sessionPath
|
|
451
|
-
})
|
|
452
|
-
}));
|
|
453
195
|
return {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
196
|
+
launch: {
|
|
197
|
+
processId: child.pid,
|
|
198
|
+
startedAt
|
|
199
|
+
},
|
|
200
|
+
latestSession: createAgentSessionRecord({ sessionDir })
|
|
457
201
|
};
|
|
458
202
|
}
|
|
459
203
|
function runAgentTurn(options) {
|
|
@@ -505,7 +249,8 @@ function runAgentTurn(options) {
|
|
|
505
249
|
session: createAgentSessionRecord({
|
|
506
250
|
sessionDir: latestSession.sessionDir,
|
|
507
251
|
sessionId: latestSession.sessionId,
|
|
508
|
-
sessionPath: latestSession.sessionPath
|
|
252
|
+
sessionPath: latestSession.sessionPath,
|
|
253
|
+
processId: null
|
|
509
254
|
})
|
|
510
255
|
}));
|
|
511
256
|
return {
|
|
@@ -533,13 +278,14 @@ function delegateTask(options) {
|
|
|
533
278
|
from: options.fromAgentId,
|
|
534
279
|
body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
|
|
535
280
|
});
|
|
536
|
-
const {
|
|
281
|
+
const { launch, latestSession } = spawnAgentRun({
|
|
537
282
|
repoRoot: options.repoRoot,
|
|
538
283
|
artifactsDir: options.artifactsDir,
|
|
539
284
|
role: options.role,
|
|
540
285
|
agentId,
|
|
541
286
|
appendedSystemPrompt: options.appendedSystemPrompt,
|
|
542
287
|
extensionPath: options.extensionPath,
|
|
288
|
+
autoResumeTarget: options.completionAutoResumeTarget,
|
|
543
289
|
task: options.task,
|
|
544
290
|
taskId: task.taskId
|
|
545
291
|
});
|
|
@@ -552,31 +298,198 @@ function delegateTask(options) {
|
|
|
552
298
|
});
|
|
553
299
|
writeTaskRecord(options.artifactsDir, {
|
|
554
300
|
...task,
|
|
555
|
-
status:
|
|
301
|
+
status: "running",
|
|
556
302
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
557
303
|
});
|
|
558
304
|
return {
|
|
559
305
|
task: {
|
|
560
306
|
...task,
|
|
561
|
-
status:
|
|
307
|
+
status: "running",
|
|
562
308
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
563
309
|
},
|
|
564
310
|
agentId,
|
|
565
|
-
|
|
311
|
+
launch,
|
|
566
312
|
latestSession
|
|
567
313
|
};
|
|
568
314
|
}
|
|
569
315
|
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region ../core/src/stop.ts
|
|
318
|
+
const DEFAULT_GRACE_MS = 750;
|
|
319
|
+
function sleepMs(ms) {
|
|
320
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
321
|
+
}
|
|
322
|
+
function getLocksDir() {
|
|
323
|
+
return join(homedir(), ".pi-town", "locks");
|
|
324
|
+
}
|
|
325
|
+
function processAlive(pid) {
|
|
326
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
327
|
+
try {
|
|
328
|
+
process.kill(pid, 0);
|
|
329
|
+
return true;
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function terminateProcess(pid, options) {
|
|
335
|
+
if (!processAlive(pid)) return {
|
|
336
|
+
signal: null,
|
|
337
|
+
exited: true
|
|
338
|
+
};
|
|
339
|
+
try {
|
|
340
|
+
process.kill(pid, "SIGTERM");
|
|
341
|
+
} catch {
|
|
342
|
+
return {
|
|
343
|
+
signal: null,
|
|
344
|
+
exited: !processAlive(pid)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
|
|
348
|
+
const deadline = Date.now() + graceMs;
|
|
349
|
+
while (Date.now() < deadline) {
|
|
350
|
+
if (!processAlive(pid)) return {
|
|
351
|
+
signal: "SIGTERM",
|
|
352
|
+
exited: true
|
|
353
|
+
};
|
|
354
|
+
sleepMs(25);
|
|
355
|
+
}
|
|
356
|
+
if (!options.force) return {
|
|
357
|
+
signal: "SIGTERM",
|
|
358
|
+
exited: !processAlive(pid)
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
process.kill(pid, "SIGKILL");
|
|
362
|
+
} catch {
|
|
363
|
+
return {
|
|
364
|
+
signal: "SIGTERM",
|
|
365
|
+
exited: !processAlive(pid)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
signal: "SIGKILL",
|
|
370
|
+
exited: !processAlive(pid)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function readLease(path) {
|
|
374
|
+
try {
|
|
375
|
+
return {
|
|
376
|
+
...JSON.parse(readFileSync(path, "utf-8")),
|
|
377
|
+
path
|
|
378
|
+
};
|
|
379
|
+
} catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function createStopMessage(options) {
|
|
384
|
+
if (options.reason) return options.reason;
|
|
385
|
+
if (options.actorId) return `Stopped by ${options.actorId}`;
|
|
386
|
+
return "Stopped by operator";
|
|
387
|
+
}
|
|
388
|
+
function createStopMessageInput(options) {
|
|
389
|
+
return {
|
|
390
|
+
...options.actorId === void 0 ? {} : { actorId: options.actorId },
|
|
391
|
+
...options.reason === void 0 ? {} : { reason: options.reason }
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function createTerminateOptions(options) {
|
|
395
|
+
return {
|
|
396
|
+
...options.force === void 0 ? {} : { force: options.force },
|
|
397
|
+
...options.graceMs === void 0 ? {} : { graceMs: options.graceMs }
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function listRepoLeases(repoId) {
|
|
401
|
+
let entries;
|
|
402
|
+
try {
|
|
403
|
+
entries = readdirSync(getLocksDir());
|
|
404
|
+
} catch {
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
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);
|
|
408
|
+
}
|
|
409
|
+
function stopRepoLeases(options) {
|
|
410
|
+
const results = listRepoLeases(options.repoId).map((lease) => {
|
|
411
|
+
const termination = terminateProcess(lease.pid, options);
|
|
412
|
+
if (termination.exited) rmSync(lease.path, { force: true });
|
|
413
|
+
return {
|
|
414
|
+
path: lease.path,
|
|
415
|
+
runId: lease.runId,
|
|
416
|
+
repoId: lease.repoId,
|
|
417
|
+
branch: lease.branch,
|
|
418
|
+
processId: lease.pid,
|
|
419
|
+
signal: termination.signal,
|
|
420
|
+
exited: termination.exited
|
|
421
|
+
};
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
results,
|
|
425
|
+
signaledProcesses: results.filter((result) => result.signal !== null).length
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function stopManagedAgents(options) {
|
|
429
|
+
const reason = createStopMessage(createStopMessageInput(options));
|
|
430
|
+
const excluded = new Set(options.excludeAgentIds ?? []);
|
|
431
|
+
const results = listAgentStates(options.artifactsDir).filter((agent) => {
|
|
432
|
+
if (excluded.has(agent.agentId)) return false;
|
|
433
|
+
if (options.agentId && agent.agentId !== options.agentId) return false;
|
|
434
|
+
return ![
|
|
435
|
+
"completed",
|
|
436
|
+
"failed",
|
|
437
|
+
"stopped"
|
|
438
|
+
].includes(agent.status);
|
|
439
|
+
}).map((state) => {
|
|
440
|
+
const processId = state.session.processId;
|
|
441
|
+
const termination = processId === null ? {
|
|
442
|
+
signal: null,
|
|
443
|
+
exited: true
|
|
444
|
+
} : terminateProcess(processId, createTerminateOptions(options));
|
|
445
|
+
if (state.taskId) updateTaskRecordStatus(options.artifactsDir, state.taskId, "aborted");
|
|
446
|
+
appendAgentMessage({
|
|
447
|
+
artifactsDir: options.artifactsDir,
|
|
448
|
+
agentId: state.agentId,
|
|
449
|
+
box: "outbox",
|
|
450
|
+
from: options.actorId ?? "system",
|
|
451
|
+
body: reason
|
|
452
|
+
});
|
|
453
|
+
writeAgentState(options.artifactsDir, createAgentState({
|
|
454
|
+
...state,
|
|
455
|
+
status: "stopped",
|
|
456
|
+
lastMessage: reason,
|
|
457
|
+
waitingOn: "stopped",
|
|
458
|
+
blocked: true,
|
|
459
|
+
session: createAgentSessionRecord({
|
|
460
|
+
sessionDir: state.session.sessionDir,
|
|
461
|
+
sessionId: state.session.sessionId,
|
|
462
|
+
sessionPath: state.session.sessionPath,
|
|
463
|
+
processId: null,
|
|
464
|
+
lastAttachedAt: state.session.lastAttachedAt
|
|
465
|
+
})
|
|
466
|
+
}));
|
|
467
|
+
return {
|
|
468
|
+
agentId: state.agentId,
|
|
469
|
+
previousStatus: state.status,
|
|
470
|
+
nextStatus: "stopped",
|
|
471
|
+
processId,
|
|
472
|
+
signal: termination.signal,
|
|
473
|
+
exited: termination.exited
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
results,
|
|
478
|
+
stoppedAgents: results.length,
|
|
479
|
+
signaledProcesses: results.filter((result) => result.signal !== null).length
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
570
483
|
//#endregion
|
|
571
484
|
//#region src/agent-id.ts
|
|
572
485
|
function normalizeAgentId(agentId) {
|
|
573
|
-
return agentId
|
|
486
|
+
return agentId;
|
|
574
487
|
}
|
|
575
488
|
|
|
576
489
|
//#endregion
|
|
577
490
|
//#region src/pi-runtime.ts
|
|
578
491
|
function isMayorAgent(agentId) {
|
|
579
|
-
return agentId === "
|
|
492
|
+
return agentId === "mayor";
|
|
580
493
|
}
|
|
581
494
|
function createPiTownRuntimeArgs(options) {
|
|
582
495
|
const args = ["--extension", resolvePiTownExtensionPath()];
|
|
@@ -589,46 +502,6 @@ function createPiTownRuntimeArgs(options) {
|
|
|
589
502
|
return args;
|
|
590
503
|
}
|
|
591
504
|
|
|
592
|
-
//#endregion
|
|
593
|
-
//#region src/repo-context.ts
|
|
594
|
-
function resolveRepoContext(argv) {
|
|
595
|
-
const { repo, rest } = parseOptionalRepoFlag(argv);
|
|
596
|
-
if (repo) {
|
|
597
|
-
const repoRoot = getRepoRoot(repo);
|
|
598
|
-
const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
|
|
599
|
-
return {
|
|
600
|
-
repoRoot,
|
|
601
|
-
repoSlug,
|
|
602
|
-
artifactsDir: getRepoArtifactsDir(repoSlug),
|
|
603
|
-
rest
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
const cwd = process.cwd();
|
|
607
|
-
const repoRoot = getRepoRoot(cwd);
|
|
608
|
-
const repoSlug = createRepoSlug(getRepoIdentity(repoRoot), repoRoot);
|
|
609
|
-
const artifactsDir = getRepoArtifactsDir(repoSlug);
|
|
610
|
-
if (isGitRepo(cwd) || existsSync(artifactsDir)) return {
|
|
611
|
-
repoRoot,
|
|
612
|
-
repoSlug,
|
|
613
|
-
artifactsDir,
|
|
614
|
-
rest
|
|
615
|
-
};
|
|
616
|
-
const latestPointerPath = getLatestRunPointerPath();
|
|
617
|
-
if (!existsSync(latestPointerPath)) return {
|
|
618
|
-
repoRoot,
|
|
619
|
-
repoSlug,
|
|
620
|
-
artifactsDir,
|
|
621
|
-
rest
|
|
622
|
-
};
|
|
623
|
-
const latest = JSON.parse(readFileSync(latestPointerPath, "utf-8"));
|
|
624
|
-
return {
|
|
625
|
-
repoRoot: latest.repoRoot,
|
|
626
|
-
repoSlug: latest.repoSlug,
|
|
627
|
-
artifactsDir: getRepoArtifactsDir(latest.repoSlug),
|
|
628
|
-
rest
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
|
|
632
505
|
//#endregion
|
|
633
506
|
//#region src/attach.ts
|
|
634
507
|
function attachTownAgent(argv = process.argv.slice(2)) {
|
|
@@ -658,23 +531,71 @@ function attachTownAgent(argv = process.argv.slice(2)) {
|
|
|
658
531
|
|
|
659
532
|
//#endregion
|
|
660
533
|
//#region src/board.ts
|
|
534
|
+
function truncate(text, max) {
|
|
535
|
+
if (!text) return "—";
|
|
536
|
+
const single = text.replace(/\n/g, " ").trim();
|
|
537
|
+
if (single.length <= max) return single;
|
|
538
|
+
return `${single.slice(0, max - 1)}…`;
|
|
539
|
+
}
|
|
661
540
|
function showTownBoard(argv = process.argv.slice(2)) {
|
|
662
541
|
const repo = resolveRepoContext(argv);
|
|
663
542
|
const agents = listAgentStates(repo.artifactsDir);
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
543
|
+
const tasks = listTaskRecords(repo.artifactsDir);
|
|
544
|
+
const repoName = basename(repo.repoRoot);
|
|
545
|
+
const branch = getCurrentBranch(repo.repoRoot);
|
|
546
|
+
const branchLabel = branch ? ` (${branch})` : "";
|
|
547
|
+
const mayor = agents.find((a) => a.agentId === "mayor");
|
|
548
|
+
const workers = agents.filter((a) => a.agentId !== "mayor");
|
|
549
|
+
const workersByStatus = (status) => workers.filter((a) => a.status === status).length;
|
|
550
|
+
console.log(`[pitown] board — ${repoName}${branchLabel}`);
|
|
551
|
+
if (mayor) {
|
|
552
|
+
const spawned = workers.length;
|
|
553
|
+
const running = workersByStatus("running") + workersByStatus("starting");
|
|
554
|
+
const completed = workersByStatus("completed") + workersByStatus("idle");
|
|
555
|
+
const blocked = workersByStatus("blocked") + workersByStatus("failed");
|
|
556
|
+
const parts = [`${spawned} spawned`];
|
|
557
|
+
if (running > 0) parts.push(`${running} running`);
|
|
558
|
+
if (completed > 0) parts.push(`${completed} done`);
|
|
559
|
+
if (blocked > 0) parts.push(`${blocked} blocked`);
|
|
560
|
+
console.log(`Mayor workers: ${parts.join(", ")}`);
|
|
670
561
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
562
|
+
console.log("");
|
|
563
|
+
console.log("Agents:");
|
|
564
|
+
if (agents.length === 0) console.log(" (no agents)");
|
|
565
|
+
else for (const agent of agents) {
|
|
566
|
+
const id = agent.agentId.padEnd(14);
|
|
567
|
+
const role = agent.role.padEnd(10);
|
|
568
|
+
const status = agent.status.padEnd(10);
|
|
569
|
+
const task = truncate(agent.task, 60);
|
|
570
|
+
const msg = agent.lastMessage ? ` | ${truncate(agent.lastMessage, 40)}` : "";
|
|
571
|
+
const waiting = agent.waitingOn ? ` | waiting on: ${agent.waitingOn}` : "";
|
|
572
|
+
console.log(` ${id}${role}${status}${task}${msg}${waiting}`);
|
|
677
573
|
}
|
|
574
|
+
console.log("");
|
|
575
|
+
console.log("Tasks:");
|
|
576
|
+
if (tasks.length === 0) console.log(" (no tasks)");
|
|
577
|
+
else for (const task of tasks) {
|
|
578
|
+
const id = task.taskId.padEnd(14);
|
|
579
|
+
const status = task.status.padEnd(12);
|
|
580
|
+
const assignee = (task.assignedAgentId ?? "—").padEnd(14);
|
|
581
|
+
const title = truncate(task.title, 60);
|
|
582
|
+
console.log(` ${id}${status}${assignee}${title}`);
|
|
583
|
+
}
|
|
584
|
+
const metricsPath = join(repo.artifactsDir, "latest", "metrics.json");
|
|
585
|
+
if (existsSync(metricsPath)) try {
|
|
586
|
+
const metrics = JSON.parse(readFileSync(metricsPath, "utf-8"));
|
|
587
|
+
console.log("");
|
|
588
|
+
console.log("Metrics (latest run):");
|
|
589
|
+
console.log(` Interrupt Rate: ${fmt(metrics.interruptRate)}`);
|
|
590
|
+
console.log(` Autonomous Completion Rate: ${fmt(metrics.autonomousCompletionRate)}`);
|
|
591
|
+
console.log(` Context Coverage Score: ${fmt(metrics.contextCoverageScore)}`);
|
|
592
|
+
console.log(` MTTC: ${metrics.meanTimeToCorrectHours != null ? `${metrics.meanTimeToCorrectHours}h` : "—"}`);
|
|
593
|
+
console.log(` Feedback-to-Demo: ${metrics.feedbackToDemoCycleTimeHours != null ? `${metrics.feedbackToDemoCycleTimeHours}h` : "—"}`);
|
|
594
|
+
} catch {}
|
|
595
|
+
}
|
|
596
|
+
function fmt(value) {
|
|
597
|
+
if (value == null) return "—";
|
|
598
|
+
return String(value);
|
|
678
599
|
}
|
|
679
600
|
|
|
680
601
|
//#endregion
|
|
@@ -711,12 +632,13 @@ function continueTownAgent(argv = process.argv.slice(2)) {
|
|
|
711
632
|
//#endregion
|
|
712
633
|
//#region src/delegate.ts
|
|
713
634
|
function parseDelegateFlags(argv) {
|
|
714
|
-
let from = "
|
|
635
|
+
let from = "mayor";
|
|
715
636
|
let role = "worker";
|
|
716
637
|
let agentId = null;
|
|
717
638
|
let task = null;
|
|
718
639
|
for (let index = 0; index < argv.length; index += 1) {
|
|
719
640
|
const arg = argv[index];
|
|
641
|
+
if (arg === void 0) continue;
|
|
720
642
|
if (arg.startsWith("--from=")) {
|
|
721
643
|
from = arg.slice(7);
|
|
722
644
|
continue;
|
|
@@ -768,14 +690,20 @@ function delegateTownTask(argv = process.argv.slice(2)) {
|
|
|
768
690
|
const flags = parseDelegateFlags(repo.rest);
|
|
769
691
|
const fromAgentId = normalizeAgentId(flags.from);
|
|
770
692
|
if (readAgentState(repo.artifactsDir, fromAgentId) === null) throw new Error(`Unknown delegating agent: ${fromAgentId}`);
|
|
771
|
-
const { agentId, latestSession,
|
|
693
|
+
const { agentId, latestSession, launch, task } = delegateTask({
|
|
772
694
|
repoRoot: repo.repoRoot,
|
|
773
695
|
artifactsDir: repo.artifactsDir,
|
|
774
696
|
fromAgentId,
|
|
775
697
|
role: flags.role,
|
|
776
698
|
agentId: flags.agentId,
|
|
777
699
|
task: flags.task,
|
|
778
|
-
extensionPath: resolvePiTownExtensionPath()
|
|
700
|
+
extensionPath: resolvePiTownExtensionPath(),
|
|
701
|
+
completionAutoResumeTarget: fromAgentId === "mayor" ? {
|
|
702
|
+
agentId: "mayor",
|
|
703
|
+
message: "New agent check-ins arrived. Review the latest board and inbox updates, then decide the next bounded action.",
|
|
704
|
+
extensionPath: resolvePiTownExtensionPath(),
|
|
705
|
+
appendedSystemPrompt: readPiTownMayorPrompt()
|
|
706
|
+
} : null
|
|
779
707
|
});
|
|
780
708
|
console.log("[pitown] delegate");
|
|
781
709
|
console.log(`- repo root: ${repo.repoRoot}`);
|
|
@@ -783,8 +711,10 @@ function delegateTownTask(argv = process.argv.slice(2)) {
|
|
|
783
711
|
console.log(`- task id: ${task.taskId}`);
|
|
784
712
|
console.log(`- agent: ${agentId}`);
|
|
785
713
|
console.log(`- role: ${flags.role}`);
|
|
786
|
-
console.log(`-
|
|
714
|
+
console.log(`- status: ${task.status}`);
|
|
715
|
+
console.log(`- launch pid: ${launch.processId}`);
|
|
787
716
|
if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
|
|
717
|
+
else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
|
|
788
718
|
}
|
|
789
719
|
|
|
790
720
|
//#endregion
|
|
@@ -806,6 +736,7 @@ function parseLoopCliFlags(argv) {
|
|
|
806
736
|
const flags = { noStopOnFailure: false };
|
|
807
737
|
for (let index = 0; index < argv.length; index += 1) {
|
|
808
738
|
const arg = argv[index];
|
|
739
|
+
if (arg === void 0) continue;
|
|
809
740
|
if (arg.startsWith("--repo=")) {
|
|
810
741
|
flags.repo = arg.slice(7);
|
|
811
742
|
continue;
|
|
@@ -926,11 +857,11 @@ function loopTown(argv = process.argv.slice(2)) {
|
|
|
926
857
|
onIterationComplete(iteration) {
|
|
927
858
|
const board = iteration.boardSnapshot;
|
|
928
859
|
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";
|
|
929
|
-
const
|
|
860
|
+
const mayorStatus = board.agents.find((a) => a.agentId === "mayor")?.status ?? "unknown";
|
|
930
861
|
console.log(`[pitown-loop] iteration ${iteration.iteration}/${config.maxIterations} completed (${formatMs(iteration.elapsedMs)})`);
|
|
931
862
|
console.log(` - pi exit code: ${iteration.controllerResult.piInvocation.exitCode}`);
|
|
932
863
|
console.log(` - run: ${iteration.controllerResult.runId}`);
|
|
933
|
-
console.log(` - board: ${taskSummary},
|
|
864
|
+
console.log(` - board: ${taskSummary}, mayor ${mayorStatus}`);
|
|
934
865
|
console.log(` - metrics: interrupt rate ${iteration.metrics.interruptRate}, autonomous completion ${iteration.metrics.autonomousCompletionRate}`);
|
|
935
866
|
if (iteration.stopReason) console.log(` - stopping: ${iteration.stopReason}`);
|
|
936
867
|
else console.log(` - continuing: ${iteration.continueReason}`);
|
|
@@ -945,10 +876,10 @@ function loopTown(argv = process.argv.slice(2)) {
|
|
|
945
876
|
//#endregion
|
|
946
877
|
//#region src/mayor.ts
|
|
947
878
|
function startFreshMayorSession(repoRoot, artifactsDir) {
|
|
948
|
-
const sessionDir = getAgentSessionsDir(artifactsDir, "
|
|
879
|
+
const sessionDir = getAgentSessionsDir(artifactsDir, "mayor");
|
|
949
880
|
writeAgentState(artifactsDir, createAgentState({
|
|
950
|
-
agentId: "
|
|
951
|
-
role: "
|
|
881
|
+
agentId: "mayor",
|
|
882
|
+
role: "mayor",
|
|
952
883
|
status: "running",
|
|
953
884
|
task: "open the mayor session and plan the next steps for this repository",
|
|
954
885
|
lastMessage: "Mayor session opened",
|
|
@@ -958,14 +889,14 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
|
|
|
958
889
|
console.log(`- repo root: ${repoRoot}`);
|
|
959
890
|
console.log("- starting a new mayor session");
|
|
960
891
|
const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
|
|
961
|
-
agentId: "
|
|
892
|
+
agentId: "mayor",
|
|
962
893
|
sessionDir
|
|
963
894
|
}), {
|
|
964
895
|
cwd: repoRoot,
|
|
965
896
|
env: process.env
|
|
966
897
|
});
|
|
967
|
-
const latestSession = getLatestAgentSession(artifactsDir, "
|
|
968
|
-
const previousState = readAgentState(artifactsDir, "
|
|
898
|
+
const latestSession = getLatestAgentSession(artifactsDir, "mayor");
|
|
899
|
+
const previousState = readAgentState(artifactsDir, "mayor");
|
|
969
900
|
if (previousState !== null) writeAgentState(artifactsDir, createAgentState({
|
|
970
901
|
...previousState,
|
|
971
902
|
status: exitCode === 0 ? "idle" : "blocked",
|
|
@@ -983,7 +914,7 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
|
|
|
983
914
|
function openTownMayor(argv = process.argv.slice(2)) {
|
|
984
915
|
const repo = resolveRepoContext(argv);
|
|
985
916
|
const message = repo.rest.join(" ").trim();
|
|
986
|
-
if (readAgentState(repo.artifactsDir, "
|
|
917
|
+
if (readAgentState(repo.artifactsDir, "mayor") === null) {
|
|
987
918
|
assertCommandAvailable("pi");
|
|
988
919
|
if (message) {
|
|
989
920
|
runTown([
|
|
@@ -1029,7 +960,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
|
|
|
1029
960
|
from: "human",
|
|
1030
961
|
body
|
|
1031
962
|
});
|
|
1032
|
-
const deliveredResult = state.role === "
|
|
963
|
+
const deliveredResult = state.role === "mayor" ? runAgentTurn({
|
|
1033
964
|
repoRoot: repo.repoRoot,
|
|
1034
965
|
artifactsDir: repo.artifactsDir,
|
|
1035
966
|
agentId,
|
|
@@ -1047,7 +978,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
|
|
|
1047
978
|
console.log(`- queued message: ${body}`);
|
|
1048
979
|
if (deliveredResult) {
|
|
1049
980
|
console.log(`- delivered to session: ${deliveredResult.latestSession.sessionPath}`);
|
|
1050
|
-
console.log(`-
|
|
981
|
+
console.log(`- mayor response: ${deliveredResult.completionMessage}`);
|
|
1051
982
|
}
|
|
1052
983
|
}
|
|
1053
984
|
|
|
@@ -1093,6 +1024,7 @@ function parseSpawnFlags(argv) {
|
|
|
1093
1024
|
let task = null;
|
|
1094
1025
|
for (let index = 0; index < argv.length; index += 1) {
|
|
1095
1026
|
const arg = argv[index];
|
|
1027
|
+
if (arg === void 0) continue;
|
|
1096
1028
|
if (arg.startsWith("--role=")) {
|
|
1097
1029
|
role = arg.slice(7);
|
|
1098
1030
|
continue;
|
|
@@ -1137,7 +1069,7 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
|
|
|
1137
1069
|
const flags = parseSpawnFlags(repo.rest);
|
|
1138
1070
|
const agentId = flags.agentId ?? `${flags.role}-${Date.now()}`;
|
|
1139
1071
|
const task = flags.task;
|
|
1140
|
-
const {
|
|
1072
|
+
const { launch, latestSession } = spawnAgent({
|
|
1141
1073
|
repoRoot: repo.repoRoot,
|
|
1142
1074
|
artifactsDir: repo.artifactsDir,
|
|
1143
1075
|
role: flags.role,
|
|
@@ -1149,18 +1081,122 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
|
|
|
1149
1081
|
console.log(`- repo root: ${repo.repoRoot}`);
|
|
1150
1082
|
console.log(`- agent: ${agentId}`);
|
|
1151
1083
|
console.log(`- role: ${flags.role}`);
|
|
1152
|
-
console.log(`-
|
|
1084
|
+
console.log(`- status: running`);
|
|
1085
|
+
console.log(`- launch pid: ${launch.processId}`);
|
|
1153
1086
|
if (task) console.log(`- task: ${task}`);
|
|
1154
1087
|
if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
|
|
1088
|
+
else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
|
|
1155
1089
|
}
|
|
1156
1090
|
|
|
1157
1091
|
//#endregion
|
|
1158
|
-
//#region
|
|
1159
|
-
|
|
1092
|
+
//#region src/stop.ts
|
|
1093
|
+
function parseStopFlags(argv) {
|
|
1094
|
+
let all = false;
|
|
1095
|
+
let agentId = null;
|
|
1096
|
+
let force = false;
|
|
1097
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1098
|
+
const arg = argv[index];
|
|
1099
|
+
if (arg === void 0) continue;
|
|
1100
|
+
if (arg === "--all") {
|
|
1101
|
+
all = true;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (arg === "--force") {
|
|
1105
|
+
force = true;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (arg.startsWith("--agent=")) {
|
|
1109
|
+
agentId = normalizeAgentId(arg.slice(8));
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (arg === "--agent") {
|
|
1113
|
+
const value = argv[index + 1];
|
|
1114
|
+
if (!value) throw new Error("Missing value for --agent");
|
|
1115
|
+
agentId = normalizeAgentId(value);
|
|
1116
|
+
index += 1;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
all,
|
|
1123
|
+
agentId,
|
|
1124
|
+
force
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function stopRepo(repoRoot, artifactsDir, flags) {
|
|
1128
|
+
const repoId = getRepoIdentity(repoRoot);
|
|
1129
|
+
const leaseResult = flags.agentId ? { signaledProcesses: 0 } : stopRepoLeases({
|
|
1130
|
+
repoId,
|
|
1131
|
+
force: flags.force
|
|
1132
|
+
});
|
|
1133
|
+
const agentResult = stopManagedAgents({
|
|
1134
|
+
artifactsDir,
|
|
1135
|
+
agentId: flags.agentId,
|
|
1136
|
+
actorId: "human",
|
|
1137
|
+
reason: flags.agentId ? `Stopped ${flags.agentId} via pitown stop` : "Stopped via pitown stop",
|
|
1138
|
+
force: flags.force
|
|
1139
|
+
});
|
|
1140
|
+
return {
|
|
1141
|
+
repoLabel: repoRoot,
|
|
1142
|
+
stoppedAgents: agentResult.stoppedAgents,
|
|
1143
|
+
signaledAgentProcesses: agentResult.signaledProcesses,
|
|
1144
|
+
signaledLeaseProcesses: leaseResult.signaledProcesses
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function listTrackedArtifactsDirs() {
|
|
1148
|
+
const reposRoot = getReposRootDir();
|
|
1149
|
+
if (!existsSync(reposRoot)) return [];
|
|
1150
|
+
return readdirSync(reposRoot).map((entry) => getRepoArtifactsDir(entry)).filter((path) => existsSync(path));
|
|
1151
|
+
}
|
|
1152
|
+
function stopTown(argv = process.argv.slice(2)) {
|
|
1153
|
+
const { repo, rest } = parseOptionalRepoFlag(argv);
|
|
1154
|
+
const flags = parseStopFlags(rest);
|
|
1155
|
+
if (flags.all && repo) throw new Error("Do not combine --all with --repo");
|
|
1156
|
+
if (flags.all && flags.agentId) throw new Error("Do not combine --all with --agent");
|
|
1157
|
+
if (flags.all) {
|
|
1158
|
+
const repoSummaries = listTrackedArtifactsDirs().map((artifactsDir) => {
|
|
1159
|
+
const result = stopManagedAgents({
|
|
1160
|
+
artifactsDir,
|
|
1161
|
+
actorId: "human",
|
|
1162
|
+
reason: "Stopped via pitown stop --all",
|
|
1163
|
+
force: flags.force
|
|
1164
|
+
});
|
|
1165
|
+
return {
|
|
1166
|
+
repoLabel: artifactsDir,
|
|
1167
|
+
stoppedAgents: result.stoppedAgents,
|
|
1168
|
+
signaledAgentProcesses: result.signaledProcesses,
|
|
1169
|
+
signaledLeaseProcesses: 0
|
|
1170
|
+
};
|
|
1171
|
+
});
|
|
1172
|
+
const leaseResult = stopRepoLeases({ force: flags.force });
|
|
1173
|
+
const totalAgents = repoSummaries.reduce((sum, result) => sum + result.stoppedAgents, 0);
|
|
1174
|
+
const totalAgentProcesses = repoSummaries.reduce((sum, result) => sum + result.signaledAgentProcesses, 0);
|
|
1175
|
+
const totalLeaseProcesses = leaseResult.signaledProcesses + repoSummaries.reduce((sum, result) => sum + result.signaledLeaseProcesses, 0);
|
|
1176
|
+
console.log("[pitown] stop");
|
|
1177
|
+
console.log("- scope: all repos");
|
|
1178
|
+
console.log(`- stopped agents: ${totalAgents}`);
|
|
1179
|
+
console.log(`- signaled agent processes: ${totalAgentProcesses}`);
|
|
1180
|
+
console.log(`- signaled lease processes: ${totalLeaseProcesses}`);
|
|
1181
|
+
if (repoSummaries.length === 0 && leaseResult.results.length === 0) console.log("- nothing was running");
|
|
1182
|
+
return repoSummaries;
|
|
1183
|
+
}
|
|
1184
|
+
const resolved = repo ? resolveRepoContext(["--repo", repo]) : resolveRepoContext([]);
|
|
1185
|
+
const result = stopRepo(resolved.repoRoot, resolved.artifactsDir, flags);
|
|
1186
|
+
console.log("[pitown] stop");
|
|
1187
|
+
console.log(`- repo root: ${result.repoLabel}`);
|
|
1188
|
+
if (flags.agentId) console.log(`- agent: ${flags.agentId}`);
|
|
1189
|
+
console.log(`- stopped agents: ${result.stoppedAgents}`);
|
|
1190
|
+
console.log(`- signaled agent processes: ${result.signaledAgentProcesses}`);
|
|
1191
|
+
if (!flags.agentId) console.log(`- signaled lease processes: ${result.signaledLeaseProcesses}`);
|
|
1192
|
+
if (result.stoppedAgents === 0 && result.signaledAgentProcesses === 0 && result.signaledLeaseProcesses === 0) console.log("- nothing was running");
|
|
1193
|
+
return [result];
|
|
1194
|
+
}
|
|
1160
1195
|
|
|
1161
1196
|
//#endregion
|
|
1162
1197
|
//#region src/version.ts
|
|
1163
|
-
const
|
|
1198
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1199
|
+
const CLI_VERSION = packageJson.version;
|
|
1164
1200
|
|
|
1165
1201
|
//#endregion
|
|
1166
1202
|
//#region src/index.ts
|
|
@@ -1169,27 +1205,23 @@ function printHelp(showAdvanced = false) {
|
|
|
1169
1205
|
"pitown",
|
|
1170
1206
|
"",
|
|
1171
1207
|
"Usage:",
|
|
1172
|
-
" pitown",
|
|
1173
|
-
" pitown mayor [--repo <path>] [\"message\"]",
|
|
1208
|
+
" pitown [--repo <path>] [\"message\"]",
|
|
1174
1209
|
" pitown board [--repo <path>]",
|
|
1175
1210
|
" pitown peek [--repo <path>] [agent]",
|
|
1176
|
-
" pitown msg [--repo <path>]
|
|
1211
|
+
" pitown msg [--repo <path>] <agent> \"message\"",
|
|
1177
1212
|
" pitown status [--repo <path>]",
|
|
1213
|
+
" pitown stop [--repo <path>] [--agent <id>] [--all] [--force]",
|
|
1178
1214
|
" pitown doctor",
|
|
1179
|
-
" pitown help",
|
|
1180
|
-
" pitown help --all",
|
|
1181
|
-
" pitown --help",
|
|
1182
|
-
" pitown -v",
|
|
1183
1215
|
" pitown --version",
|
|
1184
1216
|
"",
|
|
1185
1217
|
"Mayor workflow:",
|
|
1186
1218
|
" pitown",
|
|
1187
|
-
" pitown
|
|
1188
|
-
" pitown mayor \"plan the next milestones\"",
|
|
1219
|
+
" pitown \"plan the next milestones\"",
|
|
1189
1220
|
" /plan",
|
|
1190
1221
|
" /todos",
|
|
1191
1222
|
"",
|
|
1192
1223
|
"Inside the mayor session, `/plan` toggles read-only planning mode and `/todos` shows the captured plan.",
|
|
1224
|
+
"Aliases still work: `pitown mayor`, `pitown help`, `pitown --help`, `pitown -v`.",
|
|
1193
1225
|
"",
|
|
1194
1226
|
"If --repo is omitted, Pi Town uses the repo for the current working directory when possible.",
|
|
1195
1227
|
...showAdvanced ? [
|
|
@@ -1224,7 +1256,8 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
1224
1256
|
break;
|
|
1225
1257
|
case "run": {
|
|
1226
1258
|
const result = runTown(args);
|
|
1227
|
-
|
|
1259
|
+
const latestIteration = result.iterations[result.iterations.length - 1];
|
|
1260
|
+
if (latestIteration && latestIteration.controllerResult.piInvocation.exitCode !== 0) process.exitCode = latestIteration.controllerResult.piInvocation.exitCode;
|
|
1228
1261
|
break;
|
|
1229
1262
|
}
|
|
1230
1263
|
case "loop": {
|
|
@@ -1260,6 +1293,9 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
1260
1293
|
case "status":
|
|
1261
1294
|
showTownStatus(args);
|
|
1262
1295
|
break;
|
|
1296
|
+
case "stop":
|
|
1297
|
+
stopTown(args);
|
|
1298
|
+
break;
|
|
1263
1299
|
case "watch":
|
|
1264
1300
|
watchTown(args);
|
|
1265
1301
|
break;
|