@pitboss/cli 0.4.8 → 0.4.10
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/dist/cli.js +754 -288
- package/dist/event-worker.js +198 -59
- package/dist/mcp.js +100 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10864,7 +10864,7 @@ var require_lib2 = __commonJS({
|
|
|
10864
10864
|
return matches;
|
|
10865
10865
|
};
|
|
10866
10866
|
exports.analyse = analyse;
|
|
10867
|
-
var detectFile = (filepath, opts = {}) => new Promise((
|
|
10867
|
+
var detectFile = (filepath, opts = {}) => new Promise((resolve9, reject) => {
|
|
10868
10868
|
let fd;
|
|
10869
10869
|
const fs = (0, node_1.default)();
|
|
10870
10870
|
const handler = (err, buffer) => {
|
|
@@ -10874,7 +10874,7 @@ var require_lib2 = __commonJS({
|
|
|
10874
10874
|
if (err) {
|
|
10875
10875
|
reject(err);
|
|
10876
10876
|
} else if (buffer) {
|
|
10877
|
-
|
|
10877
|
+
resolve9((0, exports.detect)(buffer));
|
|
10878
10878
|
} else {
|
|
10879
10879
|
reject(new Error("No error and no buffer received"));
|
|
10880
10880
|
}
|
|
@@ -17281,13 +17281,13 @@ var PromisePolyfill = class extends Promise {
|
|
|
17281
17281
|
// Available starting from Node 22
|
|
17282
17282
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
|
|
17283
17283
|
static withResolver() {
|
|
17284
|
-
let
|
|
17284
|
+
let resolve9;
|
|
17285
17285
|
let reject;
|
|
17286
17286
|
const promise = new Promise((res, rej) => {
|
|
17287
|
-
|
|
17287
|
+
resolve9 = res;
|
|
17288
17288
|
reject = rej;
|
|
17289
17289
|
});
|
|
17290
|
-
return { promise, resolve:
|
|
17290
|
+
return { promise, resolve: resolve9, reject };
|
|
17291
17291
|
}
|
|
17292
17292
|
};
|
|
17293
17293
|
|
|
@@ -17324,7 +17324,7 @@ function createPrompt(view) {
|
|
|
17324
17324
|
});
|
|
17325
17325
|
output.mute();
|
|
17326
17326
|
const screen = new ScreenManager(rl);
|
|
17327
|
-
const { promise, resolve:
|
|
17327
|
+
const { promise, resolve: resolve9, reject } = PromisePolyfill.withResolver();
|
|
17328
17328
|
const cancel = () => reject(new CancelPromptError());
|
|
17329
17329
|
if (signal) {
|
|
17330
17330
|
const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
|
|
@@ -17355,7 +17355,7 @@ function createPrompt(view) {
|
|
|
17355
17355
|
try {
|
|
17356
17356
|
const nextView = view(config, (value) => {
|
|
17357
17357
|
if (effectsSettled) {
|
|
17358
|
-
|
|
17358
|
+
resolve9(value);
|
|
17359
17359
|
} else {
|
|
17360
17360
|
pendingDone = { value };
|
|
17361
17361
|
}
|
|
@@ -17378,7 +17378,7 @@ function createPrompt(view) {
|
|
|
17378
17378
|
if (pendingDone !== null) {
|
|
17379
17379
|
const { value } = pendingDone;
|
|
17380
17380
|
pendingDone = null;
|
|
17381
|
-
|
|
17381
|
+
resolve9(value);
|
|
17382
17382
|
}
|
|
17383
17383
|
});
|
|
17384
17384
|
};
|
|
@@ -17533,7 +17533,7 @@ var ExternalEditor = class {
|
|
|
17533
17533
|
}
|
|
17534
17534
|
runAsync(callback) {
|
|
17535
17535
|
this.createTempFile();
|
|
17536
|
-
const promise = new Promise((
|
|
17536
|
+
const promise = new Promise((resolve9, reject) => {
|
|
17537
17537
|
try {
|
|
17538
17538
|
const editorProcess = spawn(this.editor.bin, this.editorArgs(), {
|
|
17539
17539
|
shell: false,
|
|
@@ -17544,7 +17544,7 @@ var ExternalEditor = class {
|
|
|
17544
17544
|
});
|
|
17545
17545
|
editorProcess.once("exit", (code) => {
|
|
17546
17546
|
this.lastExitStatus = code ?? 0;
|
|
17547
|
-
|
|
17547
|
+
resolve9();
|
|
17548
17548
|
});
|
|
17549
17549
|
} catch (launchError) {
|
|
17550
17550
|
reject(new LaunchEditorError(launchError));
|
|
@@ -19107,6 +19107,7 @@ function isWorkerType(value) {
|
|
|
19107
19107
|
return typeof value === "string" && WORKER_TYPES.includes(value);
|
|
19108
19108
|
}
|
|
19109
19109
|
var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
|
|
19110
|
+
var DEFAULT_REVIEW_SILENT_MS = 40 * 60 * 1e3;
|
|
19110
19111
|
var POOL_STOPPED_REASON = "pool member stopped by pitboss node";
|
|
19111
19112
|
|
|
19112
19113
|
// src/lib/runtimes.ts
|
|
@@ -20287,6 +20288,8 @@ function summarize(e, bodyChars = 80, agentId) {
|
|
|
20287
20288
|
return `review ${p.review?.decision ?? "submitted"}: "${taskTitle}"`;
|
|
20288
20289
|
case "task.review_overdue":
|
|
20289
20290
|
return `review OVERDUE: "${taskTitle}"`;
|
|
20291
|
+
case "task.review_silent":
|
|
20292
|
+
return `review SILENT: "${taskTitle}"`;
|
|
20290
20293
|
case "thread.created":
|
|
20291
20294
|
return `new thread: "${threadTitle}"`;
|
|
20292
20295
|
case "thread.concluded":
|
|
@@ -20324,6 +20327,7 @@ var KIND_STYLE = {
|
|
|
20324
20327
|
"task.review_started": source_default.magenta,
|
|
20325
20328
|
"task.review_submitted": source_default.magenta,
|
|
20326
20329
|
"task.review_overdue": source_default.yellow,
|
|
20330
|
+
"task.review_silent": source_default.yellow,
|
|
20327
20331
|
"thread.created": source_default.cyan,
|
|
20328
20332
|
"thread.concluded": source_default.dim,
|
|
20329
20333
|
"thread.message_ceiling": source_default.yellow
|
|
@@ -20599,14 +20603,13 @@ function hookCursorPromptCommand(opts) {
|
|
|
20599
20603
|
process.stdout.write("{}");
|
|
20600
20604
|
}
|
|
20601
20605
|
|
|
20602
|
-
// src/commands/
|
|
20603
|
-
import { execFileSync as execFileSync7
|
|
20604
|
-
import {
|
|
20605
|
-
import {
|
|
20606
|
-
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
20606
|
+
// src/commands/onboard.ts
|
|
20607
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
20608
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync13, realpathSync as realpathSync5, writeFileSync as writeFileSync9 } from "node:fs";
|
|
20609
|
+
import { homedir as homedir6 } from "node:os";
|
|
20607
20610
|
|
|
20608
|
-
// src/node/
|
|
20609
|
-
import { join as join15, resolve as resolve4
|
|
20611
|
+
// src/node/onboard.ts
|
|
20612
|
+
import { join as join15, resolve as resolve4 } from "node:path";
|
|
20610
20613
|
|
|
20611
20614
|
// ../claude-worker/src/session.ts
|
|
20612
20615
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -20667,7 +20670,8 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
|
|
|
20667
20670
|
a one-line status on the task thread naming what you are checking) when you begin, then
|
|
20668
20671
|
run the tests, compare the diff against the spec the task names, then
|
|
20669
20672
|
\`mcp__relai__submit_review\` \u2014 \`approve\`, or \`reject\` with a specific note the author
|
|
20670
|
-
can act on.
|
|
20673
|
+
can act on. When the task is a goal's build with a PR, pass \`headSha\`: the full sha of the
|
|
20674
|
+
commit you reviewed. Do this even when get_my_tasks is empty.
|
|
20671
20675
|
If the change touches a long-running service or a third-party runtime integration (SSE/event streams, launchd/systemd, auth headers, a client library), run the changed path once before approving and quote what it printed. A type cast past a library's own types is a reason to stop and run it.
|
|
20672
20676
|
|
|
20673
20677
|
4. Call \`mcp__relai__get_my_tasks\` \u2014 if it returns no tasks, stop immediately.
|
|
@@ -20909,7 +20913,11 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20909
20913
|
\`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
|
|
20910
20914
|
the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
|
|
20911
20915
|
\`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
|
|
20912
|
-
|
|
20916
|
+
Before writing any build task, choose the goal's reviewer (for a \`reviewer_agent\` gate): a
|
|
20917
|
+
\`reviewer\` agent, on a different \`workerType\` from the writers when one exists. Never assign that
|
|
20918
|
+
agent a build task, and never leave the only reviewers busy building: a goal whose every reviewer
|
|
20919
|
+
built part of it has nobody left to review it. The API defaults a build task with no
|
|
20920
|
+
\`specialization\` to \`writer\` when the repo has writers. Make them small
|
|
20913
20921
|
and independently testable so several agents can work at once. Use \`blockedBy\` only where
|
|
20914
20922
|
one task truly needs another's result. Each description is self-contained: what to change,
|
|
20915
20923
|
which files, how to test it, and the acceptance criteria.
|
|
@@ -21052,6 +21060,9 @@ var USAGE_NUMBER_KEYS = [
|
|
|
21052
21060
|
"cache_read_input_tokens",
|
|
21053
21061
|
"cache_creation_input_tokens"
|
|
21054
21062
|
];
|
|
21063
|
+
function notReportedUsage(reason) {
|
|
21064
|
+
return { notReported: true, reason };
|
|
21065
|
+
}
|
|
21055
21066
|
function finiteNumber(value) {
|
|
21056
21067
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
21057
21068
|
}
|
|
@@ -21119,14 +21130,32 @@ async function getTaskStatus(conn, id, logPrefix) {
|
|
|
21119
21130
|
return void 0;
|
|
21120
21131
|
}
|
|
21121
21132
|
}
|
|
21122
|
-
async function
|
|
21133
|
+
async function listUpdatedSince(conn, since, logPrefix) {
|
|
21134
|
+
try {
|
|
21135
|
+
const res = await fetch(
|
|
21136
|
+
`${conn.apiUrl}/tasks?repoId=${conn.repoId}&assignedTo=${conn.agentId}&updatedAfter=${encodeURIComponent(since)}`,
|
|
21137
|
+
{ headers: { Authorization: `Bearer ${conn.apiSecret}` } }
|
|
21138
|
+
);
|
|
21139
|
+
if (!res.ok) throw new Error(`task list failed (${res.status} ${res.statusText})`);
|
|
21140
|
+
const { data } = await res.json();
|
|
21141
|
+
return (data ?? []).filter((row) => typeof row.id === "string" && row.id.length > 0).filter((row) => row.status !== "assigned").map((row) => row.id);
|
|
21142
|
+
} catch (err) {
|
|
21143
|
+
console.error(
|
|
21144
|
+
`${logPrefix} Could not list tasks updated since the session started:`,
|
|
21145
|
+
err instanceof Error ? err.message : String(err)
|
|
21146
|
+
);
|
|
21147
|
+
return [];
|
|
21148
|
+
}
|
|
21149
|
+
}
|
|
21150
|
+
async function sessionTouchedTaskIds(conn, candidates, logPrefix, since) {
|
|
21123
21151
|
const touched = [];
|
|
21124
21152
|
for (const id of candidates) {
|
|
21125
21153
|
const status = await getTaskStatus(conn, id, logPrefix);
|
|
21126
21154
|
if (status !== void 0 && status !== "assigned") touched.push(id);
|
|
21127
21155
|
}
|
|
21128
21156
|
const inProgressAtClose = await listSessionTaskIds(conn, logPrefix, "in_progress");
|
|
21129
|
-
|
|
21157
|
+
const updated = since ? await listUpdatedSince(conn, since, logPrefix) : [];
|
|
21158
|
+
return unionTaskIds(touched, inProgressAtClose, updated);
|
|
21130
21159
|
}
|
|
21131
21160
|
function unionTaskIds(...lists) {
|
|
21132
21161
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -21140,34 +21169,99 @@ function unionTaskIds(...lists) {
|
|
|
21140
21169
|
}
|
|
21141
21170
|
return out;
|
|
21142
21171
|
}
|
|
21143
|
-
async function writeSessionUsage(conn, taskIds, usage, logPrefix) {
|
|
21144
|
-
const sessionUsage = {
|
|
21145
|
-
...usage,
|
|
21146
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21147
|
-
sessionTaskIds: [...taskIds]
|
|
21148
|
-
};
|
|
21172
|
+
async function writeSessionUsage(conn, taskIds, usage, logPrefix, opts = {}) {
|
|
21149
21173
|
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${conn.apiSecret}` };
|
|
21150
21174
|
for (const id of taskIds) {
|
|
21175
|
+
const slice = taskIds.length > 1 && !usage.notReported ? opts.slices?.[id] : void 0;
|
|
21176
|
+
const share = usage.notReported ? void 0 : slice || taskIds.length === 1 ? "task" : "session";
|
|
21177
|
+
const body = {
|
|
21178
|
+
usage: slice ?? usage,
|
|
21179
|
+
sessionTaskIds: taskIds,
|
|
21180
|
+
...opts.model ? { model: opts.model } : {},
|
|
21181
|
+
...share ? { share } : {}
|
|
21182
|
+
};
|
|
21151
21183
|
try {
|
|
21152
|
-
const
|
|
21153
|
-
method: "
|
|
21184
|
+
const res = await fetch(`${conn.apiUrl}/tasks/${id}/usage-report`, {
|
|
21185
|
+
method: "POST",
|
|
21154
21186
|
headers,
|
|
21155
|
-
body: JSON.stringify(
|
|
21187
|
+
body: JSON.stringify(body)
|
|
21156
21188
|
});
|
|
21157
|
-
if (!
|
|
21158
|
-
console.error(`${logPrefix} Could not
|
|
21189
|
+
if (!res.ok) {
|
|
21190
|
+
console.error(`${logPrefix} Could not report usage on ${id} (${res.status} ${res.statusText})`);
|
|
21159
21191
|
}
|
|
21160
21192
|
} catch (err) {
|
|
21161
21193
|
console.error(
|
|
21162
|
-
`${logPrefix} Could not
|
|
21194
|
+
`${logPrefix} Could not report usage on ${id}:`,
|
|
21163
21195
|
err instanceof Error ? err.message : String(err)
|
|
21164
21196
|
);
|
|
21165
21197
|
}
|
|
21166
21198
|
}
|
|
21167
21199
|
}
|
|
21168
|
-
async function recordSessionUsage(conn, startIds, usage, logPrefix) {
|
|
21200
|
+
async function recordSessionUsage(conn, startIds, usage, logPrefix, opts = {}) {
|
|
21169
21201
|
const start = await startIds;
|
|
21170
|
-
|
|
21202
|
+
const { since, ...rest } = opts;
|
|
21203
|
+
await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix, since), usage, logPrefix, rest);
|
|
21204
|
+
}
|
|
21205
|
+
var BOUNDARY_STATUSES = /* @__PURE__ */ new Set(["completed", "blocked", "pending_verification"]);
|
|
21206
|
+
function addUsage(into, from) {
|
|
21207
|
+
into.num_turns = (into.num_turns ?? 0) + (from.num_turns ?? 0);
|
|
21208
|
+
const total = into.usage ?? {};
|
|
21209
|
+
for (const key of USAGE_NUMBER_KEYS) {
|
|
21210
|
+
const n = from.usage?.[key];
|
|
21211
|
+
if (n !== void 0) total[key] = (total[key] ?? 0) + n;
|
|
21212
|
+
}
|
|
21213
|
+
into.usage = total;
|
|
21214
|
+
}
|
|
21215
|
+
function createUsageTracker() {
|
|
21216
|
+
let model;
|
|
21217
|
+
let pending = /* @__PURE__ */ new Map();
|
|
21218
|
+
const slices = {};
|
|
21219
|
+
let anon = 0;
|
|
21220
|
+
function cut(taskId) {
|
|
21221
|
+
const slice = slices[taskId] ?? {};
|
|
21222
|
+
for (const messageUsage of pending.values()) addUsage(slice, messageUsage);
|
|
21223
|
+
pending = /* @__PURE__ */ new Map();
|
|
21224
|
+
if (slice.usage) slices[taskId] = slice;
|
|
21225
|
+
}
|
|
21226
|
+
return {
|
|
21227
|
+
observe(event) {
|
|
21228
|
+
if (event.type === "system" && event.subtype === "init") {
|
|
21229
|
+
if (typeof event.model === "string" && event.model) model = event.model;
|
|
21230
|
+
return;
|
|
21231
|
+
}
|
|
21232
|
+
if (event.type !== "assistant") return;
|
|
21233
|
+
const message = event.message ?? {};
|
|
21234
|
+
const raw = message.usage;
|
|
21235
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
21236
|
+
const usage = pickNumbers(raw, USAGE_NUMBER_KEYS);
|
|
21237
|
+
if (Object.keys(usage).length > 0) {
|
|
21238
|
+
pending.set(typeof message.id === "string" ? message.id : `anon_${anon++}`, { num_turns: 1, usage });
|
|
21239
|
+
}
|
|
21240
|
+
}
|
|
21241
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
21242
|
+
for (const block of content) {
|
|
21243
|
+
if (block.type !== "tool_use" || typeof block.name !== "string" || !block.name.endsWith("update_task_status")) continue;
|
|
21244
|
+
const input = block.input ?? {};
|
|
21245
|
+
if (typeof input.taskId === "string" && typeof input.status === "string" && BOUNDARY_STATUSES.has(input.status)) {
|
|
21246
|
+
cut(input.taskId);
|
|
21247
|
+
}
|
|
21248
|
+
}
|
|
21249
|
+
},
|
|
21250
|
+
model: () => model,
|
|
21251
|
+
opts: () => ({ ...model ? { model } : {}, slices })
|
|
21252
|
+
};
|
|
21253
|
+
}
|
|
21254
|
+
|
|
21255
|
+
// ../claude-worker/src/model.ts
|
|
21256
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
21257
|
+
function pinnedModel(env2 = process.env) {
|
|
21258
|
+
const file = env2.PITBOSS_MODEL_FILE;
|
|
21259
|
+
if (!file) return void 0;
|
|
21260
|
+
try {
|
|
21261
|
+
return readFileSync11(file, "utf-8").trim() || void 0;
|
|
21262
|
+
} catch {
|
|
21263
|
+
return void 0;
|
|
21264
|
+
}
|
|
21171
21265
|
}
|
|
21172
21266
|
|
|
21173
21267
|
// ../claude-worker/src/session.ts
|
|
@@ -21202,26 +21296,36 @@ function writeMcpConfig(config, prefix = "claude-worker") {
|
|
|
21202
21296
|
writeFileSync6(path3, JSON.stringify(mcpConfig), { mode: 384 });
|
|
21203
21297
|
return path3;
|
|
21204
21298
|
}
|
|
21299
|
+
function claudeArgs(opts) {
|
|
21300
|
+
return [
|
|
21301
|
+
"--print",
|
|
21302
|
+
"--mcp-config",
|
|
21303
|
+
opts.mcpConfigPath,
|
|
21304
|
+
"--no-session-persistence",
|
|
21305
|
+
"--dangerously-skip-permissions",
|
|
21306
|
+
"--output-format",
|
|
21307
|
+
"stream-json",
|
|
21308
|
+
"--verbose",
|
|
21309
|
+
"--model",
|
|
21310
|
+
opts.model,
|
|
21311
|
+
"--add-dir",
|
|
21312
|
+
opts.repoPath,
|
|
21313
|
+
...opts.isolateUserConfig ? ["--setting-sources", "project,local"] : []
|
|
21314
|
+
];
|
|
21315
|
+
}
|
|
21205
21316
|
async function runClaudeSession(config) {
|
|
21206
21317
|
const prompt = buildPrompt(config);
|
|
21207
21318
|
const mcpConfigPath = writeMcpConfig(config);
|
|
21319
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21208
21320
|
const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
|
|
21209
21321
|
try {
|
|
21210
|
-
return await new Promise((
|
|
21211
|
-
const args =
|
|
21212
|
-
"--print",
|
|
21213
|
-
"--mcp-config",
|
|
21322
|
+
return await new Promise((resolve9, reject) => {
|
|
21323
|
+
const args = claudeArgs({
|
|
21214
21324
|
mcpConfigPath,
|
|
21215
|
-
|
|
21216
|
-
|
|
21217
|
-
"
|
|
21218
|
-
|
|
21219
|
-
"--verbose",
|
|
21220
|
-
"--model",
|
|
21221
|
-
config.model,
|
|
21222
|
-
"--add-dir",
|
|
21223
|
-
config.repoPath
|
|
21224
|
-
];
|
|
21325
|
+
model: pinnedModel() ?? config.model,
|
|
21326
|
+
repoPath: config.repoPath,
|
|
21327
|
+
isolateUserConfig: process.env.PITBOSS_ISOLATE_USER_CONFIG === "1"
|
|
21328
|
+
});
|
|
21225
21329
|
console.log(`[claude-worker] Spawning: ${config.claudeBin} ${args.slice(0, 4).join(" ")} ...`);
|
|
21226
21330
|
console.log(`[claude-worker] cwd: ${config.repoPath}`);
|
|
21227
21331
|
const { ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ...workerEnv } = process.env;
|
|
@@ -21237,6 +21341,8 @@ async function runClaudeSession(config) {
|
|
|
21237
21341
|
let buffer = "";
|
|
21238
21342
|
let resultError = "";
|
|
21239
21343
|
let sessionUsage;
|
|
21344
|
+
let sawResult = false;
|
|
21345
|
+
const tracker = createUsageTracker();
|
|
21240
21346
|
proc.stdout.on("data", (chunk) => {
|
|
21241
21347
|
buffer += chunk.toString();
|
|
21242
21348
|
const lines = buffer.split("\n");
|
|
@@ -21245,6 +21351,7 @@ async function runClaudeSession(config) {
|
|
|
21245
21351
|
if (!line.trim()) continue;
|
|
21246
21352
|
try {
|
|
21247
21353
|
const event = JSON.parse(line);
|
|
21354
|
+
tracker.observe(event);
|
|
21248
21355
|
if (event.type === "assistant") {
|
|
21249
21356
|
const content = event.message?.content ?? [];
|
|
21250
21357
|
for (const block of content) {
|
|
@@ -21258,6 +21365,7 @@ async function runClaudeSession(config) {
|
|
|
21258
21365
|
}
|
|
21259
21366
|
}
|
|
21260
21367
|
} else if (event.type === "result") {
|
|
21368
|
+
sawResult = true;
|
|
21261
21369
|
const parsed = parseResultUsage(event);
|
|
21262
21370
|
if (parsed) sessionUsage = parsed;
|
|
21263
21371
|
if (event.is_error) {
|
|
@@ -21279,15 +21387,15 @@ async function runClaudeSession(config) {
|
|
|
21279
21387
|
proc.on("close", (code, signal) => {
|
|
21280
21388
|
const unique = [...new Set(toolsUsed)];
|
|
21281
21389
|
console.log(`[claude-worker] Done \u2014 tools used: ${unique.length ? unique.join(", ") : "none"}`);
|
|
21282
|
-
const usage = sessionUsage;
|
|
21283
|
-
if (
|
|
21284
|
-
const finish =
|
|
21390
|
+
const usage = sessionUsage ?? notReportedUsage(sawResult ? "adapter" : "killed");
|
|
21391
|
+
if (sessionUsage) console.log(formatSessionUsageLine("[claude-worker]", sessionUsage));
|
|
21392
|
+
const finish = recordSessionUsage(config, taskIdsPromise, usage, "[claude-worker]", { since: startedAt, ...tracker.opts() });
|
|
21285
21393
|
const settle = () => {
|
|
21286
21394
|
if (signal || code !== 0 && code !== null) {
|
|
21287
21395
|
const detail = (resultError || stderrOutput || "").trim();
|
|
21288
21396
|
reject(new Error(`claude exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21289
21397
|
} else {
|
|
21290
|
-
|
|
21398
|
+
resolve9(unique);
|
|
21291
21399
|
}
|
|
21292
21400
|
};
|
|
21293
21401
|
finish.then(settle, settle);
|
|
@@ -21339,8 +21447,8 @@ function classifySessionError(text) {
|
|
|
21339
21447
|
|
|
21340
21448
|
// ../claude-worker/src/adapters/cursor.ts
|
|
21341
21449
|
var bin = () => process.env.CURSOR_BIN ?? "cursor-agent";
|
|
21342
|
-
function cursorArgs(repoPath, prompt) {
|
|
21343
|
-
return ["-p", "--trust", "--force", "--output-format", "stream-json", "--workspace", repoPath, prompt];
|
|
21450
|
+
function cursorArgs(repoPath, prompt, model) {
|
|
21451
|
+
return ["-p", "--trust", "--force", "--output-format", "stream-json", "--workspace", repoPath, ...model ? ["--model", model] : [], prompt];
|
|
21344
21452
|
}
|
|
21345
21453
|
function readCursorEvent(line) {
|
|
21346
21454
|
let event;
|
|
@@ -21349,6 +21457,9 @@ function readCursorEvent(line) {
|
|
|
21349
21457
|
} catch {
|
|
21350
21458
|
return {};
|
|
21351
21459
|
}
|
|
21460
|
+
if (event.type === "system" && event.subtype === "init") {
|
|
21461
|
+
return typeof event.model === "string" && event.model ? { model: event.model } : {};
|
|
21462
|
+
}
|
|
21352
21463
|
if (event.type === "tool_call" && event.subtype === "started") {
|
|
21353
21464
|
const call = event.tool_call ?? {};
|
|
21354
21465
|
const [kind] = Object.keys(call);
|
|
@@ -21364,10 +21475,11 @@ function readCursorEvent(line) {
|
|
|
21364
21475
|
return {};
|
|
21365
21476
|
}
|
|
21366
21477
|
async function runCursorSession(config) {
|
|
21367
|
-
const args = cursorArgs(config.repoPath, buildPrompt(config));
|
|
21478
|
+
const args = cursorArgs(config.repoPath, buildPrompt(config), pinnedModel());
|
|
21368
21479
|
console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
|
|
21480
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21369
21481
|
const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
|
|
21370
|
-
return new Promise((
|
|
21482
|
+
return new Promise((resolve9, reject) => {
|
|
21371
21483
|
const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21372
21484
|
console.log(`[cursor-worker] Process PID: ${proc.pid}`);
|
|
21373
21485
|
const toolsUsed = [];
|
|
@@ -21375,12 +21487,14 @@ async function runCursorSession(config) {
|
|
|
21375
21487
|
let resultError = "";
|
|
21376
21488
|
let stderrOutput = "";
|
|
21377
21489
|
let sessionUsage;
|
|
21490
|
+
let model;
|
|
21378
21491
|
proc.stdout.on("data", (chunk) => {
|
|
21379
21492
|
buffer += chunk.toString();
|
|
21380
21493
|
const lines = buffer.split("\n");
|
|
21381
21494
|
buffer = lines.pop() ?? "";
|
|
21382
21495
|
for (const line of lines) {
|
|
21383
|
-
const { tool, error, usage } = readCursorEvent(line);
|
|
21496
|
+
const { tool, error, usage, model: initModel } = readCursorEvent(line);
|
|
21497
|
+
if (initModel) model = initModel;
|
|
21384
21498
|
if (tool) {
|
|
21385
21499
|
toolsUsed.push(tool);
|
|
21386
21500
|
process.stdout.write(` \u2699 ${tool}
|
|
@@ -21398,15 +21512,15 @@ async function runCursorSession(config) {
|
|
|
21398
21512
|
});
|
|
21399
21513
|
proc.on("close", (code, signal) => {
|
|
21400
21514
|
console.log(`[cursor-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
21401
|
-
const usage = sessionUsage;
|
|
21402
|
-
if (
|
|
21403
|
-
const finish =
|
|
21515
|
+
const usage = sessionUsage ?? notReportedUsage("killed");
|
|
21516
|
+
if (sessionUsage) console.log(formatSessionUsageLine("[cursor-worker]", sessionUsage));
|
|
21517
|
+
const finish = recordSessionUsage(config, taskIdsPromise, usage, "[cursor-worker]", { since: startedAt, ...model ? { model } : {} });
|
|
21404
21518
|
const settle = () => {
|
|
21405
21519
|
if (signal || code !== 0 && code !== null) {
|
|
21406
21520
|
const detail = (resultError || stderrOutput).trim();
|
|
21407
21521
|
reject(new Error(`cursor-agent exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21408
21522
|
} else {
|
|
21409
|
-
|
|
21523
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21410
21524
|
}
|
|
21411
21525
|
};
|
|
21412
21526
|
finish.then(settle, settle);
|
|
@@ -21445,10 +21559,11 @@ import { unlinkSync as unlinkSync2 } from "fs";
|
|
|
21445
21559
|
var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
|
|
21446
21560
|
var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
|
|
21447
21561
|
var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
|
|
21448
|
-
function copilotArgs(repoPath, prompt, mcpConfigPath) {
|
|
21562
|
+
function copilotArgs(repoPath, prompt, mcpConfigPath, model) {
|
|
21449
21563
|
return [
|
|
21450
21564
|
"-p",
|
|
21451
21565
|
prompt,
|
|
21566
|
+
...model ? ["--model", model] : [],
|
|
21452
21567
|
"--allow-all-tools",
|
|
21453
21568
|
"--output-format",
|
|
21454
21569
|
"json",
|
|
@@ -21470,6 +21585,7 @@ function readCopilotEvent(line) {
|
|
|
21470
21585
|
const type = typeof event.type === "string" ? event.type : "";
|
|
21471
21586
|
const data = event.data ?? {};
|
|
21472
21587
|
const message = typeof data.message === "string" ? data.message : "";
|
|
21588
|
+
if (type === "session.tools_updated" && typeof data.model === "string" && data.model) return { model: data.model };
|
|
21473
21589
|
if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
|
|
21474
21590
|
if (type === "session.error") return { error: message || "copilot session error" };
|
|
21475
21591
|
if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
|
|
@@ -21478,21 +21594,25 @@ function readCopilotEvent(line) {
|
|
|
21478
21594
|
}
|
|
21479
21595
|
async function runCopilotSession(config) {
|
|
21480
21596
|
const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
|
|
21481
|
-
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
|
|
21597
|
+
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath, pinnedModel());
|
|
21598
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21599
|
+
const taskIdsPromise = listSessionTaskIds(config, "[copilot-worker]");
|
|
21482
21600
|
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
21483
|
-
return new Promise((
|
|
21601
|
+
return new Promise((resolve9, reject) => {
|
|
21484
21602
|
const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21485
21603
|
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
21486
21604
|
const toolsUsed = [];
|
|
21487
21605
|
let buffer = "";
|
|
21488
21606
|
let sessionError = "";
|
|
21489
21607
|
let stderrOutput = "";
|
|
21608
|
+
let model;
|
|
21490
21609
|
proc.stdout.on("data", (chunk) => {
|
|
21491
21610
|
buffer += chunk.toString();
|
|
21492
21611
|
const lines = buffer.split("\n");
|
|
21493
21612
|
buffer = lines.pop() ?? "";
|
|
21494
21613
|
for (const line of lines) {
|
|
21495
|
-
const { tool, error } = readCopilotEvent(line);
|
|
21614
|
+
const { tool, error, model: initModel } = readCopilotEvent(line);
|
|
21615
|
+
if (initModel) model = initModel;
|
|
21496
21616
|
if (tool) {
|
|
21497
21617
|
toolsUsed.push(tool);
|
|
21498
21618
|
process.stdout.write(` \u2699 ${tool}
|
|
@@ -21513,12 +21633,15 @@ async function runCopilotSession(config) {
|
|
|
21513
21633
|
} catch {
|
|
21514
21634
|
}
|
|
21515
21635
|
console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21636
|
+
const settle = () => {
|
|
21637
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
21638
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
21639
|
+
reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21640
|
+
} else {
|
|
21641
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21642
|
+
}
|
|
21643
|
+
};
|
|
21644
|
+
recordSessionUsage(config, taskIdsPromise, notReportedUsage("adapter"), "[copilot-worker]", { since: startedAt, ...model ? { model } : {} }).then(settle, settle);
|
|
21522
21645
|
});
|
|
21523
21646
|
proc.on("error", (err) => {
|
|
21524
21647
|
try {
|
|
@@ -21551,7 +21674,7 @@ var bin3 = () => process.env.CODEX_BIN ?? "codex";
|
|
|
21551
21674
|
var codexHome = () => process.env.CODEX_HOME ?? join14(homedir5(), ".codex");
|
|
21552
21675
|
var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
|
|
21553
21676
|
var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
|
|
21554
|
-
function codexArgs(repoPath, prompt, profile) {
|
|
21677
|
+
function codexArgs(repoPath, prompt, profile, model) {
|
|
21555
21678
|
return [
|
|
21556
21679
|
"exec",
|
|
21557
21680
|
"--json",
|
|
@@ -21562,6 +21685,7 @@ function codexArgs(repoPath, prompt, profile) {
|
|
|
21562
21685
|
repoPath,
|
|
21563
21686
|
"-p",
|
|
21564
21687
|
profile,
|
|
21688
|
+
...model ? ["-m", model] : [],
|
|
21565
21689
|
prompt
|
|
21566
21690
|
];
|
|
21567
21691
|
}
|
|
@@ -21610,9 +21734,11 @@ async function runCodexSession(config) {
|
|
|
21610
21734
|
} catch {
|
|
21611
21735
|
}
|
|
21612
21736
|
};
|
|
21613
|
-
const args = codexArgs(config.repoPath, buildPrompt(config), profile);
|
|
21737
|
+
const args = codexArgs(config.repoPath, buildPrompt(config), profile, pinnedModel());
|
|
21738
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21739
|
+
const taskIdsPromise = listSessionTaskIds(config, "[codex-worker]");
|
|
21614
21740
|
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
21615
|
-
return new Promise((
|
|
21741
|
+
return new Promise((resolve9, reject) => {
|
|
21616
21742
|
const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21617
21743
|
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
21618
21744
|
const toolsUsed = [];
|
|
@@ -21642,12 +21768,15 @@ async function runCodexSession(config) {
|
|
|
21642
21768
|
proc.on("close", (code, signal) => {
|
|
21643
21769
|
cleanup();
|
|
21644
21770
|
console.log(`[codex-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
21645
|
-
|
|
21646
|
-
|
|
21647
|
-
|
|
21648
|
-
|
|
21649
|
-
|
|
21650
|
-
|
|
21771
|
+
const settle = () => {
|
|
21772
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
21773
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
21774
|
+
reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21775
|
+
} else {
|
|
21776
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21777
|
+
}
|
|
21778
|
+
};
|
|
21779
|
+
recordSessionUsage(config, taskIdsPromise, notReportedUsage("adapter"), "[codex-worker]", { since: startedAt }).then(settle, settle);
|
|
21651
21780
|
});
|
|
21652
21781
|
proc.on("error", (err) => {
|
|
21653
21782
|
cleanup();
|
|
@@ -21683,7 +21812,65 @@ var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
|
|
|
21683
21812
|
// ../claude-worker/src/config.ts
|
|
21684
21813
|
var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
|
|
21685
21814
|
|
|
21815
|
+
// src/node/onboard.ts
|
|
21816
|
+
var MAX_POOL = 30;
|
|
21817
|
+
var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
|
|
21818
|
+
function planOnboard(url, opts) {
|
|
21819
|
+
const match = GITHUB.exec(url.trim());
|
|
21820
|
+
if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
|
|
21821
|
+
const [, owner, name] = match;
|
|
21822
|
+
const writer = opts.writer ?? "claude";
|
|
21823
|
+
const reviewer = opts.reviewer ?? (writer === "claude" ? "copilot" : "claude");
|
|
21824
|
+
if (reviewer === writer) throw new Error(`the reviewer needs a different runtime from the writers (both are ${writer})`);
|
|
21825
|
+
for (const r of [writer, reviewer]) {
|
|
21826
|
+
if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
|
|
21827
|
+
}
|
|
21828
|
+
const poolMax = opts.poolMax ?? 2;
|
|
21829
|
+
if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
|
|
21830
|
+
throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
|
|
21831
|
+
}
|
|
21832
|
+
const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
|
|
21833
|
+
const parent = resolve4(opts.parent ?? join15(opts.home, "github"));
|
|
21834
|
+
const base2 = join15(parent, name);
|
|
21835
|
+
const nodeDir2 = join15(parent, `${name}-node`);
|
|
21836
|
+
const architect = join15(parent, `${name}-architect`);
|
|
21837
|
+
const review = join15(parent, `${name}-reviewer`);
|
|
21838
|
+
return {
|
|
21839
|
+
owner,
|
|
21840
|
+
name,
|
|
21841
|
+
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
21842
|
+
base: base2,
|
|
21843
|
+
nodeDir: nodeDir2,
|
|
21844
|
+
specPath: join15(nodeDir2, "spec.json"),
|
|
21845
|
+
configDir: join15(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
|
|
21846
|
+
worktrees: [architect, review],
|
|
21847
|
+
spec: {
|
|
21848
|
+
api: "https://api.pitboss.dev",
|
|
21849
|
+
agents: [
|
|
21850
|
+
{ name: `${id}-architect`, runtime: "claude", worktree: `../${name}-architect`, specialization: "architect", tier: 2 },
|
|
21851
|
+
{ name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
|
|
21852
|
+
{
|
|
21853
|
+
pool: `${id}-pool`,
|
|
21854
|
+
runtime: writer,
|
|
21855
|
+
base: `../${name}`,
|
|
21856
|
+
worktreeRoot: `../${name}-pool`,
|
|
21857
|
+
specialization: "writer",
|
|
21858
|
+
min: 0,
|
|
21859
|
+
max: poolMax
|
|
21860
|
+
}
|
|
21861
|
+
]
|
|
21862
|
+
}
|
|
21863
|
+
};
|
|
21864
|
+
}
|
|
21865
|
+
function originHost(origin) {
|
|
21866
|
+
const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
|
|
21867
|
+
if (url) return url[1].toLowerCase();
|
|
21868
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
|
|
21869
|
+
return scp ? scp[1].toLowerCase() : null;
|
|
21870
|
+
}
|
|
21871
|
+
|
|
21686
21872
|
// src/node/spec.ts
|
|
21873
|
+
import { join as join16, resolve as resolve5, sep as sep2 } from "node:path";
|
|
21687
21874
|
var DEFAULT_API2 = "https://api.pitboss.dev";
|
|
21688
21875
|
var MAX_POOL_SIZE = 32;
|
|
21689
21876
|
var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
@@ -21695,7 +21882,15 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
|
|
|
21695
21882
|
function poolMembers(pool) {
|
|
21696
21883
|
return Array.from({ length: pool.max }, (_, i) => {
|
|
21697
21884
|
const name = `${pool.pool}-${i + 1}`;
|
|
21698
|
-
return {
|
|
21885
|
+
return {
|
|
21886
|
+
name,
|
|
21887
|
+
runtime: pool.runtime,
|
|
21888
|
+
worktree: join16(pool.worktreeRoot, name),
|
|
21889
|
+
specialization: pool.specialization,
|
|
21890
|
+
...pool.tier ? { tier: pool.tier } : {},
|
|
21891
|
+
...pool.model ? { model: pool.model } : {},
|
|
21892
|
+
...pool.isolateUserConfig ? { isolateUserConfig: true } : {}
|
|
21893
|
+
};
|
|
21699
21894
|
});
|
|
21700
21895
|
}
|
|
21701
21896
|
var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
|
|
@@ -21712,6 +21907,24 @@ function specializationOf(a, i) {
|
|
|
21712
21907
|
}
|
|
21713
21908
|
return specialization;
|
|
21714
21909
|
}
|
|
21910
|
+
function tierOf(a, i, specialization) {
|
|
21911
|
+
if (a.tier === void 0) return specialization === "architect" ? { tier: 2 } : {};
|
|
21912
|
+
if (a.tier !== 1 && a.tier !== 2) throw new Error(`agents[${i}].tier must be 1 or 2`);
|
|
21913
|
+
return { tier: a.tier };
|
|
21914
|
+
}
|
|
21915
|
+
function modelOf(a, i) {
|
|
21916
|
+
if (a.model === void 0) return {};
|
|
21917
|
+
if (typeof a.model !== "string" || !/^[\w.:/@-]{1,100}$/.test(a.model)) {
|
|
21918
|
+
throw new Error(`agents[${i}].model must be a model name (letters, digits and . : / @ _ -), at most 100 characters`);
|
|
21919
|
+
}
|
|
21920
|
+
return { model: a.model };
|
|
21921
|
+
}
|
|
21922
|
+
function isolateOf(a, i, runtime) {
|
|
21923
|
+
if (a.isolateUserConfig === void 0 || a.isolateUserConfig === false) return {};
|
|
21924
|
+
if (a.isolateUserConfig !== true) throw new Error(`agents[${i}].isolateUserConfig must be true or false`);
|
|
21925
|
+
if (runtime !== "claude") throw new Error(`agents[${i}]: isolateUserConfig only applies to the claude runtime`);
|
|
21926
|
+
return { isolateUserConfig: true };
|
|
21927
|
+
}
|
|
21715
21928
|
function parsePool(a, i, specDir) {
|
|
21716
21929
|
if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
|
|
21717
21930
|
if (typeof a.pool !== "string" || !POOL_NAME.test(a.pool)) throw new Error(`agents[${i}].pool must be letters, digits, '.', '_' or '-' (at most 60)`);
|
|
@@ -21727,10 +21940,10 @@ function parsePool(a, i, specDir) {
|
|
|
21727
21940
|
if (typeof min !== "number" || !Number.isInteger(min) || min < 0 || min > max) {
|
|
21728
21941
|
throw new Error(`agents[${i}].min must be an integer from 0 to max`);
|
|
21729
21942
|
}
|
|
21730
|
-
const base2 =
|
|
21731
|
-
const worktreeRoot =
|
|
21943
|
+
const base2 = resolve5(specDir, a.base);
|
|
21944
|
+
const worktreeRoot = resolve5(specDir, a.worktreeRoot);
|
|
21732
21945
|
if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
|
|
21733
|
-
return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, min, max };
|
|
21946
|
+
return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), min, max };
|
|
21734
21947
|
}
|
|
21735
21948
|
function parseSpec(text, specDir) {
|
|
21736
21949
|
let raw;
|
|
@@ -21756,7 +21969,8 @@ function parseSpec(text, specDir) {
|
|
|
21756
21969
|
if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
|
|
21757
21970
|
if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
|
|
21758
21971
|
const runtime = runtimeOf(a, i);
|
|
21759
|
-
|
|
21972
|
+
const specialization = specializationOf(a, i);
|
|
21973
|
+
agents.push({ name: a.name, runtime, worktree: resolve5(specDir, a.worktree), specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime) });
|
|
21760
21974
|
});
|
|
21761
21975
|
for (const p of pools) {
|
|
21762
21976
|
for (const a of agents) {
|
|
@@ -21783,13 +21997,15 @@ function parseSpec(text, specDir) {
|
|
|
21783
21997
|
}
|
|
21784
21998
|
|
|
21785
21999
|
// src/node/store.ts
|
|
21786
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as
|
|
21787
|
-
import { dirname as dirname8, join as
|
|
22000
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync12, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
|
|
22001
|
+
import { dirname as dirname8, join as join17 } from "node:path";
|
|
21788
22002
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
21789
|
-
var nodeDir = () =>
|
|
21790
|
-
var credsPath = (name) =>
|
|
21791
|
-
var statePath2 = () =>
|
|
21792
|
-
var logPath = (name) =>
|
|
22003
|
+
var nodeDir = () => join17(configDir(), "node");
|
|
22004
|
+
var credsPath = (name) => join17(nodeDir(), "agents", `${name}.json`);
|
|
22005
|
+
var statePath2 = () => join17(nodeDir(), "state.json");
|
|
22006
|
+
var logPath = (name) => join17(nodeDir(), "logs", `${name}.log`);
|
|
22007
|
+
var activityPath = (name) => join17(nodeDir(), "activity", name);
|
|
22008
|
+
var modelPath = (name) => join17(nodeDir(), "model", name);
|
|
21793
22009
|
function writePrivate(path3, value) {
|
|
21794
22010
|
mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
|
|
21795
22011
|
const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
|
|
@@ -21798,7 +22014,7 @@ function writePrivate(path3, value) {
|
|
|
21798
22014
|
}
|
|
21799
22015
|
function readJson(path3) {
|
|
21800
22016
|
if (!existsSync8(path3)) return null;
|
|
21801
|
-
return JSON.parse(
|
|
22017
|
+
return JSON.parse(readFileSync12(path3, "utf-8"));
|
|
21802
22018
|
}
|
|
21803
22019
|
var readCreds = (name) => readJson(credsPath(name));
|
|
21804
22020
|
var writeCreds = (name, creds) => writePrivate(credsPath(name), creds);
|
|
@@ -21807,14 +22023,187 @@ var removeCreds = (name) => {
|
|
|
21807
22023
|
if (existsSync8(path3)) rmSync4(path3);
|
|
21808
22024
|
};
|
|
21809
22025
|
var listStoredAgentNames = () => {
|
|
21810
|
-
const dir =
|
|
22026
|
+
const dir = join17(nodeDir(), "agents");
|
|
21811
22027
|
if (!existsSync8(dir)) return [];
|
|
21812
22028
|
return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
21813
22029
|
};
|
|
21814
22030
|
var readState = () => readJson(statePath2());
|
|
21815
22031
|
var writeState = (state) => writePrivate(statePath2(), state);
|
|
21816
22032
|
|
|
22033
|
+
// src/node/pool.ts
|
|
22034
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
22035
|
+
import { existsSync as existsSync9, lstatSync as lstatSync3, mkdirSync as mkdirSync8, realpathSync as realpathSync4 } from "node:fs";
|
|
22036
|
+
import { basename as basename2, dirname as dirname9, join as join18, resolve as resolve6 } from "node:path";
|
|
22037
|
+
var IDLE_STOP_MS = 10 * 6e4;
|
|
22038
|
+
var RECONCILE_MS = 15e3;
|
|
22039
|
+
var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
|
|
22040
|
+
var REMARK_AFTER_MS = 24 * 60 * 6e4;
|
|
22041
|
+
function positiveMs(raw, fallback2) {
|
|
22042
|
+
const n = Number(raw);
|
|
22043
|
+
return raw && Number.isInteger(n) && n > 0 ? n : fallback2;
|
|
22044
|
+
}
|
|
22045
|
+
function poolTimings(env2 = process.env) {
|
|
22046
|
+
return {
|
|
22047
|
+
reconcileMs: positiveMs(env2.PITBOSS_POOL_RECONCILE_MS, RECONCILE_MS),
|
|
22048
|
+
idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
|
|
22049
|
+
};
|
|
22050
|
+
}
|
|
22051
|
+
function effectiveBounds(spec, desired) {
|
|
22052
|
+
if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
|
|
22053
|
+
const max = Math.min(desired.max, spec.max);
|
|
22054
|
+
return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
|
|
22055
|
+
}
|
|
22056
|
+
function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
22057
|
+
const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
|
|
22058
|
+
const running = members.filter((m) => m.running);
|
|
22059
|
+
const stopped = members.filter((m) => !m.running);
|
|
22060
|
+
let count = running.length;
|
|
22061
|
+
const start = [];
|
|
22062
|
+
const take = (m) => {
|
|
22063
|
+
start.push(m.name);
|
|
22064
|
+
count++;
|
|
22065
|
+
};
|
|
22066
|
+
for (const m of stopped) {
|
|
22067
|
+
if (count >= max) break;
|
|
22068
|
+
if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
|
|
22069
|
+
}
|
|
22070
|
+
const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
|
|
22071
|
+
let demand = pending - running.filter(isIdle).length;
|
|
22072
|
+
for (const m of spare()) {
|
|
22073
|
+
if (demand <= 0 || count >= max) break;
|
|
22074
|
+
take(m);
|
|
22075
|
+
demand--;
|
|
22076
|
+
}
|
|
22077
|
+
for (const m of spare()) {
|
|
22078
|
+
if (count >= min || count >= max) break;
|
|
22079
|
+
take(m);
|
|
22080
|
+
}
|
|
22081
|
+
if (start.length > 0) return { start, stop: [] };
|
|
22082
|
+
const stop = [];
|
|
22083
|
+
let surplus = running.filter(isIdle).length - pending;
|
|
22084
|
+
const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
|
|
22085
|
+
for (const m of eligible) {
|
|
22086
|
+
if (surplus <= 0 || count <= min) break;
|
|
22087
|
+
stop.push(m.name);
|
|
22088
|
+
count--;
|
|
22089
|
+
surplus--;
|
|
22090
|
+
}
|
|
22091
|
+
return { start, stop };
|
|
22092
|
+
}
|
|
22093
|
+
function summarizeTasks(ready, held, members) {
|
|
22094
|
+
const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
|
|
22095
|
+
const counts = Object.fromEntries(
|
|
22096
|
+
members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
|
|
22097
|
+
);
|
|
22098
|
+
const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
|
|
22099
|
+
let readyAssigned = 0;
|
|
22100
|
+
for (const t of ready) {
|
|
22101
|
+
const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22102
|
+
if (!name) continue;
|
|
22103
|
+
counts[name].readyAssigned++;
|
|
22104
|
+
readyAssigned++;
|
|
22105
|
+
}
|
|
22106
|
+
for (const t of held) {
|
|
22107
|
+
const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22108
|
+
if (!name) continue;
|
|
22109
|
+
if (t.status === "assigned") counts[name].assigned++;
|
|
22110
|
+
if (t.status === "in_progress") counts[name].inProgress++;
|
|
22111
|
+
}
|
|
22112
|
+
return { pending, ready: pending + readyAssigned, members: counts };
|
|
22113
|
+
}
|
|
22114
|
+
function poolRootProblem(root, protectedPaths) {
|
|
22115
|
+
for (const p of protectedPaths) {
|
|
22116
|
+
if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
|
|
22117
|
+
}
|
|
22118
|
+
return null;
|
|
22119
|
+
}
|
|
22120
|
+
function memberPathProblem(s) {
|
|
22121
|
+
if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
|
|
22122
|
+
if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
|
|
22123
|
+
if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
|
|
22124
|
+
return null;
|
|
22125
|
+
}
|
|
22126
|
+
function realpathAllowMissing(path3) {
|
|
22127
|
+
const abs = resolve6(path3);
|
|
22128
|
+
if (existsSync9(abs)) return realpathSync4.native(abs);
|
|
22129
|
+
return join18(realpathAllowMissing(dirname9(abs)), basename2(abs));
|
|
22130
|
+
}
|
|
22131
|
+
function run2(cwd, args) {
|
|
22132
|
+
try {
|
|
22133
|
+
return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
22134
|
+
} catch (err) {
|
|
22135
|
+
const stderr = err.stderr?.toString().trim();
|
|
22136
|
+
throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
|
|
22137
|
+
}
|
|
22138
|
+
}
|
|
22139
|
+
var commonDir = (cwd) => {
|
|
22140
|
+
const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
|
|
22141
|
+
return dir ? realpathSync4.native(dir) : null;
|
|
22142
|
+
};
|
|
22143
|
+
function defaultBranch(cwd) {
|
|
22144
|
+
const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
22145
|
+
let ref = head();
|
|
22146
|
+
if (!ref) {
|
|
22147
|
+
git(["remote", "set-head", "origin", "--auto"], cwd);
|
|
22148
|
+
ref = head();
|
|
22149
|
+
}
|
|
22150
|
+
if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
|
|
22151
|
+
return ref.slice("origin/".length);
|
|
22152
|
+
}
|
|
22153
|
+
function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
22154
|
+
if (!existsSync9(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
|
|
22155
|
+
const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
|
|
22156
|
+
const baseCommon = commonDir(pool.base);
|
|
22157
|
+
if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
|
|
22158
|
+
const root = realpathAllowMissing(pool.worktreeRoot);
|
|
22159
|
+
const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
|
|
22160
|
+
if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
|
|
22161
|
+
const members = poolMembers({ ...pool, worktreeRoot: root });
|
|
22162
|
+
for (const m of members) {
|
|
22163
|
+
let symlink = false;
|
|
22164
|
+
try {
|
|
22165
|
+
symlink = lstatSync3(m.worktree).isSymbolicLink();
|
|
22166
|
+
} catch {
|
|
22167
|
+
symlink = false;
|
|
22168
|
+
}
|
|
22169
|
+
const exists = !symlink && existsSync9(m.worktree);
|
|
22170
|
+
const problem = memberPathProblem({
|
|
22171
|
+
path: m.worktree,
|
|
22172
|
+
symlink,
|
|
22173
|
+
exists,
|
|
22174
|
+
baseCommonDir: baseCommon,
|
|
22175
|
+
commonDir: exists ? commonDir(m.worktree) : null,
|
|
22176
|
+
tracked: !exists && !symlink && isTracked(m.worktree)
|
|
22177
|
+
});
|
|
22178
|
+
if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
|
|
22179
|
+
}
|
|
22180
|
+
const missing = members.filter((m) => !existsSync9(m.worktree));
|
|
22181
|
+
if (missing.length > 0) {
|
|
22182
|
+
mkdirSync8(root, { recursive: true });
|
|
22183
|
+
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22184
|
+
const branch = defaultBranch(pool.base);
|
|
22185
|
+
run2(pool.base, ["worktree", "prune"]);
|
|
22186
|
+
for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
|
|
22187
|
+
}
|
|
22188
|
+
return members.map((m) => realpathSync4.native(m.worktree));
|
|
22189
|
+
}
|
|
22190
|
+
function resetWorktree(worktree) {
|
|
22191
|
+
run2(worktree, ["fetch", "-q", "--prune", "origin"]);
|
|
22192
|
+
const target = `origin/${defaultBranch(worktree)}`;
|
|
22193
|
+
run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
|
|
22194
|
+
run2(worktree, ["reset", "-q", "--hard", target]);
|
|
22195
|
+
run2(worktree, ["clean", "-fdq"]);
|
|
22196
|
+
}
|
|
22197
|
+
|
|
21817
22198
|
// src/node/identity.ts
|
|
22199
|
+
function requestedAgents(agents) {
|
|
22200
|
+
return agents.map((a) => ({
|
|
22201
|
+
name: a.name,
|
|
22202
|
+
workerType: getAdapter(a.runtime).workerType,
|
|
22203
|
+
specialization: a.specialization,
|
|
22204
|
+
...a.tier ? { tier: a.tier } : {}
|
|
22205
|
+
}));
|
|
22206
|
+
}
|
|
21818
22207
|
function collectInvites(agentNames, flags, env2) {
|
|
21819
22208
|
const byLower = new Map(agentNames.map((n) => [n.toLowerCase(), n]));
|
|
21820
22209
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -21851,6 +22240,7 @@ function matchInvites(agents, invites) {
|
|
|
21851
22240
|
}
|
|
21852
22241
|
return { matched, ungranted, extra: left, refused };
|
|
21853
22242
|
}
|
|
22243
|
+
var MAX_REQUESTED = 32;
|
|
21854
22244
|
function groupByRepo(agents, originOf2) {
|
|
21855
22245
|
const groups = /* @__PURE__ */ new Map();
|
|
21856
22246
|
for (const agent of agents) {
|
|
@@ -21858,7 +22248,10 @@ function groupByRepo(agents, originOf2) {
|
|
|
21858
22248
|
if (!remote) throw new Error(`${agent.name}: ${agent.worktree} has no git origin; each worktree must be a clone of the agent's repo`);
|
|
21859
22249
|
groups.set(remote, [...groups.get(remote) ?? [], agent]);
|
|
21860
22250
|
}
|
|
21861
|
-
return [...groups].
|
|
22251
|
+
return [...groups].flatMap(([remote, members]) => Array.from({ length: Math.ceil(members.length / MAX_REQUESTED) }, (_, i) => ({
|
|
22252
|
+
remote,
|
|
22253
|
+
agents: members.slice(i * MAX_REQUESTED, (i + 1) * MAX_REQUESTED)
|
|
22254
|
+
})));
|
|
21862
22255
|
}
|
|
21863
22256
|
var originOf = (worktree) => git(["remote", "get-url", "origin"], worktree);
|
|
21864
22257
|
async function abandonMinted(spec, agent, minted, reason) {
|
|
@@ -21964,10 +22357,12 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
21964
22357
|
console.log(source_default.bold(`
|
|
21965
22358
|
Approve ${repoName}`));
|
|
21966
22359
|
console.log(` On the approval screen add these agents as workers:`);
|
|
21967
|
-
for (const a of group.agents)
|
|
22360
|
+
for (const a of group.agents) {
|
|
22361
|
+
console.log(` ${source_default.cyan(a.name)} ${source_default.dim(`${a.runtime}, ${a.specialization}${a.tier ? `, tier ${a.tier}` : ""}`)}`);
|
|
22362
|
+
}
|
|
21968
22363
|
let grant;
|
|
21969
22364
|
try {
|
|
21970
|
-
grant = await requestDeviceGrant(spec.api, { repoName, remote: group.remote }, "pitboss node up");
|
|
22365
|
+
grant = await requestDeviceGrant(spec.api, { repoName, remote: group.remote, agents: requestedAgents(group.agents) }, "pitboss node up");
|
|
21971
22366
|
} catch (err) {
|
|
21972
22367
|
console.log(source_default.red(` \u2715 ${repoName}: ${err instanceof Error ? err.message : String(err)} ${group.agents.map((a) => a.name).join(", ")} will not start.`));
|
|
21973
22368
|
continue;
|
|
@@ -22007,6 +22402,106 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
22007
22402
|
return have;
|
|
22008
22403
|
}
|
|
22009
22404
|
|
|
22405
|
+
// src/commands/onboard.ts
|
|
22406
|
+
function isGitHubHost(host) {
|
|
22407
|
+
if (!host) return false;
|
|
22408
|
+
if (host === "github.com") return true;
|
|
22409
|
+
try {
|
|
22410
|
+
const resolved = execFileSync7("ssh", ["-G", host], { encoding: "utf-8" }).match(/^hostname (\S+)$/m)?.[1];
|
|
22411
|
+
return resolved?.toLowerCase() === "github.com";
|
|
22412
|
+
} catch {
|
|
22413
|
+
return false;
|
|
22414
|
+
}
|
|
22415
|
+
}
|
|
22416
|
+
var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
22417
|
+
async function onboardCommand(url, opts) {
|
|
22418
|
+
try {
|
|
22419
|
+
const plan = planOnboard(url, {
|
|
22420
|
+
home: homedir6(),
|
|
22421
|
+
parent: opts.dir,
|
|
22422
|
+
writer: opts.writer,
|
|
22423
|
+
reviewer: opts.reviewer,
|
|
22424
|
+
poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax)
|
|
22425
|
+
});
|
|
22426
|
+
const env2 = { ...process.env };
|
|
22427
|
+
if (opts.account) {
|
|
22428
|
+
env2.GH_TOKEN = execFileSync7("gh", ["auth", "token", "--hostname", "github.com", "--user", opts.account], { encoding: "utf-8" }).trim();
|
|
22429
|
+
}
|
|
22430
|
+
if (existsSync10(plan.base)) {
|
|
22431
|
+
const origin = execFileSync7("git", ["-C", plan.base, "remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
|
|
22432
|
+
if (normalizeRepoUrl(origin) !== normalizeRepoUrl(plan.cloneUrl) || !isGitHubHost(originHost(origin))) {
|
|
22433
|
+
throw new Error(`${plan.base} exists but its origin is ${origin}, not ${plan.cloneUrl} on github.com`);
|
|
22434
|
+
}
|
|
22435
|
+
console.log(source_default.dim(` using the clone at ${plan.base}`));
|
|
22436
|
+
} else {
|
|
22437
|
+
execFileSync7("gh", ["repo", "clone", `${plan.owner}/${plan.name}`, plan.base], { stdio: "inherit", env: env2 });
|
|
22438
|
+
}
|
|
22439
|
+
const commonDir2 = (dir) => realpathSync5.native(
|
|
22440
|
+
execFileSync7("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf-8" }).trim()
|
|
22441
|
+
);
|
|
22442
|
+
for (const path3 of plan.worktrees) {
|
|
22443
|
+
if (!existsSync10(path3)) {
|
|
22444
|
+
execFileSync7("git", ["-C", plan.base, "worktree", "add", "--detach", path3], { stdio: "inherit" });
|
|
22445
|
+
} else if (commonDir2(path3) !== commonDir2(plan.base)) {
|
|
22446
|
+
throw new Error(`${path3} exists but is not a worktree of ${plan.base}; move it aside or pass --dir`);
|
|
22447
|
+
}
|
|
22448
|
+
}
|
|
22449
|
+
const specText = JSON.stringify(plan.spec, null, 2) + "\n";
|
|
22450
|
+
const parsed = parseSpec(specText, plan.nodeDir);
|
|
22451
|
+
if (existsSync10(plan.specPath) && readFileSync13(plan.specPath, "utf-8") !== specText && !opts.force) {
|
|
22452
|
+
throw new Error(`${plan.specPath} already exists with different contents; pass --force to replace it`);
|
|
22453
|
+
}
|
|
22454
|
+
mkdirSync9(plan.nodeDir, { recursive: true });
|
|
22455
|
+
writeFileSync9(plan.specPath, specText);
|
|
22456
|
+
process.env.PITBOSS_CONFIG_DIR = plan.configDir;
|
|
22457
|
+
mkdirSync9(plan.configDir, { recursive: true, mode: 448 });
|
|
22458
|
+
const specReal = realpathSync5.native(plan.specPath);
|
|
22459
|
+
const foreign = allAgents(parsed).map((a) => readCreds(a.name)).find((c) => c?.spec && c.spec !== specReal);
|
|
22460
|
+
if (foreign) throw new Error(`${plan.configDir} holds credentials for another node spec (${foreign.spec}); onboard will not overwrite them`);
|
|
22461
|
+
const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync5.native(a.worktree) }));
|
|
22462
|
+
const ownToken = process.env.GH_TOKEN;
|
|
22463
|
+
if (env2.GH_TOKEN) process.env.GH_TOKEN = env2.GH_TOKEN;
|
|
22464
|
+
let pooled;
|
|
22465
|
+
try {
|
|
22466
|
+
pooled = parsed.pools.flatMap((pool) => {
|
|
22467
|
+
const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
|
|
22468
|
+
return poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] }));
|
|
22469
|
+
});
|
|
22470
|
+
} finally {
|
|
22471
|
+
if (ownToken === void 0) delete process.env.GH_TOKEN;
|
|
22472
|
+
else process.env.GH_TOKEN = ownToken;
|
|
22473
|
+
}
|
|
22474
|
+
const everyone = [...plain, ...pooled];
|
|
22475
|
+
const seen = /* @__PURE__ */ new Map();
|
|
22476
|
+
for (const a of everyone) {
|
|
22477
|
+
const other = seen.get(a.worktree);
|
|
22478
|
+
if (other) throw new Error(`agents "${other}" and "${a.name}" resolve to the same worktree ${a.worktree}; each needs its own`);
|
|
22479
|
+
seen.set(a.worktree, a.name);
|
|
22480
|
+
}
|
|
22481
|
+
const creds = await ensureIdentities({ ...parsed, agents: everyone }, /* @__PURE__ */ new Map(), specReal);
|
|
22482
|
+
const missing = everyone.filter((a) => !creds.has(a.name)).map((a) => a.name);
|
|
22483
|
+
if (missing.length === everyone.length) throw new Error("no agent was granted; approve the device code again by re-running onboard");
|
|
22484
|
+
if (missing.length > 0) process.exitCode = 1;
|
|
22485
|
+
console.log(source_default.bold(`
|
|
22486
|
+
${plan.name} is onboarded${missing.length ? source_default.yellow(` except ${missing.join(", ")}`) : ""}.`));
|
|
22487
|
+
console.log(` spec ${plan.specPath}`);
|
|
22488
|
+
console.log(` config ${plan.configDir}`);
|
|
22489
|
+
console.log(`
|
|
22490
|
+
Start it when you want it running (long-running, foreground):`);
|
|
22491
|
+
console.log(source_default.cyan(` cd ${shellQuote(plan.nodeDir)} && PITBOSS_CONFIG_DIR=${shellQuote(plan.configDir)} pitboss node up spec.json`));
|
|
22492
|
+
if (opts.account) console.log(source_default.dim(` with GH_TOKEN from \`gh auth token --user ${opts.account}\` in that shell`));
|
|
22493
|
+
} catch (err) {
|
|
22494
|
+
console.error(source_default.red(` \u2715 ${err instanceof Error ? err.message : String(err)}`));
|
|
22495
|
+
process.exitCode = 1;
|
|
22496
|
+
}
|
|
22497
|
+
}
|
|
22498
|
+
|
|
22499
|
+
// src/commands/node.ts
|
|
22500
|
+
import { execFileSync as execFileSync8, spawn as spawnChild } from "node:child_process";
|
|
22501
|
+
import { closeSync, existsSync as existsSync12, mkdirSync as mkdirSync11, openSync, readFileSync as readFileSync15, realpathSync as realpathSync6, rmSync as rmSync6 } from "node:fs";
|
|
22502
|
+
import { dirname as dirname12, resolve as resolve8 } from "node:path";
|
|
22503
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
22504
|
+
|
|
22010
22505
|
// src/node/supervisor.ts
|
|
22011
22506
|
function nextDelay(consecutiveFailures, { baseMs, maxMs }) {
|
|
22012
22507
|
return Math.min(baseMs * 2 ** Math.max(0, consecutiveFailures - 1), maxMs);
|
|
@@ -22131,44 +22626,67 @@ var Supervisor = class {
|
|
|
22131
22626
|
}
|
|
22132
22627
|
};
|
|
22133
22628
|
|
|
22629
|
+
// src/node/model.ts
|
|
22630
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
22631
|
+
import { dirname as dirname10 } from "node:path";
|
|
22632
|
+
function writeModelFiles(agents) {
|
|
22633
|
+
for (const a of agents) {
|
|
22634
|
+
const path3 = modelPath(a.name);
|
|
22635
|
+
if (!a.model) {
|
|
22636
|
+
rmSync5(path3, { force: true });
|
|
22637
|
+
continue;
|
|
22638
|
+
}
|
|
22639
|
+
let current = null;
|
|
22640
|
+
try {
|
|
22641
|
+
current = readFileSync14(path3, "utf-8");
|
|
22642
|
+
} catch {
|
|
22643
|
+
}
|
|
22644
|
+
if (current === a.model) continue;
|
|
22645
|
+
mkdirSync10(dirname10(path3), { recursive: true });
|
|
22646
|
+
const tmp = `${path3}.${process.pid}.tmp`;
|
|
22647
|
+
writeFileSync10(tmp, a.model);
|
|
22648
|
+
renameSync5(tmp, path3);
|
|
22649
|
+
}
|
|
22650
|
+
}
|
|
22651
|
+
|
|
22134
22652
|
// src/node/entries.ts
|
|
22135
|
-
import { existsSync as
|
|
22136
|
-
import { dirname as
|
|
22653
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
22654
|
+
import { dirname as dirname11, join as join19, resolve as resolve7 } from "node:path";
|
|
22137
22655
|
var MISSING_WORKER = "event-worker not found next to this CLI. Run node from a pitboss checkout, or set PITBOSS_EVENT_WORKER to packages/event-worker.";
|
|
22138
22656
|
function checkoutWorker(dir) {
|
|
22139
22657
|
return {
|
|
22140
|
-
command:
|
|
22141
|
-
args: [
|
|
22658
|
+
command: join19(dir, "node_modules", ".bin", "tsx"),
|
|
22659
|
+
args: [join19(dir, "src", "index.ts")]
|
|
22142
22660
|
};
|
|
22143
22661
|
}
|
|
22144
22662
|
function checkoutMcp(dir, execPath) {
|
|
22145
22663
|
return {
|
|
22146
22664
|
command: execPath,
|
|
22147
22665
|
args: [
|
|
22148
|
-
|
|
22149
|
-
|
|
22666
|
+
join19(dir, "node_modules", "tsx", "dist", "cli.mjs"),
|
|
22667
|
+
join19(dir, "src", "index.ts")
|
|
22150
22668
|
]
|
|
22151
22669
|
};
|
|
22152
22670
|
}
|
|
22153
22671
|
function walkFor(here, packageName, exists) {
|
|
22154
|
-
for (let dir =
|
|
22155
|
-
const candidate =
|
|
22156
|
-
if (exists(
|
|
22672
|
+
for (let dir = dirname11(here); dirname11(dir) !== dir; dir = dirname11(dir)) {
|
|
22673
|
+
const candidate = join19(dir, packageName);
|
|
22674
|
+
if (exists(join19(candidate, "src", "index.ts"))) return candidate;
|
|
22157
22675
|
}
|
|
22158
22676
|
return null;
|
|
22159
22677
|
}
|
|
22160
22678
|
function resolveEventWorker(opts) {
|
|
22161
22679
|
const env2 = opts.env ?? process.env;
|
|
22162
22680
|
const execPath = opts.execPath ?? process.execPath;
|
|
22163
|
-
const exists = opts.exists ??
|
|
22681
|
+
const exists = opts.exists ?? existsSync11;
|
|
22164
22682
|
const override = env2.PITBOSS_EVENT_WORKER;
|
|
22165
22683
|
if (override) {
|
|
22166
|
-
const resolved =
|
|
22684
|
+
const resolved = resolve7(override);
|
|
22167
22685
|
if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
|
|
22168
|
-
if (exists(
|
|
22686
|
+
if (exists(join19(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
|
|
22169
22687
|
throw new Error(MISSING_WORKER);
|
|
22170
22688
|
}
|
|
22171
|
-
const bundled =
|
|
22689
|
+
const bundled = join19(dirname11(opts.here), "event-worker.js");
|
|
22172
22690
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
22173
22691
|
const dir = walkFor(opts.here, "event-worker", exists);
|
|
22174
22692
|
if (dir) return checkoutWorker(dir);
|
|
@@ -22176,8 +22694,8 @@ function resolveEventWorker(opts) {
|
|
|
22176
22694
|
}
|
|
22177
22695
|
function resolveMcpServer(opts) {
|
|
22178
22696
|
const execPath = opts.execPath ?? process.execPath;
|
|
22179
|
-
const exists = opts.exists ??
|
|
22180
|
-
const bundled =
|
|
22697
|
+
const exists = opts.exists ?? existsSync11;
|
|
22698
|
+
const bundled = join19(dirname11(opts.here), "mcp.js");
|
|
22181
22699
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
22182
22700
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
22183
22701
|
if (dir) return checkoutMcp(dir, execPath);
|
|
@@ -22285,176 +22803,41 @@ function loadPruneTargets(names) {
|
|
|
22285
22803
|
});
|
|
22286
22804
|
}
|
|
22287
22805
|
|
|
22288
|
-
// src/node/
|
|
22289
|
-
|
|
22290
|
-
|
|
22291
|
-
|
|
22292
|
-
|
|
22293
|
-
|
|
22294
|
-
|
|
22295
|
-
|
|
22296
|
-
|
|
22297
|
-
|
|
22298
|
-
|
|
22299
|
-
|
|
22300
|
-
|
|
22301
|
-
|
|
22302
|
-
|
|
22303
|
-
|
|
22304
|
-
};
|
|
22305
|
-
}
|
|
22306
|
-
function
|
|
22307
|
-
|
|
22308
|
-
const
|
|
22309
|
-
|
|
22310
|
-
|
|
22311
|
-
|
|
22312
|
-
|
|
22313
|
-
|
|
22314
|
-
|
|
22315
|
-
|
|
22316
|
-
const start = [];
|
|
22317
|
-
const take = (m) => {
|
|
22318
|
-
start.push(m.name);
|
|
22319
|
-
count++;
|
|
22320
|
-
};
|
|
22321
|
-
for (const m of stopped) {
|
|
22322
|
-
if (count >= max) break;
|
|
22323
|
-
if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
|
|
22324
|
-
}
|
|
22325
|
-
const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
|
|
22326
|
-
let demand = pending - running.filter(isIdle).length;
|
|
22327
|
-
for (const m of spare()) {
|
|
22328
|
-
if (demand <= 0 || count >= max) break;
|
|
22329
|
-
take(m);
|
|
22330
|
-
demand--;
|
|
22331
|
-
}
|
|
22332
|
-
for (const m of spare()) {
|
|
22333
|
-
if (count >= min || count >= max) break;
|
|
22334
|
-
take(m);
|
|
22335
|
-
}
|
|
22336
|
-
if (start.length > 0) return { start, stop: [] };
|
|
22337
|
-
const stop = [];
|
|
22338
|
-
let surplus = running.filter(isIdle).length - pending;
|
|
22339
|
-
const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
|
|
22340
|
-
for (const m of eligible) {
|
|
22341
|
-
if (surplus <= 0 || count <= min) break;
|
|
22342
|
-
stop.push(m.name);
|
|
22343
|
-
count--;
|
|
22344
|
-
surplus--;
|
|
22345
|
-
}
|
|
22346
|
-
return { start, stop };
|
|
22347
|
-
}
|
|
22348
|
-
function summarizeTasks(ready, held, members) {
|
|
22349
|
-
const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
|
|
22350
|
-
const counts = Object.fromEntries(
|
|
22351
|
-
members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
|
|
22352
|
-
);
|
|
22353
|
-
const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
|
|
22354
|
-
let readyAssigned = 0;
|
|
22355
|
-
for (const t of ready) {
|
|
22356
|
-
const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22357
|
-
if (!name) continue;
|
|
22358
|
-
counts[name].readyAssigned++;
|
|
22359
|
-
readyAssigned++;
|
|
22360
|
-
}
|
|
22361
|
-
for (const t of held) {
|
|
22362
|
-
const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22363
|
-
if (!name) continue;
|
|
22364
|
-
if (t.status === "assigned") counts[name].assigned++;
|
|
22365
|
-
if (t.status === "in_progress") counts[name].inProgress++;
|
|
22366
|
-
}
|
|
22367
|
-
return { pending, ready: pending + readyAssigned, members: counts };
|
|
22368
|
-
}
|
|
22369
|
-
function poolRootProblem(root, protectedPaths) {
|
|
22370
|
-
for (const p of protectedPaths) {
|
|
22371
|
-
if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
|
|
22372
|
-
}
|
|
22373
|
-
return null;
|
|
22374
|
-
}
|
|
22375
|
-
function memberPathProblem(s) {
|
|
22376
|
-
if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
|
|
22377
|
-
if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
|
|
22378
|
-
if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
|
|
22379
|
-
return null;
|
|
22380
|
-
}
|
|
22381
|
-
function realpathAllowMissing(path3) {
|
|
22382
|
-
const abs = resolve6(path3);
|
|
22383
|
-
if (existsSync10(abs)) return realpathSync4.native(abs);
|
|
22384
|
-
return join18(realpathAllowMissing(dirname10(abs)), basename2(abs));
|
|
22385
|
-
}
|
|
22386
|
-
function run2(cwd, args) {
|
|
22387
|
-
try {
|
|
22388
|
-
return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
22389
|
-
} catch (err) {
|
|
22390
|
-
const stderr = err.stderr?.toString().trim();
|
|
22391
|
-
throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
|
|
22392
|
-
}
|
|
22393
|
-
}
|
|
22394
|
-
var commonDir = (cwd) => {
|
|
22395
|
-
const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
|
|
22396
|
-
return dir ? realpathSync4.native(dir) : null;
|
|
22397
|
-
};
|
|
22398
|
-
function defaultBranch(cwd) {
|
|
22399
|
-
const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
22400
|
-
let ref = head();
|
|
22401
|
-
if (!ref) {
|
|
22402
|
-
git(["remote", "set-head", "origin", "--auto"], cwd);
|
|
22403
|
-
ref = head();
|
|
22404
|
-
}
|
|
22405
|
-
if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
|
|
22406
|
-
return ref.slice("origin/".length);
|
|
22407
|
-
}
|
|
22408
|
-
function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
22409
|
-
if (!existsSync10(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
|
|
22410
|
-
const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
|
|
22411
|
-
const baseCommon = commonDir(pool.base);
|
|
22412
|
-
if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
|
|
22413
|
-
const root = realpathAllowMissing(pool.worktreeRoot);
|
|
22414
|
-
const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
|
|
22415
|
-
if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
|
|
22416
|
-
const members = poolMembers({ ...pool, worktreeRoot: root });
|
|
22417
|
-
for (const m of members) {
|
|
22418
|
-
let symlink = false;
|
|
22419
|
-
try {
|
|
22420
|
-
symlink = lstatSync3(m.worktree).isSymbolicLink();
|
|
22421
|
-
} catch {
|
|
22422
|
-
symlink = false;
|
|
22423
|
-
}
|
|
22424
|
-
const exists = !symlink && existsSync10(m.worktree);
|
|
22425
|
-
const problem = memberPathProblem({
|
|
22426
|
-
path: m.worktree,
|
|
22427
|
-
symlink,
|
|
22428
|
-
exists,
|
|
22429
|
-
baseCommonDir: baseCommon,
|
|
22430
|
-
commonDir: exists ? commonDir(m.worktree) : null,
|
|
22431
|
-
tracked: !exists && !symlink && isTracked(m.worktree)
|
|
22432
|
-
});
|
|
22433
|
-
if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
|
|
22434
|
-
}
|
|
22435
|
-
const missing = members.filter((m) => !existsSync10(m.worktree));
|
|
22436
|
-
if (missing.length > 0) {
|
|
22437
|
-
mkdirSync8(root, { recursive: true });
|
|
22438
|
-
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22439
|
-
const branch = defaultBranch(pool.base);
|
|
22440
|
-
run2(pool.base, ["worktree", "prune"]);
|
|
22441
|
-
for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
|
|
22442
|
-
}
|
|
22443
|
-
return members.map((m) => realpathSync4.native(m.worktree));
|
|
22444
|
-
}
|
|
22445
|
-
function resetWorktree(worktree) {
|
|
22446
|
-
run2(worktree, ["fetch", "-q", "--prune", "origin"]);
|
|
22447
|
-
const target = `origin/${defaultBranch(worktree)}`;
|
|
22448
|
-
run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
|
|
22449
|
-
run2(worktree, ["reset", "-q", "--hard", target]);
|
|
22450
|
-
run2(worktree, ["clean", "-fdq"]);
|
|
22806
|
+
// src/node/activity.ts
|
|
22807
|
+
var ACTIVITY_REPORT_MS = 15e3;
|
|
22808
|
+
var REVIEW_HEARTBEAT_MS = 6e4;
|
|
22809
|
+
function deriveActivity({ running, sessionActive, reviewing, waiting }) {
|
|
22810
|
+
if (!running) return "stopped";
|
|
22811
|
+
if (sessionActive) return "working";
|
|
22812
|
+
if (reviewing) return "reviewing";
|
|
22813
|
+
return waiting ? "waiting" : "idle";
|
|
22814
|
+
}
|
|
22815
|
+
function heldAgentIds(tasks) {
|
|
22816
|
+
const reviewing = /* @__PURE__ */ new Set();
|
|
22817
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
22818
|
+
for (const t of tasks) {
|
|
22819
|
+
if (t.assignedTo && (t.status === "blocked" || t.status === "pending_verification")) waiting.add(t.assignedTo);
|
|
22820
|
+
if (t.status === "pending_verification" && t.verifyReviewerId && t.metadata?.reviewStartedBy === t.verifyReviewerId) reviewing.add(t.verifyReviewerId);
|
|
22821
|
+
}
|
|
22822
|
+
return { reviewing, waiting };
|
|
22823
|
+
}
|
|
22824
|
+
function startedReviewTaskIds(tasks) {
|
|
22825
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
22826
|
+
for (const t of tasks) {
|
|
22827
|
+
if (t.status !== "pending_verification" || !t.verifyReviewerId || t.metadata?.reviewStartedBy !== t.verifyReviewerId) continue;
|
|
22828
|
+
byAgent.set(t.verifyReviewerId, [...byAgent.get(t.verifyReviewerId) ?? [], t.id]);
|
|
22829
|
+
}
|
|
22830
|
+
return byAgent;
|
|
22831
|
+
}
|
|
22832
|
+
function reviewerIsLive({ running, sessionActive }) {
|
|
22833
|
+
return running && sessionActive;
|
|
22451
22834
|
}
|
|
22452
22835
|
|
|
22453
22836
|
// src/commands/node.ts
|
|
22454
22837
|
var HERE = fileURLToPath3(import.meta.url);
|
|
22455
22838
|
function inspectProcess(pid) {
|
|
22456
22839
|
try {
|
|
22457
|
-
const out =
|
|
22840
|
+
const out = execFileSync8("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
|
|
22458
22841
|
const match = /^(\d+)\s+(.*)$/.exec(out);
|
|
22459
22842
|
return match ? { pgid: Number(match[1]), command: match[2] } : null;
|
|
22460
22843
|
} catch {
|
|
@@ -22462,7 +22845,7 @@ function inspectProcess(pid) {
|
|
|
22462
22845
|
}
|
|
22463
22846
|
}
|
|
22464
22847
|
function childSpec(agent, creds, worker, onDemand = false) {
|
|
22465
|
-
const { ANTHROPIC_API_KEY: _key, ANTHROPIC_AUTH_TOKEN: _tok, ...inherited } = process.env;
|
|
22848
|
+
const { ANTHROPIC_API_KEY: _key, ANTHROPIC_AUTH_TOKEN: _tok, PITBOSS_ISOLATE_USER_CONFIG: _iso, ...inherited } = process.env;
|
|
22466
22849
|
return {
|
|
22467
22850
|
name: agent.name,
|
|
22468
22851
|
command: worker.command,
|
|
@@ -22478,7 +22861,10 @@ function childSpec(agent, creds, worker, onDemand = false) {
|
|
|
22478
22861
|
REPO_ID: creds.repoId,
|
|
22479
22862
|
REPO_PATH: agent.worktree,
|
|
22480
22863
|
SPECIALIZATION: agent.specialization,
|
|
22481
|
-
WORKER_RUNTIME: agent.runtime
|
|
22864
|
+
WORKER_RUNTIME: agent.runtime,
|
|
22865
|
+
PITBOSS_ACTIVITY_FILE: activityPath(agent.name),
|
|
22866
|
+
PITBOSS_MODEL_FILE: resolve8(modelPath(agent.name)),
|
|
22867
|
+
...agent.isolateUserConfig ? { PITBOSS_ISOLATE_USER_CONFIG: "1" } : {}
|
|
22482
22868
|
}
|
|
22483
22869
|
};
|
|
22484
22870
|
}
|
|
@@ -22494,12 +22880,13 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
22494
22880
|
excludeIfUntracked(root, target);
|
|
22495
22881
|
},
|
|
22496
22882
|
run(bin4, args, cwd) {
|
|
22497
|
-
|
|
22883
|
+
execFileSync8(bin4, args, { cwd, stdio: "ignore" });
|
|
22498
22884
|
}
|
|
22499
22885
|
});
|
|
22500
22886
|
}
|
|
22501
22887
|
function spawnWorker(spec) {
|
|
22502
|
-
|
|
22888
|
+
mkdirSync11(dirname12(spec.logPath), { recursive: true, mode: 448 });
|
|
22889
|
+
rmSync6(activityPath(spec.name), { force: true });
|
|
22503
22890
|
const log = openSync(spec.logPath, "a", 384);
|
|
22504
22891
|
try {
|
|
22505
22892
|
const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
|
|
@@ -22546,6 +22933,63 @@ async function reportScale(creds, event) {
|
|
|
22546
22933
|
});
|
|
22547
22934
|
if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
|
|
22548
22935
|
}
|
|
22936
|
+
async function reportActivity(creds, state) {
|
|
22937
|
+
const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/activity`, {
|
|
22938
|
+
method: "PUT",
|
|
22939
|
+
headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
|
|
22940
|
+
body: JSON.stringify({ state })
|
|
22941
|
+
});
|
|
22942
|
+
if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/activity returned HTTP ${res.status}`);
|
|
22943
|
+
}
|
|
22944
|
+
function startActivityLoop(agents, sup) {
|
|
22945
|
+
let busy = false;
|
|
22946
|
+
const sent = /* @__PURE__ */ new Map();
|
|
22947
|
+
const beatAt = /* @__PURE__ */ new Map();
|
|
22948
|
+
const heartbeatReviews = async (creds, name, live, taskIds) => {
|
|
22949
|
+
if (!live || taskIds.length === 0 || Date.now() - (beatAt.get(name) ?? 0) < REVIEW_HEARTBEAT_MS) return;
|
|
22950
|
+
beatAt.set(name, Date.now());
|
|
22951
|
+
for (const id of taskIds) {
|
|
22952
|
+
const res = await fetch(`${creds.api}/tasks/${encodeURIComponent(id)}/review/start`, {
|
|
22953
|
+
method: "POST",
|
|
22954
|
+
headers: { Authorization: `Bearer ${creds.token}` }
|
|
22955
|
+
});
|
|
22956
|
+
if (!res.ok) throw new Error(`POST /tasks/${id}/review/start returned HTTP ${res.status}`);
|
|
22957
|
+
}
|
|
22958
|
+
};
|
|
22959
|
+
const tick = async () => {
|
|
22960
|
+
if (busy) return;
|
|
22961
|
+
busy = true;
|
|
22962
|
+
try {
|
|
22963
|
+
const reader = agents[0].creds;
|
|
22964
|
+
const held = await fetchTasks(reader, "status=blocked,pending_verification");
|
|
22965
|
+
const { reviewing, waiting } = heldAgentIds(held);
|
|
22966
|
+
const reviewIds = startedReviewTaskIds(held);
|
|
22967
|
+
for (const { name, creds } of agents) {
|
|
22968
|
+
const signals2 = {
|
|
22969
|
+
running: sup.isActive(name),
|
|
22970
|
+
sessionActive: existsSync12(activityPath(name))
|
|
22971
|
+
};
|
|
22972
|
+
const live = reviewerIsLive(signals2);
|
|
22973
|
+
const state = deriveActivity({
|
|
22974
|
+
...signals2,
|
|
22975
|
+
reviewing: reviewing.has(creds.agentId),
|
|
22976
|
+
waiting: waiting.has(creds.agentId)
|
|
22977
|
+
});
|
|
22978
|
+
await heartbeatReviews(creds, name, live, reviewIds.get(creds.agentId) ?? []);
|
|
22979
|
+
if (sent.get(name) === state) continue;
|
|
22980
|
+
await reportActivity(creds, state);
|
|
22981
|
+
sent.set(name, state);
|
|
22982
|
+
}
|
|
22983
|
+
} catch (err) {
|
|
22984
|
+
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} activity: ${err instanceof Error ? err.message : String(err)}`);
|
|
22985
|
+
} finally {
|
|
22986
|
+
busy = false;
|
|
22987
|
+
}
|
|
22988
|
+
};
|
|
22989
|
+
void tick();
|
|
22990
|
+
const timer = setInterval(() => void tick(), ACTIVITY_REPORT_MS);
|
|
22991
|
+
return () => clearInterval(timer);
|
|
22992
|
+
}
|
|
22549
22993
|
function startPoolLoop(run3, sup, record) {
|
|
22550
22994
|
const { pool, members } = run3;
|
|
22551
22995
|
const { reconcileMs, idleStopMs } = poolTimings();
|
|
@@ -22631,19 +23075,20 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22631
23075
|
const timer = setInterval(() => void tick(), reconcileMs);
|
|
22632
23076
|
return () => clearInterval(timer);
|
|
22633
23077
|
}
|
|
23078
|
+
var MODEL_REFRESH_MS = 15e3;
|
|
22634
23079
|
async function nodeUpCommand(specFile, opts = {}) {
|
|
22635
23080
|
try {
|
|
22636
|
-
const specPath =
|
|
22637
|
-
const specRealpath =
|
|
22638
|
-
const parsed = parseSpec(
|
|
23081
|
+
const specPath = resolve8(specFile);
|
|
23082
|
+
const specRealpath = realpathSync6.native(specPath);
|
|
23083
|
+
const parsed = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
|
|
22639
23084
|
const previous = readState();
|
|
22640
23085
|
if (previous && previous.supervisorPid !== process.pid && isAlive(previous.supervisorPid)) {
|
|
22641
23086
|
throw new Error(`a node is already running (pid ${previous.supervisorPid}, ${previous.specPath}). Stop it first.`);
|
|
22642
23087
|
}
|
|
22643
23088
|
for (const a of parsed.agents) {
|
|
22644
|
-
if (!
|
|
23089
|
+
if (!existsSync12(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
|
|
22645
23090
|
}
|
|
22646
|
-
const plain = parsed.agents.map((a) => ({ ...a, worktree:
|
|
23091
|
+
const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync6.native(a.worktree) }));
|
|
22647
23092
|
const pools = parsed.pools.map((pool) => {
|
|
22648
23093
|
const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
|
|
22649
23094
|
return { pool, members: poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] })) };
|
|
@@ -22673,6 +23118,20 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22673
23118
|
const inviteCodes = collectInvites(spec.agents.map((a) => a.name), opts.invite ?? [], process.env);
|
|
22674
23119
|
const creds = await ensureIdentities(spec, inviteCodes, specRealpath);
|
|
22675
23120
|
const runnable = spec.agents.filter((a) => creds.has(a.name));
|
|
23121
|
+
writeModelFiles(spec.agents);
|
|
23122
|
+
let modelError = "";
|
|
23123
|
+
const refreshModels = setInterval(() => {
|
|
23124
|
+
try {
|
|
23125
|
+
const fresh = new Map(allAgents(parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath))).map((a) => [a.name, a.model]));
|
|
23126
|
+
writeModelFiles(spec.agents.map((a) => ({ name: a.name, model: fresh.get(a.name) })));
|
|
23127
|
+
modelError = "";
|
|
23128
|
+
} catch (err) {
|
|
23129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23130
|
+
if (message !== modelError) console.log(source_default.yellow(` ! spec not re-read for models: ${message}`));
|
|
23131
|
+
modelError = message;
|
|
23132
|
+
}
|
|
23133
|
+
}, MODEL_REFRESH_MS);
|
|
23134
|
+
refreshModels.unref();
|
|
22676
23135
|
if (runnable.length === 0) throw new Error("no agent has credentials, nothing to start");
|
|
22677
23136
|
const mcp = resolveMcpServer({ here: HERE });
|
|
22678
23137
|
for (const a of runnable) prepareAgent(a, creds.get(a.name), mcp);
|
|
@@ -22711,6 +23170,12 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22711
23170
|
process.on("SIGTERM", shutdown);
|
|
22712
23171
|
process.on("SIGHUP", shutdown);
|
|
22713
23172
|
sup.start();
|
|
23173
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
23174
|
+
for (const a of runnable) {
|
|
23175
|
+
const c = creds.get(a.name);
|
|
23176
|
+
byRepo.set(c.repoId, [...byRepo.get(c.repoId) ?? [], { name: a.name, creds: c }]);
|
|
23177
|
+
}
|
|
23178
|
+
for (const group of byRepo.values()) stopLoops.push(startActivityLoop(group, sup));
|
|
22714
23179
|
for (const { pool, members } of pools) {
|
|
22715
23180
|
const ready = members.filter((m) => creds.has(m.name)).map((agent) => ({ agent, creds: creds.get(agent.name) }));
|
|
22716
23181
|
if (ready.length === 0) {
|
|
@@ -22731,9 +23196,9 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22731
23196
|
}
|
|
22732
23197
|
async function nodePruneCommand(specFile, opts = {}) {
|
|
22733
23198
|
try {
|
|
22734
|
-
const specPath =
|
|
22735
|
-
const specRealpath =
|
|
22736
|
-
const spec = parseSpec(
|
|
23199
|
+
const specPath = resolve8(specFile);
|
|
23200
|
+
const specRealpath = realpathSync6.native(specPath);
|
|
23201
|
+
const spec = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
|
|
22737
23202
|
const { orphans: names, unattributed } = classifyStored(allAgents(spec).map((a) => a.name), specRealpath);
|
|
22738
23203
|
if (names.length === 0) {
|
|
22739
23204
|
console.log("No stored credentials are missing from the spec.");
|
|
@@ -22839,6 +23304,7 @@ var token = program2.command("token").description("Manage your agent's API token
|
|
|
22839
23304
|
token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
|
|
22840
23305
|
token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
|
|
22841
23306
|
token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
|
|
23307
|
+
program2.command("onboard <github-url>").description("Set a GitHub repo up for pitboss: clone it, write a node spec (tier-2 architect, reviewer, writer pool) and print one device code that grants every agent. Starts nothing").option("--account <login>", "GitHub account to clone and work as, from `gh auth` (for example a personal account)").option("--dir <path>", "Parent directory for the clone, worktrees and spec (default ~/github)").option("--writer <runtime>", "Runtime for the writer pool (default claude)").option("--reviewer <runtime>", "Runtime for the reviewer; must differ from the writers (default copilot, or claude for non-claude writers)").option("--pool-max <n>", "Writer pool size (default 2)").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
|
|
22842
23308
|
var node = program2.command("node").description("Run this machine's agents headless and keep them running");
|
|
22843
23309
|
node.command("up <spec>").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them)").option(
|
|
22844
23310
|
"--invite <name=code>",
|