@pitboss/cli 0.4.8 → 0.4.9
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 +592 -241
- package/dist/event-worker.js +43 -11
- 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.
|
|
@@ -21170,6 +21178,18 @@ async function recordSessionUsage(conn, startIds, usage, logPrefix) {
|
|
|
21170
21178
|
await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
|
|
21171
21179
|
}
|
|
21172
21180
|
|
|
21181
|
+
// ../claude-worker/src/model.ts
|
|
21182
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
21183
|
+
function pinnedModel(env2 = process.env) {
|
|
21184
|
+
const file = env2.PITBOSS_MODEL_FILE;
|
|
21185
|
+
if (!file) return void 0;
|
|
21186
|
+
try {
|
|
21187
|
+
return readFileSync11(file, "utf-8").trim() || void 0;
|
|
21188
|
+
} catch {
|
|
21189
|
+
return void 0;
|
|
21190
|
+
}
|
|
21191
|
+
}
|
|
21192
|
+
|
|
21173
21193
|
// ../claude-worker/src/session.ts
|
|
21174
21194
|
function mcpServerEntry() {
|
|
21175
21195
|
return fileURLToPath2(new URL("../../mcp-server/src/index.ts", import.meta.url));
|
|
@@ -21207,7 +21227,7 @@ async function runClaudeSession(config) {
|
|
|
21207
21227
|
const mcpConfigPath = writeMcpConfig(config);
|
|
21208
21228
|
const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
|
|
21209
21229
|
try {
|
|
21210
|
-
return await new Promise((
|
|
21230
|
+
return await new Promise((resolve9, reject) => {
|
|
21211
21231
|
const args = [
|
|
21212
21232
|
"--print",
|
|
21213
21233
|
"--mcp-config",
|
|
@@ -21218,7 +21238,7 @@ async function runClaudeSession(config) {
|
|
|
21218
21238
|
"stream-json",
|
|
21219
21239
|
"--verbose",
|
|
21220
21240
|
"--model",
|
|
21221
|
-
config.model,
|
|
21241
|
+
pinnedModel() ?? config.model,
|
|
21222
21242
|
"--add-dir",
|
|
21223
21243
|
config.repoPath
|
|
21224
21244
|
];
|
|
@@ -21287,7 +21307,7 @@ async function runClaudeSession(config) {
|
|
|
21287
21307
|
const detail = (resultError || stderrOutput || "").trim();
|
|
21288
21308
|
reject(new Error(`claude exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21289
21309
|
} else {
|
|
21290
|
-
|
|
21310
|
+
resolve9(unique);
|
|
21291
21311
|
}
|
|
21292
21312
|
};
|
|
21293
21313
|
finish.then(settle, settle);
|
|
@@ -21339,8 +21359,8 @@ function classifySessionError(text) {
|
|
|
21339
21359
|
|
|
21340
21360
|
// ../claude-worker/src/adapters/cursor.ts
|
|
21341
21361
|
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];
|
|
21362
|
+
function cursorArgs(repoPath, prompt, model) {
|
|
21363
|
+
return ["-p", "--trust", "--force", "--output-format", "stream-json", "--workspace", repoPath, ...model ? ["--model", model] : [], prompt];
|
|
21344
21364
|
}
|
|
21345
21365
|
function readCursorEvent(line) {
|
|
21346
21366
|
let event;
|
|
@@ -21364,10 +21384,10 @@ function readCursorEvent(line) {
|
|
|
21364
21384
|
return {};
|
|
21365
21385
|
}
|
|
21366
21386
|
async function runCursorSession(config) {
|
|
21367
|
-
const args = cursorArgs(config.repoPath, buildPrompt(config));
|
|
21387
|
+
const args = cursorArgs(config.repoPath, buildPrompt(config), pinnedModel());
|
|
21368
21388
|
console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
|
|
21369
21389
|
const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
|
|
21370
|
-
return new Promise((
|
|
21390
|
+
return new Promise((resolve9, reject) => {
|
|
21371
21391
|
const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21372
21392
|
console.log(`[cursor-worker] Process PID: ${proc.pid}`);
|
|
21373
21393
|
const toolsUsed = [];
|
|
@@ -21406,7 +21426,7 @@ async function runCursorSession(config) {
|
|
|
21406
21426
|
const detail = (resultError || stderrOutput).trim();
|
|
21407
21427
|
reject(new Error(`cursor-agent exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21408
21428
|
} else {
|
|
21409
|
-
|
|
21429
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21410
21430
|
}
|
|
21411
21431
|
};
|
|
21412
21432
|
finish.then(settle, settle);
|
|
@@ -21445,10 +21465,11 @@ import { unlinkSync as unlinkSync2 } from "fs";
|
|
|
21445
21465
|
var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
|
|
21446
21466
|
var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
|
|
21447
21467
|
var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
|
|
21448
|
-
function copilotArgs(repoPath, prompt, mcpConfigPath) {
|
|
21468
|
+
function copilotArgs(repoPath, prompt, mcpConfigPath, model) {
|
|
21449
21469
|
return [
|
|
21450
21470
|
"-p",
|
|
21451
21471
|
prompt,
|
|
21472
|
+
...model ? ["--model", model] : [],
|
|
21452
21473
|
"--allow-all-tools",
|
|
21453
21474
|
"--output-format",
|
|
21454
21475
|
"json",
|
|
@@ -21478,9 +21499,9 @@ function readCopilotEvent(line) {
|
|
|
21478
21499
|
}
|
|
21479
21500
|
async function runCopilotSession(config) {
|
|
21480
21501
|
const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
|
|
21481
|
-
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
|
|
21502
|
+
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath, pinnedModel());
|
|
21482
21503
|
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
21483
|
-
return new Promise((
|
|
21504
|
+
return new Promise((resolve9, reject) => {
|
|
21484
21505
|
const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21485
21506
|
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
21486
21507
|
const toolsUsed = [];
|
|
@@ -21517,7 +21538,7 @@ async function runCopilotSession(config) {
|
|
|
21517
21538
|
const detail = (sessionError || stderrOutput).trim();
|
|
21518
21539
|
reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21519
21540
|
} else {
|
|
21520
|
-
|
|
21541
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21521
21542
|
}
|
|
21522
21543
|
});
|
|
21523
21544
|
proc.on("error", (err) => {
|
|
@@ -21551,7 +21572,7 @@ var bin3 = () => process.env.CODEX_BIN ?? "codex";
|
|
|
21551
21572
|
var codexHome = () => process.env.CODEX_HOME ?? join14(homedir5(), ".codex");
|
|
21552
21573
|
var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
|
|
21553
21574
|
var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
|
|
21554
|
-
function codexArgs(repoPath, prompt, profile) {
|
|
21575
|
+
function codexArgs(repoPath, prompt, profile, model) {
|
|
21555
21576
|
return [
|
|
21556
21577
|
"exec",
|
|
21557
21578
|
"--json",
|
|
@@ -21562,6 +21583,7 @@ function codexArgs(repoPath, prompt, profile) {
|
|
|
21562
21583
|
repoPath,
|
|
21563
21584
|
"-p",
|
|
21564
21585
|
profile,
|
|
21586
|
+
...model ? ["-m", model] : [],
|
|
21565
21587
|
prompt
|
|
21566
21588
|
];
|
|
21567
21589
|
}
|
|
@@ -21610,9 +21632,9 @@ async function runCodexSession(config) {
|
|
|
21610
21632
|
} catch {
|
|
21611
21633
|
}
|
|
21612
21634
|
};
|
|
21613
|
-
const args = codexArgs(config.repoPath, buildPrompt(config), profile);
|
|
21635
|
+
const args = codexArgs(config.repoPath, buildPrompt(config), profile, pinnedModel());
|
|
21614
21636
|
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
21615
|
-
return new Promise((
|
|
21637
|
+
return new Promise((resolve9, reject) => {
|
|
21616
21638
|
const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21617
21639
|
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
21618
21640
|
const toolsUsed = [];
|
|
@@ -21646,7 +21668,7 @@ async function runCodexSession(config) {
|
|
|
21646
21668
|
const detail = (sessionError || stderrOutput).trim();
|
|
21647
21669
|
reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21648
21670
|
} else {
|
|
21649
|
-
|
|
21671
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21650
21672
|
}
|
|
21651
21673
|
});
|
|
21652
21674
|
proc.on("error", (err) => {
|
|
@@ -21683,7 +21705,65 @@ var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
|
|
|
21683
21705
|
// ../claude-worker/src/config.ts
|
|
21684
21706
|
var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
|
|
21685
21707
|
|
|
21708
|
+
// src/node/onboard.ts
|
|
21709
|
+
var MAX_POOL = 30;
|
|
21710
|
+
var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
|
|
21711
|
+
function planOnboard(url, opts) {
|
|
21712
|
+
const match = GITHUB.exec(url.trim());
|
|
21713
|
+
if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
|
|
21714
|
+
const [, owner, name] = match;
|
|
21715
|
+
const writer = opts.writer ?? "claude";
|
|
21716
|
+
const reviewer = opts.reviewer ?? (writer === "claude" ? "copilot" : "claude");
|
|
21717
|
+
if (reviewer === writer) throw new Error(`the reviewer needs a different runtime from the writers (both are ${writer})`);
|
|
21718
|
+
for (const r of [writer, reviewer]) {
|
|
21719
|
+
if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
|
|
21720
|
+
}
|
|
21721
|
+
const poolMax = opts.poolMax ?? 2;
|
|
21722
|
+
if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
|
|
21723
|
+
throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
|
|
21724
|
+
}
|
|
21725
|
+
const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
|
|
21726
|
+
const parent = resolve4(opts.parent ?? join15(opts.home, "github"));
|
|
21727
|
+
const base2 = join15(parent, name);
|
|
21728
|
+
const nodeDir2 = join15(parent, `${name}-node`);
|
|
21729
|
+
const architect = join15(parent, `${name}-architect`);
|
|
21730
|
+
const review = join15(parent, `${name}-reviewer`);
|
|
21731
|
+
return {
|
|
21732
|
+
owner,
|
|
21733
|
+
name,
|
|
21734
|
+
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
21735
|
+
base: base2,
|
|
21736
|
+
nodeDir: nodeDir2,
|
|
21737
|
+
specPath: join15(nodeDir2, "spec.json"),
|
|
21738
|
+
configDir: join15(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
|
|
21739
|
+
worktrees: [architect, review],
|
|
21740
|
+
spec: {
|
|
21741
|
+
api: "https://api.pitboss.dev",
|
|
21742
|
+
agents: [
|
|
21743
|
+
{ name: `${id}-architect`, runtime: "claude", worktree: `../${name}-architect`, specialization: "architect", tier: 2 },
|
|
21744
|
+
{ name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
|
|
21745
|
+
{
|
|
21746
|
+
pool: `${id}-pool`,
|
|
21747
|
+
runtime: writer,
|
|
21748
|
+
base: `../${name}`,
|
|
21749
|
+
worktreeRoot: `../${name}-pool`,
|
|
21750
|
+
specialization: "writer",
|
|
21751
|
+
min: 0,
|
|
21752
|
+
max: poolMax
|
|
21753
|
+
}
|
|
21754
|
+
]
|
|
21755
|
+
}
|
|
21756
|
+
};
|
|
21757
|
+
}
|
|
21758
|
+
function originHost(origin) {
|
|
21759
|
+
const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
|
|
21760
|
+
if (url) return url[1].toLowerCase();
|
|
21761
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
|
|
21762
|
+
return scp ? scp[1].toLowerCase() : null;
|
|
21763
|
+
}
|
|
21764
|
+
|
|
21686
21765
|
// src/node/spec.ts
|
|
21766
|
+
import { join as join16, resolve as resolve5, sep as sep2 } from "node:path";
|
|
21687
21767
|
var DEFAULT_API2 = "https://api.pitboss.dev";
|
|
21688
21768
|
var MAX_POOL_SIZE = 32;
|
|
21689
21769
|
var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
@@ -21695,7 +21775,14 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
|
|
|
21695
21775
|
function poolMembers(pool) {
|
|
21696
21776
|
return Array.from({ length: pool.max }, (_, i) => {
|
|
21697
21777
|
const name = `${pool.pool}-${i + 1}`;
|
|
21698
|
-
return {
|
|
21778
|
+
return {
|
|
21779
|
+
name,
|
|
21780
|
+
runtime: pool.runtime,
|
|
21781
|
+
worktree: join16(pool.worktreeRoot, name),
|
|
21782
|
+
specialization: pool.specialization,
|
|
21783
|
+
...pool.tier ? { tier: pool.tier } : {},
|
|
21784
|
+
...pool.model ? { model: pool.model } : {}
|
|
21785
|
+
};
|
|
21699
21786
|
});
|
|
21700
21787
|
}
|
|
21701
21788
|
var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
|
|
@@ -21712,6 +21799,18 @@ function specializationOf(a, i) {
|
|
|
21712
21799
|
}
|
|
21713
21800
|
return specialization;
|
|
21714
21801
|
}
|
|
21802
|
+
function tierOf(a, i, specialization) {
|
|
21803
|
+
if (a.tier === void 0) return specialization === "architect" ? { tier: 2 } : {};
|
|
21804
|
+
if (a.tier !== 1 && a.tier !== 2) throw new Error(`agents[${i}].tier must be 1 or 2`);
|
|
21805
|
+
return { tier: a.tier };
|
|
21806
|
+
}
|
|
21807
|
+
function modelOf(a, i) {
|
|
21808
|
+
if (a.model === void 0) return {};
|
|
21809
|
+
if (typeof a.model !== "string" || !/^[\w.:/@-]{1,100}$/.test(a.model)) {
|
|
21810
|
+
throw new Error(`agents[${i}].model must be a model name (letters, digits and . : / @ _ -), at most 100 characters`);
|
|
21811
|
+
}
|
|
21812
|
+
return { model: a.model };
|
|
21813
|
+
}
|
|
21715
21814
|
function parsePool(a, i, specDir) {
|
|
21716
21815
|
if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
|
|
21717
21816
|
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 +21826,10 @@ function parsePool(a, i, specDir) {
|
|
|
21727
21826
|
if (typeof min !== "number" || !Number.isInteger(min) || min < 0 || min > max) {
|
|
21728
21827
|
throw new Error(`agents[${i}].min must be an integer from 0 to max`);
|
|
21729
21828
|
}
|
|
21730
|
-
const base2 =
|
|
21731
|
-
const worktreeRoot =
|
|
21829
|
+
const base2 = resolve5(specDir, a.base);
|
|
21830
|
+
const worktreeRoot = resolve5(specDir, a.worktreeRoot);
|
|
21732
21831
|
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 };
|
|
21832
|
+
return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), min, max };
|
|
21734
21833
|
}
|
|
21735
21834
|
function parseSpec(text, specDir) {
|
|
21736
21835
|
let raw;
|
|
@@ -21756,7 +21855,8 @@ function parseSpec(text, specDir) {
|
|
|
21756
21855
|
if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
|
|
21757
21856
|
if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
|
|
21758
21857
|
const runtime = runtimeOf(a, i);
|
|
21759
|
-
|
|
21858
|
+
const specialization = specializationOf(a, i);
|
|
21859
|
+
agents.push({ name: a.name, runtime, worktree: resolve5(specDir, a.worktree), specialization, ...tierOf(a, i, specialization), ...modelOf(a, i) });
|
|
21760
21860
|
});
|
|
21761
21861
|
for (const p of pools) {
|
|
21762
21862
|
for (const a of agents) {
|
|
@@ -21783,13 +21883,15 @@ function parseSpec(text, specDir) {
|
|
|
21783
21883
|
}
|
|
21784
21884
|
|
|
21785
21885
|
// src/node/store.ts
|
|
21786
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as
|
|
21787
|
-
import { dirname as dirname8, join as
|
|
21886
|
+
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";
|
|
21887
|
+
import { dirname as dirname8, join as join17 } from "node:path";
|
|
21788
21888
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
21789
|
-
var nodeDir = () =>
|
|
21790
|
-
var credsPath = (name) =>
|
|
21791
|
-
var statePath2 = () =>
|
|
21792
|
-
var logPath = (name) =>
|
|
21889
|
+
var nodeDir = () => join17(configDir(), "node");
|
|
21890
|
+
var credsPath = (name) => join17(nodeDir(), "agents", `${name}.json`);
|
|
21891
|
+
var statePath2 = () => join17(nodeDir(), "state.json");
|
|
21892
|
+
var logPath = (name) => join17(nodeDir(), "logs", `${name}.log`);
|
|
21893
|
+
var activityPath = (name) => join17(nodeDir(), "activity", name);
|
|
21894
|
+
var modelPath = (name) => join17(nodeDir(), "model", name);
|
|
21793
21895
|
function writePrivate(path3, value) {
|
|
21794
21896
|
mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
|
|
21795
21897
|
const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
|
|
@@ -21798,7 +21900,7 @@ function writePrivate(path3, value) {
|
|
|
21798
21900
|
}
|
|
21799
21901
|
function readJson(path3) {
|
|
21800
21902
|
if (!existsSync8(path3)) return null;
|
|
21801
|
-
return JSON.parse(
|
|
21903
|
+
return JSON.parse(readFileSync12(path3, "utf-8"));
|
|
21802
21904
|
}
|
|
21803
21905
|
var readCreds = (name) => readJson(credsPath(name));
|
|
21804
21906
|
var writeCreds = (name, creds) => writePrivate(credsPath(name), creds);
|
|
@@ -21807,14 +21909,187 @@ var removeCreds = (name) => {
|
|
|
21807
21909
|
if (existsSync8(path3)) rmSync4(path3);
|
|
21808
21910
|
};
|
|
21809
21911
|
var listStoredAgentNames = () => {
|
|
21810
|
-
const dir =
|
|
21912
|
+
const dir = join17(nodeDir(), "agents");
|
|
21811
21913
|
if (!existsSync8(dir)) return [];
|
|
21812
21914
|
return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
21813
21915
|
};
|
|
21814
21916
|
var readState = () => readJson(statePath2());
|
|
21815
21917
|
var writeState = (state) => writePrivate(statePath2(), state);
|
|
21816
21918
|
|
|
21919
|
+
// src/node/pool.ts
|
|
21920
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
21921
|
+
import { existsSync as existsSync9, lstatSync as lstatSync3, mkdirSync as mkdirSync8, realpathSync as realpathSync4 } from "node:fs";
|
|
21922
|
+
import { basename as basename2, dirname as dirname9, join as join18, resolve as resolve6 } from "node:path";
|
|
21923
|
+
var IDLE_STOP_MS = 10 * 6e4;
|
|
21924
|
+
var RECONCILE_MS = 15e3;
|
|
21925
|
+
var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
|
|
21926
|
+
var REMARK_AFTER_MS = 24 * 60 * 6e4;
|
|
21927
|
+
function positiveMs(raw, fallback2) {
|
|
21928
|
+
const n = Number(raw);
|
|
21929
|
+
return raw && Number.isInteger(n) && n > 0 ? n : fallback2;
|
|
21930
|
+
}
|
|
21931
|
+
function poolTimings(env2 = process.env) {
|
|
21932
|
+
return {
|
|
21933
|
+
reconcileMs: positiveMs(env2.PITBOSS_POOL_RECONCILE_MS, RECONCILE_MS),
|
|
21934
|
+
idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
|
|
21935
|
+
};
|
|
21936
|
+
}
|
|
21937
|
+
function effectiveBounds(spec, desired) {
|
|
21938
|
+
if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
|
|
21939
|
+
const max = Math.min(desired.max, spec.max);
|
|
21940
|
+
return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
|
|
21941
|
+
}
|
|
21942
|
+
function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
21943
|
+
const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
|
|
21944
|
+
const running = members.filter((m) => m.running);
|
|
21945
|
+
const stopped = members.filter((m) => !m.running);
|
|
21946
|
+
let count = running.length;
|
|
21947
|
+
const start = [];
|
|
21948
|
+
const take = (m) => {
|
|
21949
|
+
start.push(m.name);
|
|
21950
|
+
count++;
|
|
21951
|
+
};
|
|
21952
|
+
for (const m of stopped) {
|
|
21953
|
+
if (count >= max) break;
|
|
21954
|
+
if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
|
|
21955
|
+
}
|
|
21956
|
+
const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
|
|
21957
|
+
let demand = pending - running.filter(isIdle).length;
|
|
21958
|
+
for (const m of spare()) {
|
|
21959
|
+
if (demand <= 0 || count >= max) break;
|
|
21960
|
+
take(m);
|
|
21961
|
+
demand--;
|
|
21962
|
+
}
|
|
21963
|
+
for (const m of spare()) {
|
|
21964
|
+
if (count >= min || count >= max) break;
|
|
21965
|
+
take(m);
|
|
21966
|
+
}
|
|
21967
|
+
if (start.length > 0) return { start, stop: [] };
|
|
21968
|
+
const stop = [];
|
|
21969
|
+
let surplus = running.filter(isIdle).length - pending;
|
|
21970
|
+
const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
|
|
21971
|
+
for (const m of eligible) {
|
|
21972
|
+
if (surplus <= 0 || count <= min) break;
|
|
21973
|
+
stop.push(m.name);
|
|
21974
|
+
count--;
|
|
21975
|
+
surplus--;
|
|
21976
|
+
}
|
|
21977
|
+
return { start, stop };
|
|
21978
|
+
}
|
|
21979
|
+
function summarizeTasks(ready, held, members) {
|
|
21980
|
+
const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
|
|
21981
|
+
const counts = Object.fromEntries(
|
|
21982
|
+
members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
|
|
21983
|
+
);
|
|
21984
|
+
const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
|
|
21985
|
+
let readyAssigned = 0;
|
|
21986
|
+
for (const t of ready) {
|
|
21987
|
+
const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
21988
|
+
if (!name) continue;
|
|
21989
|
+
counts[name].readyAssigned++;
|
|
21990
|
+
readyAssigned++;
|
|
21991
|
+
}
|
|
21992
|
+
for (const t of held) {
|
|
21993
|
+
const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
21994
|
+
if (!name) continue;
|
|
21995
|
+
if (t.status === "assigned") counts[name].assigned++;
|
|
21996
|
+
if (t.status === "in_progress") counts[name].inProgress++;
|
|
21997
|
+
}
|
|
21998
|
+
return { pending, ready: pending + readyAssigned, members: counts };
|
|
21999
|
+
}
|
|
22000
|
+
function poolRootProblem(root, protectedPaths) {
|
|
22001
|
+
for (const p of protectedPaths) {
|
|
22002
|
+
if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
|
|
22003
|
+
}
|
|
22004
|
+
return null;
|
|
22005
|
+
}
|
|
22006
|
+
function memberPathProblem(s) {
|
|
22007
|
+
if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
|
|
22008
|
+
if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
|
|
22009
|
+
if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
|
|
22010
|
+
return null;
|
|
22011
|
+
}
|
|
22012
|
+
function realpathAllowMissing(path3) {
|
|
22013
|
+
const abs = resolve6(path3);
|
|
22014
|
+
if (existsSync9(abs)) return realpathSync4.native(abs);
|
|
22015
|
+
return join18(realpathAllowMissing(dirname9(abs)), basename2(abs));
|
|
22016
|
+
}
|
|
22017
|
+
function run2(cwd, args) {
|
|
22018
|
+
try {
|
|
22019
|
+
return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
22020
|
+
} catch (err) {
|
|
22021
|
+
const stderr = err.stderr?.toString().trim();
|
|
22022
|
+
throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
|
|
22023
|
+
}
|
|
22024
|
+
}
|
|
22025
|
+
var commonDir = (cwd) => {
|
|
22026
|
+
const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
|
|
22027
|
+
return dir ? realpathSync4.native(dir) : null;
|
|
22028
|
+
};
|
|
22029
|
+
function defaultBranch(cwd) {
|
|
22030
|
+
const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
22031
|
+
let ref = head();
|
|
22032
|
+
if (!ref) {
|
|
22033
|
+
git(["remote", "set-head", "origin", "--auto"], cwd);
|
|
22034
|
+
ref = head();
|
|
22035
|
+
}
|
|
22036
|
+
if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
|
|
22037
|
+
return ref.slice("origin/".length);
|
|
22038
|
+
}
|
|
22039
|
+
function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
22040
|
+
if (!existsSync9(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
|
|
22041
|
+
const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
|
|
22042
|
+
const baseCommon = commonDir(pool.base);
|
|
22043
|
+
if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
|
|
22044
|
+
const root = realpathAllowMissing(pool.worktreeRoot);
|
|
22045
|
+
const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
|
|
22046
|
+
if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
|
|
22047
|
+
const members = poolMembers({ ...pool, worktreeRoot: root });
|
|
22048
|
+
for (const m of members) {
|
|
22049
|
+
let symlink = false;
|
|
22050
|
+
try {
|
|
22051
|
+
symlink = lstatSync3(m.worktree).isSymbolicLink();
|
|
22052
|
+
} catch {
|
|
22053
|
+
symlink = false;
|
|
22054
|
+
}
|
|
22055
|
+
const exists = !symlink && existsSync9(m.worktree);
|
|
22056
|
+
const problem = memberPathProblem({
|
|
22057
|
+
path: m.worktree,
|
|
22058
|
+
symlink,
|
|
22059
|
+
exists,
|
|
22060
|
+
baseCommonDir: baseCommon,
|
|
22061
|
+
commonDir: exists ? commonDir(m.worktree) : null,
|
|
22062
|
+
tracked: !exists && !symlink && isTracked(m.worktree)
|
|
22063
|
+
});
|
|
22064
|
+
if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
|
|
22065
|
+
}
|
|
22066
|
+
const missing = members.filter((m) => !existsSync9(m.worktree));
|
|
22067
|
+
if (missing.length > 0) {
|
|
22068
|
+
mkdirSync8(root, { recursive: true });
|
|
22069
|
+
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22070
|
+
const branch = defaultBranch(pool.base);
|
|
22071
|
+
run2(pool.base, ["worktree", "prune"]);
|
|
22072
|
+
for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
|
|
22073
|
+
}
|
|
22074
|
+
return members.map((m) => realpathSync4.native(m.worktree));
|
|
22075
|
+
}
|
|
22076
|
+
function resetWorktree(worktree) {
|
|
22077
|
+
run2(worktree, ["fetch", "-q", "--prune", "origin"]);
|
|
22078
|
+
const target = `origin/${defaultBranch(worktree)}`;
|
|
22079
|
+
run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
|
|
22080
|
+
run2(worktree, ["reset", "-q", "--hard", target]);
|
|
22081
|
+
run2(worktree, ["clean", "-fdq"]);
|
|
22082
|
+
}
|
|
22083
|
+
|
|
21817
22084
|
// src/node/identity.ts
|
|
22085
|
+
function requestedAgents(agents) {
|
|
22086
|
+
return agents.map((a) => ({
|
|
22087
|
+
name: a.name,
|
|
22088
|
+
workerType: getAdapter(a.runtime).workerType,
|
|
22089
|
+
specialization: a.specialization,
|
|
22090
|
+
...a.tier ? { tier: a.tier } : {}
|
|
22091
|
+
}));
|
|
22092
|
+
}
|
|
21818
22093
|
function collectInvites(agentNames, flags, env2) {
|
|
21819
22094
|
const byLower = new Map(agentNames.map((n) => [n.toLowerCase(), n]));
|
|
21820
22095
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -21851,6 +22126,7 @@ function matchInvites(agents, invites) {
|
|
|
21851
22126
|
}
|
|
21852
22127
|
return { matched, ungranted, extra: left, refused };
|
|
21853
22128
|
}
|
|
22129
|
+
var MAX_REQUESTED = 32;
|
|
21854
22130
|
function groupByRepo(agents, originOf2) {
|
|
21855
22131
|
const groups = /* @__PURE__ */ new Map();
|
|
21856
22132
|
for (const agent of agents) {
|
|
@@ -21858,7 +22134,10 @@ function groupByRepo(agents, originOf2) {
|
|
|
21858
22134
|
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
22135
|
groups.set(remote, [...groups.get(remote) ?? [], agent]);
|
|
21860
22136
|
}
|
|
21861
|
-
return [...groups].
|
|
22137
|
+
return [...groups].flatMap(([remote, members]) => Array.from({ length: Math.ceil(members.length / MAX_REQUESTED) }, (_, i) => ({
|
|
22138
|
+
remote,
|
|
22139
|
+
agents: members.slice(i * MAX_REQUESTED, (i + 1) * MAX_REQUESTED)
|
|
22140
|
+
})));
|
|
21862
22141
|
}
|
|
21863
22142
|
var originOf = (worktree) => git(["remote", "get-url", "origin"], worktree);
|
|
21864
22143
|
async function abandonMinted(spec, agent, minted, reason) {
|
|
@@ -21964,10 +22243,12 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
21964
22243
|
console.log(source_default.bold(`
|
|
21965
22244
|
Approve ${repoName}`));
|
|
21966
22245
|
console.log(` On the approval screen add these agents as workers:`);
|
|
21967
|
-
for (const a of group.agents)
|
|
22246
|
+
for (const a of group.agents) {
|
|
22247
|
+
console.log(` ${source_default.cyan(a.name)} ${source_default.dim(`${a.runtime}, ${a.specialization}${a.tier ? `, tier ${a.tier}` : ""}`)}`);
|
|
22248
|
+
}
|
|
21968
22249
|
let grant;
|
|
21969
22250
|
try {
|
|
21970
|
-
grant = await requestDeviceGrant(spec.api, { repoName, remote: group.remote }, "pitboss node up");
|
|
22251
|
+
grant = await requestDeviceGrant(spec.api, { repoName, remote: group.remote, agents: requestedAgents(group.agents) }, "pitboss node up");
|
|
21971
22252
|
} catch (err) {
|
|
21972
22253
|
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
22254
|
continue;
|
|
@@ -22007,6 +22288,106 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
22007
22288
|
return have;
|
|
22008
22289
|
}
|
|
22009
22290
|
|
|
22291
|
+
// src/commands/onboard.ts
|
|
22292
|
+
function isGitHubHost(host) {
|
|
22293
|
+
if (!host) return false;
|
|
22294
|
+
if (host === "github.com") return true;
|
|
22295
|
+
try {
|
|
22296
|
+
const resolved = execFileSync7("ssh", ["-G", host], { encoding: "utf-8" }).match(/^hostname (\S+)$/m)?.[1];
|
|
22297
|
+
return resolved?.toLowerCase() === "github.com";
|
|
22298
|
+
} catch {
|
|
22299
|
+
return false;
|
|
22300
|
+
}
|
|
22301
|
+
}
|
|
22302
|
+
var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
22303
|
+
async function onboardCommand(url, opts) {
|
|
22304
|
+
try {
|
|
22305
|
+
const plan = planOnboard(url, {
|
|
22306
|
+
home: homedir6(),
|
|
22307
|
+
parent: opts.dir,
|
|
22308
|
+
writer: opts.writer,
|
|
22309
|
+
reviewer: opts.reviewer,
|
|
22310
|
+
poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax)
|
|
22311
|
+
});
|
|
22312
|
+
const env2 = { ...process.env };
|
|
22313
|
+
if (opts.account) {
|
|
22314
|
+
env2.GH_TOKEN = execFileSync7("gh", ["auth", "token", "--hostname", "github.com", "--user", opts.account], { encoding: "utf-8" }).trim();
|
|
22315
|
+
}
|
|
22316
|
+
if (existsSync10(plan.base)) {
|
|
22317
|
+
const origin = execFileSync7("git", ["-C", plan.base, "remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
|
|
22318
|
+
if (normalizeRepoUrl(origin) !== normalizeRepoUrl(plan.cloneUrl) || !isGitHubHost(originHost(origin))) {
|
|
22319
|
+
throw new Error(`${plan.base} exists but its origin is ${origin}, not ${plan.cloneUrl} on github.com`);
|
|
22320
|
+
}
|
|
22321
|
+
console.log(source_default.dim(` using the clone at ${plan.base}`));
|
|
22322
|
+
} else {
|
|
22323
|
+
execFileSync7("gh", ["repo", "clone", `${plan.owner}/${plan.name}`, plan.base], { stdio: "inherit", env: env2 });
|
|
22324
|
+
}
|
|
22325
|
+
const commonDir2 = (dir) => realpathSync5.native(
|
|
22326
|
+
execFileSync7("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf-8" }).trim()
|
|
22327
|
+
);
|
|
22328
|
+
for (const path3 of plan.worktrees) {
|
|
22329
|
+
if (!existsSync10(path3)) {
|
|
22330
|
+
execFileSync7("git", ["-C", plan.base, "worktree", "add", "--detach", path3], { stdio: "inherit" });
|
|
22331
|
+
} else if (commonDir2(path3) !== commonDir2(plan.base)) {
|
|
22332
|
+
throw new Error(`${path3} exists but is not a worktree of ${plan.base}; move it aside or pass --dir`);
|
|
22333
|
+
}
|
|
22334
|
+
}
|
|
22335
|
+
const specText = JSON.stringify(plan.spec, null, 2) + "\n";
|
|
22336
|
+
const parsed = parseSpec(specText, plan.nodeDir);
|
|
22337
|
+
if (existsSync10(plan.specPath) && readFileSync13(plan.specPath, "utf-8") !== specText && !opts.force) {
|
|
22338
|
+
throw new Error(`${plan.specPath} already exists with different contents; pass --force to replace it`);
|
|
22339
|
+
}
|
|
22340
|
+
mkdirSync9(plan.nodeDir, { recursive: true });
|
|
22341
|
+
writeFileSync9(plan.specPath, specText);
|
|
22342
|
+
process.env.PITBOSS_CONFIG_DIR = plan.configDir;
|
|
22343
|
+
mkdirSync9(plan.configDir, { recursive: true, mode: 448 });
|
|
22344
|
+
const specReal = realpathSync5.native(plan.specPath);
|
|
22345
|
+
const foreign = allAgents(parsed).map((a) => readCreds(a.name)).find((c) => c?.spec && c.spec !== specReal);
|
|
22346
|
+
if (foreign) throw new Error(`${plan.configDir} holds credentials for another node spec (${foreign.spec}); onboard will not overwrite them`);
|
|
22347
|
+
const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync5.native(a.worktree) }));
|
|
22348
|
+
const ownToken = process.env.GH_TOKEN;
|
|
22349
|
+
if (env2.GH_TOKEN) process.env.GH_TOKEN = env2.GH_TOKEN;
|
|
22350
|
+
let pooled;
|
|
22351
|
+
try {
|
|
22352
|
+
pooled = parsed.pools.flatMap((pool) => {
|
|
22353
|
+
const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
|
|
22354
|
+
return poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] }));
|
|
22355
|
+
});
|
|
22356
|
+
} finally {
|
|
22357
|
+
if (ownToken === void 0) delete process.env.GH_TOKEN;
|
|
22358
|
+
else process.env.GH_TOKEN = ownToken;
|
|
22359
|
+
}
|
|
22360
|
+
const everyone = [...plain, ...pooled];
|
|
22361
|
+
const seen = /* @__PURE__ */ new Map();
|
|
22362
|
+
for (const a of everyone) {
|
|
22363
|
+
const other = seen.get(a.worktree);
|
|
22364
|
+
if (other) throw new Error(`agents "${other}" and "${a.name}" resolve to the same worktree ${a.worktree}; each needs its own`);
|
|
22365
|
+
seen.set(a.worktree, a.name);
|
|
22366
|
+
}
|
|
22367
|
+
const creds = await ensureIdentities({ ...parsed, agents: everyone }, /* @__PURE__ */ new Map(), specReal);
|
|
22368
|
+
const missing = everyone.filter((a) => !creds.has(a.name)).map((a) => a.name);
|
|
22369
|
+
if (missing.length === everyone.length) throw new Error("no agent was granted; approve the device code again by re-running onboard");
|
|
22370
|
+
if (missing.length > 0) process.exitCode = 1;
|
|
22371
|
+
console.log(source_default.bold(`
|
|
22372
|
+
${plan.name} is onboarded${missing.length ? source_default.yellow(` except ${missing.join(", ")}`) : ""}.`));
|
|
22373
|
+
console.log(` spec ${plan.specPath}`);
|
|
22374
|
+
console.log(` config ${plan.configDir}`);
|
|
22375
|
+
console.log(`
|
|
22376
|
+
Start it when you want it running (long-running, foreground):`);
|
|
22377
|
+
console.log(source_default.cyan(` cd ${shellQuote(plan.nodeDir)} && PITBOSS_CONFIG_DIR=${shellQuote(plan.configDir)} pitboss node up spec.json`));
|
|
22378
|
+
if (opts.account) console.log(source_default.dim(` with GH_TOKEN from \`gh auth token --user ${opts.account}\` in that shell`));
|
|
22379
|
+
} catch (err) {
|
|
22380
|
+
console.error(source_default.red(` \u2715 ${err instanceof Error ? err.message : String(err)}`));
|
|
22381
|
+
process.exitCode = 1;
|
|
22382
|
+
}
|
|
22383
|
+
}
|
|
22384
|
+
|
|
22385
|
+
// src/commands/node.ts
|
|
22386
|
+
import { execFileSync as execFileSync8, spawn as spawnChild } from "node:child_process";
|
|
22387
|
+
import { closeSync, existsSync as existsSync12, mkdirSync as mkdirSync11, openSync, readFileSync as readFileSync15, realpathSync as realpathSync6, rmSync as rmSync6 } from "node:fs";
|
|
22388
|
+
import { dirname as dirname12, resolve as resolve8 } from "node:path";
|
|
22389
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
22390
|
+
|
|
22010
22391
|
// src/node/supervisor.ts
|
|
22011
22392
|
function nextDelay(consecutiveFailures, { baseMs, maxMs }) {
|
|
22012
22393
|
return Math.min(baseMs * 2 ** Math.max(0, consecutiveFailures - 1), maxMs);
|
|
@@ -22131,44 +22512,67 @@ var Supervisor = class {
|
|
|
22131
22512
|
}
|
|
22132
22513
|
};
|
|
22133
22514
|
|
|
22515
|
+
// src/node/model.ts
|
|
22516
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
22517
|
+
import { dirname as dirname10 } from "node:path";
|
|
22518
|
+
function writeModelFiles(agents) {
|
|
22519
|
+
for (const a of agents) {
|
|
22520
|
+
const path3 = modelPath(a.name);
|
|
22521
|
+
if (!a.model) {
|
|
22522
|
+
rmSync5(path3, { force: true });
|
|
22523
|
+
continue;
|
|
22524
|
+
}
|
|
22525
|
+
let current = null;
|
|
22526
|
+
try {
|
|
22527
|
+
current = readFileSync14(path3, "utf-8");
|
|
22528
|
+
} catch {
|
|
22529
|
+
}
|
|
22530
|
+
if (current === a.model) continue;
|
|
22531
|
+
mkdirSync10(dirname10(path3), { recursive: true });
|
|
22532
|
+
const tmp = `${path3}.${process.pid}.tmp`;
|
|
22533
|
+
writeFileSync10(tmp, a.model);
|
|
22534
|
+
renameSync5(tmp, path3);
|
|
22535
|
+
}
|
|
22536
|
+
}
|
|
22537
|
+
|
|
22134
22538
|
// src/node/entries.ts
|
|
22135
|
-
import { existsSync as
|
|
22136
|
-
import { dirname as
|
|
22539
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
22540
|
+
import { dirname as dirname11, join as join19, resolve as resolve7 } from "node:path";
|
|
22137
22541
|
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
22542
|
function checkoutWorker(dir) {
|
|
22139
22543
|
return {
|
|
22140
|
-
command:
|
|
22141
|
-
args: [
|
|
22544
|
+
command: join19(dir, "node_modules", ".bin", "tsx"),
|
|
22545
|
+
args: [join19(dir, "src", "index.ts")]
|
|
22142
22546
|
};
|
|
22143
22547
|
}
|
|
22144
22548
|
function checkoutMcp(dir, execPath) {
|
|
22145
22549
|
return {
|
|
22146
22550
|
command: execPath,
|
|
22147
22551
|
args: [
|
|
22148
|
-
|
|
22149
|
-
|
|
22552
|
+
join19(dir, "node_modules", "tsx", "dist", "cli.mjs"),
|
|
22553
|
+
join19(dir, "src", "index.ts")
|
|
22150
22554
|
]
|
|
22151
22555
|
};
|
|
22152
22556
|
}
|
|
22153
22557
|
function walkFor(here, packageName, exists) {
|
|
22154
|
-
for (let dir =
|
|
22155
|
-
const candidate =
|
|
22156
|
-
if (exists(
|
|
22558
|
+
for (let dir = dirname11(here); dirname11(dir) !== dir; dir = dirname11(dir)) {
|
|
22559
|
+
const candidate = join19(dir, packageName);
|
|
22560
|
+
if (exists(join19(candidate, "src", "index.ts"))) return candidate;
|
|
22157
22561
|
}
|
|
22158
22562
|
return null;
|
|
22159
22563
|
}
|
|
22160
22564
|
function resolveEventWorker(opts) {
|
|
22161
22565
|
const env2 = opts.env ?? process.env;
|
|
22162
22566
|
const execPath = opts.execPath ?? process.execPath;
|
|
22163
|
-
const exists = opts.exists ??
|
|
22567
|
+
const exists = opts.exists ?? existsSync11;
|
|
22164
22568
|
const override = env2.PITBOSS_EVENT_WORKER;
|
|
22165
22569
|
if (override) {
|
|
22166
|
-
const resolved =
|
|
22570
|
+
const resolved = resolve7(override);
|
|
22167
22571
|
if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
|
|
22168
|
-
if (exists(
|
|
22572
|
+
if (exists(join19(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
|
|
22169
22573
|
throw new Error(MISSING_WORKER);
|
|
22170
22574
|
}
|
|
22171
|
-
const bundled =
|
|
22575
|
+
const bundled = join19(dirname11(opts.here), "event-worker.js");
|
|
22172
22576
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
22173
22577
|
const dir = walkFor(opts.here, "event-worker", exists);
|
|
22174
22578
|
if (dir) return checkoutWorker(dir);
|
|
@@ -22176,8 +22580,8 @@ function resolveEventWorker(opts) {
|
|
|
22176
22580
|
}
|
|
22177
22581
|
function resolveMcpServer(opts) {
|
|
22178
22582
|
const execPath = opts.execPath ?? process.execPath;
|
|
22179
|
-
const exists = opts.exists ??
|
|
22180
|
-
const bundled =
|
|
22583
|
+
const exists = opts.exists ?? existsSync11;
|
|
22584
|
+
const bundled = join19(dirname11(opts.here), "mcp.js");
|
|
22181
22585
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
22182
22586
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
22183
22587
|
if (dir) return checkoutMcp(dir, execPath);
|
|
@@ -22285,176 +22689,41 @@ function loadPruneTargets(names) {
|
|
|
22285
22689
|
});
|
|
22286
22690
|
}
|
|
22287
22691
|
|
|
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"]);
|
|
22692
|
+
// src/node/activity.ts
|
|
22693
|
+
var ACTIVITY_REPORT_MS = 15e3;
|
|
22694
|
+
var REVIEW_HEARTBEAT_MS = 6e4;
|
|
22695
|
+
function deriveActivity({ running, sessionActive, reviewing, waiting }) {
|
|
22696
|
+
if (!running) return "stopped";
|
|
22697
|
+
if (sessionActive) return "working";
|
|
22698
|
+
if (reviewing) return "reviewing";
|
|
22699
|
+
return waiting ? "waiting" : "idle";
|
|
22700
|
+
}
|
|
22701
|
+
function heldAgentIds(tasks) {
|
|
22702
|
+
const reviewing = /* @__PURE__ */ new Set();
|
|
22703
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
22704
|
+
for (const t of tasks) {
|
|
22705
|
+
if (t.assignedTo && (t.status === "blocked" || t.status === "pending_verification")) waiting.add(t.assignedTo);
|
|
22706
|
+
if (t.status === "pending_verification" && t.verifyReviewerId && t.metadata?.reviewStartedBy === t.verifyReviewerId) reviewing.add(t.verifyReviewerId);
|
|
22707
|
+
}
|
|
22708
|
+
return { reviewing, waiting };
|
|
22709
|
+
}
|
|
22710
|
+
function startedReviewTaskIds(tasks) {
|
|
22711
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
22712
|
+
for (const t of tasks) {
|
|
22713
|
+
if (t.status !== "pending_verification" || !t.verifyReviewerId || t.metadata?.reviewStartedBy !== t.verifyReviewerId) continue;
|
|
22714
|
+
byAgent.set(t.verifyReviewerId, [...byAgent.get(t.verifyReviewerId) ?? [], t.id]);
|
|
22715
|
+
}
|
|
22716
|
+
return byAgent;
|
|
22717
|
+
}
|
|
22718
|
+
function reviewerIsLive({ running, sessionActive }) {
|
|
22719
|
+
return running && sessionActive;
|
|
22451
22720
|
}
|
|
22452
22721
|
|
|
22453
22722
|
// src/commands/node.ts
|
|
22454
22723
|
var HERE = fileURLToPath3(import.meta.url);
|
|
22455
22724
|
function inspectProcess(pid) {
|
|
22456
22725
|
try {
|
|
22457
|
-
const out =
|
|
22726
|
+
const out = execFileSync8("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
|
|
22458
22727
|
const match = /^(\d+)\s+(.*)$/.exec(out);
|
|
22459
22728
|
return match ? { pgid: Number(match[1]), command: match[2] } : null;
|
|
22460
22729
|
} catch {
|
|
@@ -22478,7 +22747,9 @@ function childSpec(agent, creds, worker, onDemand = false) {
|
|
|
22478
22747
|
REPO_ID: creds.repoId,
|
|
22479
22748
|
REPO_PATH: agent.worktree,
|
|
22480
22749
|
SPECIALIZATION: agent.specialization,
|
|
22481
|
-
WORKER_RUNTIME: agent.runtime
|
|
22750
|
+
WORKER_RUNTIME: agent.runtime,
|
|
22751
|
+
PITBOSS_ACTIVITY_FILE: activityPath(agent.name),
|
|
22752
|
+
PITBOSS_MODEL_FILE: resolve8(modelPath(agent.name))
|
|
22482
22753
|
}
|
|
22483
22754
|
};
|
|
22484
22755
|
}
|
|
@@ -22494,12 +22765,13 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
22494
22765
|
excludeIfUntracked(root, target);
|
|
22495
22766
|
},
|
|
22496
22767
|
run(bin4, args, cwd) {
|
|
22497
|
-
|
|
22768
|
+
execFileSync8(bin4, args, { cwd, stdio: "ignore" });
|
|
22498
22769
|
}
|
|
22499
22770
|
});
|
|
22500
22771
|
}
|
|
22501
22772
|
function spawnWorker(spec) {
|
|
22502
|
-
|
|
22773
|
+
mkdirSync11(dirname12(spec.logPath), { recursive: true, mode: 448 });
|
|
22774
|
+
rmSync6(activityPath(spec.name), { force: true });
|
|
22503
22775
|
const log = openSync(spec.logPath, "a", 384);
|
|
22504
22776
|
try {
|
|
22505
22777
|
const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
|
|
@@ -22546,6 +22818,63 @@ async function reportScale(creds, event) {
|
|
|
22546
22818
|
});
|
|
22547
22819
|
if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
|
|
22548
22820
|
}
|
|
22821
|
+
async function reportActivity(creds, state) {
|
|
22822
|
+
const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/activity`, {
|
|
22823
|
+
method: "PUT",
|
|
22824
|
+
headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
|
|
22825
|
+
body: JSON.stringify({ state })
|
|
22826
|
+
});
|
|
22827
|
+
if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/activity returned HTTP ${res.status}`);
|
|
22828
|
+
}
|
|
22829
|
+
function startActivityLoop(agents, sup) {
|
|
22830
|
+
let busy = false;
|
|
22831
|
+
const sent = /* @__PURE__ */ new Map();
|
|
22832
|
+
const beatAt = /* @__PURE__ */ new Map();
|
|
22833
|
+
const heartbeatReviews = async (creds, name, live, taskIds) => {
|
|
22834
|
+
if (!live || taskIds.length === 0 || Date.now() - (beatAt.get(name) ?? 0) < REVIEW_HEARTBEAT_MS) return;
|
|
22835
|
+
beatAt.set(name, Date.now());
|
|
22836
|
+
for (const id of taskIds) {
|
|
22837
|
+
const res = await fetch(`${creds.api}/tasks/${encodeURIComponent(id)}/review/start`, {
|
|
22838
|
+
method: "POST",
|
|
22839
|
+
headers: { Authorization: `Bearer ${creds.token}` }
|
|
22840
|
+
});
|
|
22841
|
+
if (!res.ok) throw new Error(`POST /tasks/${id}/review/start returned HTTP ${res.status}`);
|
|
22842
|
+
}
|
|
22843
|
+
};
|
|
22844
|
+
const tick = async () => {
|
|
22845
|
+
if (busy) return;
|
|
22846
|
+
busy = true;
|
|
22847
|
+
try {
|
|
22848
|
+
const reader = agents[0].creds;
|
|
22849
|
+
const held = await fetchTasks(reader, "status=blocked,pending_verification");
|
|
22850
|
+
const { reviewing, waiting } = heldAgentIds(held);
|
|
22851
|
+
const reviewIds = startedReviewTaskIds(held);
|
|
22852
|
+
for (const { name, creds } of agents) {
|
|
22853
|
+
const signals2 = {
|
|
22854
|
+
running: sup.isActive(name),
|
|
22855
|
+
sessionActive: existsSync12(activityPath(name))
|
|
22856
|
+
};
|
|
22857
|
+
const live = reviewerIsLive(signals2);
|
|
22858
|
+
const state = deriveActivity({
|
|
22859
|
+
...signals2,
|
|
22860
|
+
reviewing: reviewing.has(creds.agentId),
|
|
22861
|
+
waiting: waiting.has(creds.agentId)
|
|
22862
|
+
});
|
|
22863
|
+
await heartbeatReviews(creds, name, live, reviewIds.get(creds.agentId) ?? []);
|
|
22864
|
+
if (sent.get(name) === state) continue;
|
|
22865
|
+
await reportActivity(creds, state);
|
|
22866
|
+
sent.set(name, state);
|
|
22867
|
+
}
|
|
22868
|
+
} catch (err) {
|
|
22869
|
+
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} activity: ${err instanceof Error ? err.message : String(err)}`);
|
|
22870
|
+
} finally {
|
|
22871
|
+
busy = false;
|
|
22872
|
+
}
|
|
22873
|
+
};
|
|
22874
|
+
void tick();
|
|
22875
|
+
const timer = setInterval(() => void tick(), ACTIVITY_REPORT_MS);
|
|
22876
|
+
return () => clearInterval(timer);
|
|
22877
|
+
}
|
|
22549
22878
|
function startPoolLoop(run3, sup, record) {
|
|
22550
22879
|
const { pool, members } = run3;
|
|
22551
22880
|
const { reconcileMs, idleStopMs } = poolTimings();
|
|
@@ -22631,19 +22960,20 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22631
22960
|
const timer = setInterval(() => void tick(), reconcileMs);
|
|
22632
22961
|
return () => clearInterval(timer);
|
|
22633
22962
|
}
|
|
22963
|
+
var MODEL_REFRESH_MS = 15e3;
|
|
22634
22964
|
async function nodeUpCommand(specFile, opts = {}) {
|
|
22635
22965
|
try {
|
|
22636
|
-
const specPath =
|
|
22637
|
-
const specRealpath =
|
|
22638
|
-
const parsed = parseSpec(
|
|
22966
|
+
const specPath = resolve8(specFile);
|
|
22967
|
+
const specRealpath = realpathSync6.native(specPath);
|
|
22968
|
+
const parsed = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
|
|
22639
22969
|
const previous = readState();
|
|
22640
22970
|
if (previous && previous.supervisorPid !== process.pid && isAlive(previous.supervisorPid)) {
|
|
22641
22971
|
throw new Error(`a node is already running (pid ${previous.supervisorPid}, ${previous.specPath}). Stop it first.`);
|
|
22642
22972
|
}
|
|
22643
22973
|
for (const a of parsed.agents) {
|
|
22644
|
-
if (!
|
|
22974
|
+
if (!existsSync12(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
|
|
22645
22975
|
}
|
|
22646
|
-
const plain = parsed.agents.map((a) => ({ ...a, worktree:
|
|
22976
|
+
const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync6.native(a.worktree) }));
|
|
22647
22977
|
const pools = parsed.pools.map((pool) => {
|
|
22648
22978
|
const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
|
|
22649
22979
|
return { pool, members: poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] })) };
|
|
@@ -22673,6 +23003,20 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22673
23003
|
const inviteCodes = collectInvites(spec.agents.map((a) => a.name), opts.invite ?? [], process.env);
|
|
22674
23004
|
const creds = await ensureIdentities(spec, inviteCodes, specRealpath);
|
|
22675
23005
|
const runnable = spec.agents.filter((a) => creds.has(a.name));
|
|
23006
|
+
writeModelFiles(spec.agents);
|
|
23007
|
+
let modelError = "";
|
|
23008
|
+
const refreshModels = setInterval(() => {
|
|
23009
|
+
try {
|
|
23010
|
+
const fresh = new Map(allAgents(parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath))).map((a) => [a.name, a.model]));
|
|
23011
|
+
writeModelFiles(spec.agents.map((a) => ({ name: a.name, model: fresh.get(a.name) })));
|
|
23012
|
+
modelError = "";
|
|
23013
|
+
} catch (err) {
|
|
23014
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23015
|
+
if (message !== modelError) console.log(source_default.yellow(` ! spec not re-read for models: ${message}`));
|
|
23016
|
+
modelError = message;
|
|
23017
|
+
}
|
|
23018
|
+
}, MODEL_REFRESH_MS);
|
|
23019
|
+
refreshModels.unref();
|
|
22676
23020
|
if (runnable.length === 0) throw new Error("no agent has credentials, nothing to start");
|
|
22677
23021
|
const mcp = resolveMcpServer({ here: HERE });
|
|
22678
23022
|
for (const a of runnable) prepareAgent(a, creds.get(a.name), mcp);
|
|
@@ -22711,6 +23055,12 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22711
23055
|
process.on("SIGTERM", shutdown);
|
|
22712
23056
|
process.on("SIGHUP", shutdown);
|
|
22713
23057
|
sup.start();
|
|
23058
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
23059
|
+
for (const a of runnable) {
|
|
23060
|
+
const c = creds.get(a.name);
|
|
23061
|
+
byRepo.set(c.repoId, [...byRepo.get(c.repoId) ?? [], { name: a.name, creds: c }]);
|
|
23062
|
+
}
|
|
23063
|
+
for (const group of byRepo.values()) stopLoops.push(startActivityLoop(group, sup));
|
|
22714
23064
|
for (const { pool, members } of pools) {
|
|
22715
23065
|
const ready = members.filter((m) => creds.has(m.name)).map((agent) => ({ agent, creds: creds.get(agent.name) }));
|
|
22716
23066
|
if (ready.length === 0) {
|
|
@@ -22731,9 +23081,9 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22731
23081
|
}
|
|
22732
23082
|
async function nodePruneCommand(specFile, opts = {}) {
|
|
22733
23083
|
try {
|
|
22734
|
-
const specPath =
|
|
22735
|
-
const specRealpath =
|
|
22736
|
-
const spec = parseSpec(
|
|
23084
|
+
const specPath = resolve8(specFile);
|
|
23085
|
+
const specRealpath = realpathSync6.native(specPath);
|
|
23086
|
+
const spec = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
|
|
22737
23087
|
const { orphans: names, unattributed } = classifyStored(allAgents(spec).map((a) => a.name), specRealpath);
|
|
22738
23088
|
if (names.length === 0) {
|
|
22739
23089
|
console.log("No stored credentials are missing from the spec.");
|
|
@@ -22839,6 +23189,7 @@ var token = program2.command("token").description("Manage your agent's API token
|
|
|
22839
23189
|
token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
|
|
22840
23190
|
token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
|
|
22841
23191
|
token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
|
|
23192
|
+
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
23193
|
var node = program2.command("node").description("Run this machine's agents headless and keep them running");
|
|
22843
23194
|
node.command("up <spec>").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them)").option(
|
|
22844
23195
|
"--invite <name=code>",
|