@schilderlabs/pitown 0.2.1 → 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 +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 +433 -405
- 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 { 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";
|
|
11
14
|
import { homedir } 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,41 @@ 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
|
+
}), "utf-8").toString("base64url"));
|
|
176
|
+
const child = spawn(runner.command, runner.args, {
|
|
418
177
|
cwd: options.repoRoot,
|
|
419
|
-
|
|
178
|
+
detached: true,
|
|
179
|
+
env: process.env,
|
|
180
|
+
stdio: "ignore"
|
|
420
181
|
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
writeFileSync(`${
|
|
424
|
-
writeFileSync(`${agentArtifactsDir}/latest-stderr.txt`, piResult.stderr, "utf-8");
|
|
425
|
-
writeFileSync(`${agentArtifactsDir}/latest-invocation.json`, `${JSON.stringify({
|
|
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({
|
|
426
185
|
command: "pi",
|
|
427
186
|
args: piArgs,
|
|
428
|
-
exitCode:
|
|
187
|
+
exitCode: null,
|
|
429
188
|
sessionDir,
|
|
430
|
-
sessionPath:
|
|
431
|
-
sessionId:
|
|
189
|
+
sessionPath: null,
|
|
190
|
+
sessionId: null,
|
|
191
|
+
processId: child.pid,
|
|
192
|
+
startedAt
|
|
432
193
|
}, 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
194
|
return {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
195
|
+
launch: {
|
|
196
|
+
processId: child.pid,
|
|
197
|
+
startedAt
|
|
198
|
+
},
|
|
199
|
+
latestSession: createAgentSessionRecord({ sessionDir })
|
|
457
200
|
};
|
|
458
201
|
}
|
|
459
202
|
function runAgentTurn(options) {
|
|
@@ -505,7 +248,8 @@ function runAgentTurn(options) {
|
|
|
505
248
|
session: createAgentSessionRecord({
|
|
506
249
|
sessionDir: latestSession.sessionDir,
|
|
507
250
|
sessionId: latestSession.sessionId,
|
|
508
|
-
sessionPath: latestSession.sessionPath
|
|
251
|
+
sessionPath: latestSession.sessionPath,
|
|
252
|
+
processId: null
|
|
509
253
|
})
|
|
510
254
|
}));
|
|
511
255
|
return {
|
|
@@ -533,7 +277,7 @@ function delegateTask(options) {
|
|
|
533
277
|
from: options.fromAgentId,
|
|
534
278
|
body: `Delegated ${task.taskId} to ${agentId}: ${options.task}`
|
|
535
279
|
});
|
|
536
|
-
const {
|
|
280
|
+
const { launch, latestSession } = spawnAgentRun({
|
|
537
281
|
repoRoot: options.repoRoot,
|
|
538
282
|
artifactsDir: options.artifactsDir,
|
|
539
283
|
role: options.role,
|
|
@@ -552,31 +296,198 @@ function delegateTask(options) {
|
|
|
552
296
|
});
|
|
553
297
|
writeTaskRecord(options.artifactsDir, {
|
|
554
298
|
...task,
|
|
555
|
-
status:
|
|
299
|
+
status: "running",
|
|
556
300
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
557
301
|
});
|
|
558
302
|
return {
|
|
559
303
|
task: {
|
|
560
304
|
...task,
|
|
561
|
-
status:
|
|
305
|
+
status: "running",
|
|
562
306
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
563
307
|
},
|
|
564
308
|
agentId,
|
|
565
|
-
|
|
309
|
+
launch,
|
|
566
310
|
latestSession
|
|
567
311
|
};
|
|
568
312
|
}
|
|
569
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
|
+
|
|
570
481
|
//#endregion
|
|
571
482
|
//#region src/agent-id.ts
|
|
572
483
|
function normalizeAgentId(agentId) {
|
|
573
|
-
return agentId
|
|
484
|
+
return agentId;
|
|
574
485
|
}
|
|
575
486
|
|
|
576
487
|
//#endregion
|
|
577
488
|
//#region src/pi-runtime.ts
|
|
578
489
|
function isMayorAgent(agentId) {
|
|
579
|
-
return agentId === "
|
|
490
|
+
return agentId === "mayor";
|
|
580
491
|
}
|
|
581
492
|
function createPiTownRuntimeArgs(options) {
|
|
582
493
|
const args = ["--extension", resolvePiTownExtensionPath()];
|
|
@@ -589,46 +500,6 @@ function createPiTownRuntimeArgs(options) {
|
|
|
589
500
|
return args;
|
|
590
501
|
}
|
|
591
502
|
|
|
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
503
|
//#endregion
|
|
633
504
|
//#region src/attach.ts
|
|
634
505
|
function attachTownAgent(argv = process.argv.slice(2)) {
|
|
@@ -658,23 +529,71 @@ function attachTownAgent(argv = process.argv.slice(2)) {
|
|
|
658
529
|
|
|
659
530
|
//#endregion
|
|
660
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
|
+
}
|
|
661
538
|
function showTownBoard(argv = process.argv.slice(2)) {
|
|
662
539
|
const repo = resolveRepoContext(argv);
|
|
663
540
|
const agents = listAgentStates(repo.artifactsDir);
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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}`);
|
|
670
571
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
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}`);
|
|
677
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);
|
|
678
597
|
}
|
|
679
598
|
|
|
680
599
|
//#endregion
|
|
@@ -711,12 +630,13 @@ function continueTownAgent(argv = process.argv.slice(2)) {
|
|
|
711
630
|
//#endregion
|
|
712
631
|
//#region src/delegate.ts
|
|
713
632
|
function parseDelegateFlags(argv) {
|
|
714
|
-
let from = "
|
|
633
|
+
let from = "mayor";
|
|
715
634
|
let role = "worker";
|
|
716
635
|
let agentId = null;
|
|
717
636
|
let task = null;
|
|
718
637
|
for (let index = 0; index < argv.length; index += 1) {
|
|
719
638
|
const arg = argv[index];
|
|
639
|
+
if (arg === void 0) continue;
|
|
720
640
|
if (arg.startsWith("--from=")) {
|
|
721
641
|
from = arg.slice(7);
|
|
722
642
|
continue;
|
|
@@ -768,7 +688,7 @@ function delegateTownTask(argv = process.argv.slice(2)) {
|
|
|
768
688
|
const flags = parseDelegateFlags(repo.rest);
|
|
769
689
|
const fromAgentId = normalizeAgentId(flags.from);
|
|
770
690
|
if (readAgentState(repo.artifactsDir, fromAgentId) === null) throw new Error(`Unknown delegating agent: ${fromAgentId}`);
|
|
771
|
-
const { agentId, latestSession,
|
|
691
|
+
const { agentId, latestSession, launch, task } = delegateTask({
|
|
772
692
|
repoRoot: repo.repoRoot,
|
|
773
693
|
artifactsDir: repo.artifactsDir,
|
|
774
694
|
fromAgentId,
|
|
@@ -783,8 +703,10 @@ function delegateTownTask(argv = process.argv.slice(2)) {
|
|
|
783
703
|
console.log(`- task id: ${task.taskId}`);
|
|
784
704
|
console.log(`- agent: ${agentId}`);
|
|
785
705
|
console.log(`- role: ${flags.role}`);
|
|
786
|
-
console.log(`-
|
|
706
|
+
console.log(`- status: ${task.status}`);
|
|
707
|
+
console.log(`- launch pid: ${launch.processId}`);
|
|
787
708
|
if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
|
|
709
|
+
else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
|
|
788
710
|
}
|
|
789
711
|
|
|
790
712
|
//#endregion
|
|
@@ -806,6 +728,7 @@ function parseLoopCliFlags(argv) {
|
|
|
806
728
|
const flags = { noStopOnFailure: false };
|
|
807
729
|
for (let index = 0; index < argv.length; index += 1) {
|
|
808
730
|
const arg = argv[index];
|
|
731
|
+
if (arg === void 0) continue;
|
|
809
732
|
if (arg.startsWith("--repo=")) {
|
|
810
733
|
flags.repo = arg.slice(7);
|
|
811
734
|
continue;
|
|
@@ -926,11 +849,11 @@ function loopTown(argv = process.argv.slice(2)) {
|
|
|
926
849
|
onIterationComplete(iteration) {
|
|
927
850
|
const board = iteration.boardSnapshot;
|
|
928
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";
|
|
929
|
-
const
|
|
852
|
+
const mayorStatus = board.agents.find((a) => a.agentId === "mayor")?.status ?? "unknown";
|
|
930
853
|
console.log(`[pitown-loop] iteration ${iteration.iteration}/${config.maxIterations} completed (${formatMs(iteration.elapsedMs)})`);
|
|
931
854
|
console.log(` - pi exit code: ${iteration.controllerResult.piInvocation.exitCode}`);
|
|
932
855
|
console.log(` - run: ${iteration.controllerResult.runId}`);
|
|
933
|
-
console.log(` - board: ${taskSummary},
|
|
856
|
+
console.log(` - board: ${taskSummary}, mayor ${mayorStatus}`);
|
|
934
857
|
console.log(` - metrics: interrupt rate ${iteration.metrics.interruptRate}, autonomous completion ${iteration.metrics.autonomousCompletionRate}`);
|
|
935
858
|
if (iteration.stopReason) console.log(` - stopping: ${iteration.stopReason}`);
|
|
936
859
|
else console.log(` - continuing: ${iteration.continueReason}`);
|
|
@@ -945,10 +868,10 @@ function loopTown(argv = process.argv.slice(2)) {
|
|
|
945
868
|
//#endregion
|
|
946
869
|
//#region src/mayor.ts
|
|
947
870
|
function startFreshMayorSession(repoRoot, artifactsDir) {
|
|
948
|
-
const sessionDir = getAgentSessionsDir(artifactsDir, "
|
|
871
|
+
const sessionDir = getAgentSessionsDir(artifactsDir, "mayor");
|
|
949
872
|
writeAgentState(artifactsDir, createAgentState({
|
|
950
|
-
agentId: "
|
|
951
|
-
role: "
|
|
873
|
+
agentId: "mayor",
|
|
874
|
+
role: "mayor",
|
|
952
875
|
status: "running",
|
|
953
876
|
task: "open the mayor session and plan the next steps for this repository",
|
|
954
877
|
lastMessage: "Mayor session opened",
|
|
@@ -958,14 +881,14 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
|
|
|
958
881
|
console.log(`- repo root: ${repoRoot}`);
|
|
959
882
|
console.log("- starting a new mayor session");
|
|
960
883
|
const exitCode = runCommandInteractive("pi", createPiTownRuntimeArgs({
|
|
961
|
-
agentId: "
|
|
884
|
+
agentId: "mayor",
|
|
962
885
|
sessionDir
|
|
963
886
|
}), {
|
|
964
887
|
cwd: repoRoot,
|
|
965
888
|
env: process.env
|
|
966
889
|
});
|
|
967
|
-
const latestSession = getLatestAgentSession(artifactsDir, "
|
|
968
|
-
const previousState = readAgentState(artifactsDir, "
|
|
890
|
+
const latestSession = getLatestAgentSession(artifactsDir, "mayor");
|
|
891
|
+
const previousState = readAgentState(artifactsDir, "mayor");
|
|
969
892
|
if (previousState !== null) writeAgentState(artifactsDir, createAgentState({
|
|
970
893
|
...previousState,
|
|
971
894
|
status: exitCode === 0 ? "idle" : "blocked",
|
|
@@ -983,7 +906,7 @@ function startFreshMayorSession(repoRoot, artifactsDir) {
|
|
|
983
906
|
function openTownMayor(argv = process.argv.slice(2)) {
|
|
984
907
|
const repo = resolveRepoContext(argv);
|
|
985
908
|
const message = repo.rest.join(" ").trim();
|
|
986
|
-
if (readAgentState(repo.artifactsDir, "
|
|
909
|
+
if (readAgentState(repo.artifactsDir, "mayor") === null) {
|
|
987
910
|
assertCommandAvailable("pi");
|
|
988
911
|
if (message) {
|
|
989
912
|
runTown([
|
|
@@ -1029,7 +952,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
|
|
|
1029
952
|
from: "human",
|
|
1030
953
|
body
|
|
1031
954
|
});
|
|
1032
|
-
const deliveredResult = state.role === "
|
|
955
|
+
const deliveredResult = state.role === "mayor" ? runAgentTurn({
|
|
1033
956
|
repoRoot: repo.repoRoot,
|
|
1034
957
|
artifactsDir: repo.artifactsDir,
|
|
1035
958
|
agentId,
|
|
@@ -1047,7 +970,7 @@ function messageTownAgent(argv = process.argv.slice(2)) {
|
|
|
1047
970
|
console.log(`- queued message: ${body}`);
|
|
1048
971
|
if (deliveredResult) {
|
|
1049
972
|
console.log(`- delivered to session: ${deliveredResult.latestSession.sessionPath}`);
|
|
1050
|
-
console.log(`-
|
|
973
|
+
console.log(`- mayor response: ${deliveredResult.completionMessage}`);
|
|
1051
974
|
}
|
|
1052
975
|
}
|
|
1053
976
|
|
|
@@ -1093,6 +1016,7 @@ function parseSpawnFlags(argv) {
|
|
|
1093
1016
|
let task = null;
|
|
1094
1017
|
for (let index = 0; index < argv.length; index += 1) {
|
|
1095
1018
|
const arg = argv[index];
|
|
1019
|
+
if (arg === void 0) continue;
|
|
1096
1020
|
if (arg.startsWith("--role=")) {
|
|
1097
1021
|
role = arg.slice(7);
|
|
1098
1022
|
continue;
|
|
@@ -1137,7 +1061,7 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
|
|
|
1137
1061
|
const flags = parseSpawnFlags(repo.rest);
|
|
1138
1062
|
const agentId = flags.agentId ?? `${flags.role}-${Date.now()}`;
|
|
1139
1063
|
const task = flags.task;
|
|
1140
|
-
const {
|
|
1064
|
+
const { launch, latestSession } = spawnAgent({
|
|
1141
1065
|
repoRoot: repo.repoRoot,
|
|
1142
1066
|
artifactsDir: repo.artifactsDir,
|
|
1143
1067
|
role: flags.role,
|
|
@@ -1149,18 +1073,122 @@ function spawnTownAgent(argv = process.argv.slice(2)) {
|
|
|
1149
1073
|
console.log(`- repo root: ${repo.repoRoot}`);
|
|
1150
1074
|
console.log(`- agent: ${agentId}`);
|
|
1151
1075
|
console.log(`- role: ${flags.role}`);
|
|
1152
|
-
console.log(`-
|
|
1076
|
+
console.log(`- status: running`);
|
|
1077
|
+
console.log(`- launch pid: ${launch.processId}`);
|
|
1153
1078
|
if (task) console.log(`- task: ${task}`);
|
|
1154
1079
|
if (latestSession.sessionPath) console.log(`- session: ${latestSession.sessionPath}`);
|
|
1080
|
+
else if (latestSession.sessionDir) console.log(`- session dir: ${latestSession.sessionDir}`);
|
|
1155
1081
|
}
|
|
1156
1082
|
|
|
1157
1083
|
//#endregion
|
|
1158
|
-
//#region
|
|
1159
|
-
|
|
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
|
+
}
|
|
1160
1187
|
|
|
1161
1188
|
//#endregion
|
|
1162
1189
|
//#region src/version.ts
|
|
1163
|
-
const
|
|
1190
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1191
|
+
const CLI_VERSION = packageJson.version;
|
|
1164
1192
|
|
|
1165
1193
|
//#endregion
|
|
1166
1194
|
//#region src/index.ts
|
|
@@ -1169,27 +1197,23 @@ function printHelp(showAdvanced = false) {
|
|
|
1169
1197
|
"pitown",
|
|
1170
1198
|
"",
|
|
1171
1199
|
"Usage:",
|
|
1172
|
-
" pitown",
|
|
1173
|
-
" pitown mayor [--repo <path>] [\"message\"]",
|
|
1200
|
+
" pitown [--repo <path>] [\"message\"]",
|
|
1174
1201
|
" pitown board [--repo <path>]",
|
|
1175
1202
|
" pitown peek [--repo <path>] [agent]",
|
|
1176
|
-
" pitown msg [--repo <path>]
|
|
1203
|
+
" pitown msg [--repo <path>] <agent> \"message\"",
|
|
1177
1204
|
" pitown status [--repo <path>]",
|
|
1205
|
+
" pitown stop [--repo <path>] [--agent <id>] [--all] [--force]",
|
|
1178
1206
|
" pitown doctor",
|
|
1179
|
-
" pitown help",
|
|
1180
|
-
" pitown help --all",
|
|
1181
|
-
" pitown --help",
|
|
1182
|
-
" pitown -v",
|
|
1183
1207
|
" pitown --version",
|
|
1184
1208
|
"",
|
|
1185
1209
|
"Mayor workflow:",
|
|
1186
1210
|
" pitown",
|
|
1187
|
-
" pitown
|
|
1188
|
-
" pitown mayor \"plan the next milestones\"",
|
|
1211
|
+
" pitown \"plan the next milestones\"",
|
|
1189
1212
|
" /plan",
|
|
1190
1213
|
" /todos",
|
|
1191
1214
|
"",
|
|
1192
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`.",
|
|
1193
1217
|
"",
|
|
1194
1218
|
"If --repo is omitted, Pi Town uses the repo for the current working directory when possible.",
|
|
1195
1219
|
...showAdvanced ? [
|
|
@@ -1224,7 +1248,8 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
1224
1248
|
break;
|
|
1225
1249
|
case "run": {
|
|
1226
1250
|
const result = runTown(args);
|
|
1227
|
-
|
|
1251
|
+
const latestIteration = result.iterations[result.iterations.length - 1];
|
|
1252
|
+
if (latestIteration && latestIteration.controllerResult.piInvocation.exitCode !== 0) process.exitCode = latestIteration.controllerResult.piInvocation.exitCode;
|
|
1228
1253
|
break;
|
|
1229
1254
|
}
|
|
1230
1255
|
case "loop": {
|
|
@@ -1260,6 +1285,9 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
1260
1285
|
case "status":
|
|
1261
1286
|
showTownStatus(args);
|
|
1262
1287
|
break;
|
|
1288
|
+
case "stop":
|
|
1289
|
+
stopTown(args);
|
|
1290
|
+
break;
|
|
1263
1291
|
case "watch":
|
|
1264
1292
|
watchTown(args);
|
|
1265
1293
|
break;
|