@pitboss/cli 0.4.7 → 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 +656 -241
- package/dist/event-worker.js +107 -11
- package/dist/mcp.js +108 -18
- 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";
|
|
@@ -20643,6 +20646,14 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
|
|
|
20643
20646
|
human. If you need the answer before you can continue, block the task with
|
|
20644
20647
|
\`blockedThreadId\` set to the thread the DM returned; you will be resumed when they answer.
|
|
20645
20648
|
|
|
20649
|
+
**A blocker travels up the chain.** When another agent's task blocks, the repo's orchestrator
|
|
20650
|
+
and the planner of that task's goal are told, as well as the owner. If that is you, read the
|
|
20651
|
+
question on its \`blockedThreadId\`. When the plan, the goal, its issue or its PR already answers
|
|
20652
|
+
it, reply there with the answer and where it comes from. As the planner, if the block shows the
|
|
20653
|
+
plan is wrong (a missing dependency, a task too big), fix the plan. Never answer for the owner on
|
|
20654
|
+
deletes, scope changes, approvals or publishing; those wait for the owner or their delegate.
|
|
20655
|
+
Otherwise do nothing: the owner has already been told.
|
|
20656
|
+
|
|
20646
20657
|
2. Call \`mcp__relai__session_start\` for orientation. It is an **index, not an archive**: lists are
|
|
20647
20658
|
capped and long bodies are clipped. Anything marked \`truncated: true\`, and anything named under
|
|
20648
20659
|
\`notShown\`, is a pointer: fetch the real thing with the tool it names before you rely on it,
|
|
@@ -20659,7 +20670,8 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
|
|
|
20659
20670
|
a one-line status on the task thread naming what you are checking) when you begin, then
|
|
20660
20671
|
run the tests, compare the diff against the spec the task names, then
|
|
20661
20672
|
\`mcp__relai__submit_review\` \u2014 \`approve\`, or \`reject\` with a specific note the author
|
|
20662
|
-
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.
|
|
20663
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.
|
|
20664
20676
|
|
|
20665
20677
|
4. Call \`mcp__relai__get_my_tasks\` \u2014 if it returns no tasks, stop immediately.
|
|
@@ -20901,7 +20913,11 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20901
20913
|
\`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
|
|
20902
20914
|
the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
|
|
20903
20915
|
\`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
|
|
20904
|
-
|
|
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
|
|
20905
20921
|
and independently testable so several agents can work at once. Use \`blockedBy\` only where
|
|
20906
20922
|
one task truly needs another's result. Each description is self-contained: what to change,
|
|
20907
20923
|
which files, how to test it, and the acceptance criteria.
|
|
@@ -20921,6 +20937,62 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20921
20937
|
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
20922
20938
|
the planning task's thread and complete it.
|
|
20923
20939
|
|
|
20940
|
+
**Review goals** (the order's \`kind\` is \`"review"\`; the planning task carries
|
|
20941
|
+
\`metadata.orderKind: "review"\`): the goal is one review posted on the existing pull request at the
|
|
20942
|
+
order's \`prUrl\`. There is no order branch, no draft PR and no merge, so skip steps 2 to 6 above.
|
|
20943
|
+
Nobody pushes to the PR's branch, approves it, or requests changes on it.
|
|
20944
|
+
1. As the planner, read the PR with \`gh pr view <prUrl>\` and \`gh pr diff <prUrl>\`. Pick the
|
|
20945
|
+
angles it needs: correctness, security, simplicity, tests and consistency with the codebase for a
|
|
20946
|
+
real change; correctness and consistency alone for a small, low-risk one (docs, tests, under about
|
|
20947
|
+
150 changed lines with no auth, input handling, secrets, money or migrations).
|
|
20948
|
+
2. One task per angle, \`epicId\` = the order's \`epicId\`,
|
|
20949
|
+
\`metadata: { orderId, orderRole: "review", angle: "<angle>", prUrl }\`. Spread the angles
|
|
20950
|
+
across vendors: assign each to a different agent from \`mcp__relai__list_agents\`, preferring a
|
|
20951
|
+
different \`workerType\` from the agents already given one, and reuse agents only when there are
|
|
20952
|
+
more angles than agents. Each description names the angle, the PR, what that angle hunts for, and
|
|
20953
|
+
the reporting format below.
|
|
20954
|
+
3. One lead task, \`blockedBy\` every angle task, assigned to yourself, with
|
|
20955
|
+
\`metadata: { orderId, orderRole: "review_lead", prUrl }\`.
|
|
20956
|
+
4. When acceptance includes \`human_approval\`, add a gate task blocked by the lead
|
|
20957
|
+
(\`metadata: { orderId, orderRole: "gate" }\`, \`verifyKind: "human_approval"\`) and a post task
|
|
20958
|
+
blocked by the gate (\`metadata: { orderId, orderRole: "post_review", prUrl }\`). Without it,
|
|
20959
|
+
the lead posts the review itself.
|
|
20960
|
+
5. If the order has \`relatedRepos\`, copy them into every angle and lead task's metadata and name them in
|
|
20961
|
+
each description, so reviewers check the contracts this PR depends on in those repos.
|
|
20962
|
+
6. Post the task list on the planning task's thread and complete it.
|
|
20963
|
+
|
|
20964
|
+
**orderRole "review"** (one angle of a review goal):
|
|
20965
|
+
- Read the diff and as much surrounding code as you need. To run anything, check out the PR head
|
|
20966
|
+
detached: \`git fetch origin pull/<n>/head && git checkout --detach FETCH_HEAD\`. Never push, never
|
|
20967
|
+
comment on the PR, never edit the repo's history.
|
|
20968
|
+
- Verify by running code where you can. Report your 3 to 5 best-evidenced findings for your angle, most
|
|
20969
|
+
severe first, each with severity (blocker, high, medium, low), file:line, and a concrete failing
|
|
20970
|
+
input or scenario. "No findings" is a valid report. A missing code comment is not a finding.
|
|
20971
|
+
- When the task names \`relatedRepos\`, read them for any contract this change depends on (an endpoint it
|
|
20972
|
+
calls, a helper it imports, a config value it reads). Read only, outside your worktree: a shallow
|
|
20973
|
+
clone into a temp directory (\`gh repo clone <url> <tmp dir> -- --depth 1\`) or one file at a time with
|
|
20974
|
+
\`gh api repos/<owner>/<repo>/contents/<path>\`. Never push, branch, comment or open anything there.
|
|
20975
|
+
A finding that rests on a related repo cites the file and line there. If you could not read the
|
|
20976
|
+
repo, mark that finding UNPROVEN and say why rather than guessing what the other side does.
|
|
20977
|
+
- Post the report with \`mcp__relai__add_task_comment\` on this task, then complete it.
|
|
20978
|
+
|
|
20979
|
+
**orderRole "review_lead"**:
|
|
20980
|
+
- Read every angle task's report with \`mcp__relai__get_task_comments\`. Merge duplicates, drop
|
|
20981
|
+
findings you cannot reproduce or whose premise is wrong, and rank what is left. Keep an UNPROVEN mark
|
|
20982
|
+
on any finding about a related repo nobody could read. Severity sets the
|
|
20983
|
+
verdict: any blocker or high means changes are needed; otherwise it is safe to merge with the
|
|
20984
|
+
noted follow-ups.
|
|
20985
|
+
- Write one review: the verdict in the first line, then the findings, each with file:line, the
|
|
20986
|
+
failure and the fix you would ask for, then which angles ran, each with its agent's name and vendor
|
|
20987
|
+
(\`workerType\` from \`mcp__relai__list_agents\`), and which related repos were read. Keep it short.
|
|
20988
|
+
- Without a \`human_approval\` gate, post it with \`gh pr review <prUrl> --comment --body-file <file>\`
|
|
20989
|
+
and put the review URL on the task's thread. With a gate, post the review as a comment on this task
|
|
20990
|
+
for the owner to approve, and do not touch the PR. Then complete.
|
|
20991
|
+
|
|
20992
|
+
**orderRole "post_review"**: once the gate is approved, post the lead's review comment from the
|
|
20993
|
+
lead task's thread with \`gh pr review <prUrl> --comment --body-file <file>\`, put the review URL on
|
|
20994
|
+
this task's thread, and complete.
|
|
20995
|
+
|
|
20924
20996
|
**orderRole "plan_review"** (assigned to the planner, reviewed by another agent):
|
|
20925
20997
|
- After the task graph is written, complete each plan review task. That hands the plan to its
|
|
20926
20998
|
reviewer. Every build task waits until all plan reviews are approved.
|
|
@@ -21106,6 +21178,18 @@ async function recordSessionUsage(conn, startIds, usage, logPrefix) {
|
|
|
21106
21178
|
await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
|
|
21107
21179
|
}
|
|
21108
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
|
+
|
|
21109
21193
|
// ../claude-worker/src/session.ts
|
|
21110
21194
|
function mcpServerEntry() {
|
|
21111
21195
|
return fileURLToPath2(new URL("../../mcp-server/src/index.ts", import.meta.url));
|
|
@@ -21143,7 +21227,7 @@ async function runClaudeSession(config) {
|
|
|
21143
21227
|
const mcpConfigPath = writeMcpConfig(config);
|
|
21144
21228
|
const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
|
|
21145
21229
|
try {
|
|
21146
|
-
return await new Promise((
|
|
21230
|
+
return await new Promise((resolve9, reject) => {
|
|
21147
21231
|
const args = [
|
|
21148
21232
|
"--print",
|
|
21149
21233
|
"--mcp-config",
|
|
@@ -21154,7 +21238,7 @@ async function runClaudeSession(config) {
|
|
|
21154
21238
|
"stream-json",
|
|
21155
21239
|
"--verbose",
|
|
21156
21240
|
"--model",
|
|
21157
|
-
config.model,
|
|
21241
|
+
pinnedModel() ?? config.model,
|
|
21158
21242
|
"--add-dir",
|
|
21159
21243
|
config.repoPath
|
|
21160
21244
|
];
|
|
@@ -21223,7 +21307,7 @@ async function runClaudeSession(config) {
|
|
|
21223
21307
|
const detail = (resultError || stderrOutput || "").trim();
|
|
21224
21308
|
reject(new Error(`claude exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21225
21309
|
} else {
|
|
21226
|
-
|
|
21310
|
+
resolve9(unique);
|
|
21227
21311
|
}
|
|
21228
21312
|
};
|
|
21229
21313
|
finish.then(settle, settle);
|
|
@@ -21275,8 +21359,8 @@ function classifySessionError(text) {
|
|
|
21275
21359
|
|
|
21276
21360
|
// ../claude-worker/src/adapters/cursor.ts
|
|
21277
21361
|
var bin = () => process.env.CURSOR_BIN ?? "cursor-agent";
|
|
21278
|
-
function cursorArgs(repoPath, prompt) {
|
|
21279
|
-
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];
|
|
21280
21364
|
}
|
|
21281
21365
|
function readCursorEvent(line) {
|
|
21282
21366
|
let event;
|
|
@@ -21300,10 +21384,10 @@ function readCursorEvent(line) {
|
|
|
21300
21384
|
return {};
|
|
21301
21385
|
}
|
|
21302
21386
|
async function runCursorSession(config) {
|
|
21303
|
-
const args = cursorArgs(config.repoPath, buildPrompt(config));
|
|
21387
|
+
const args = cursorArgs(config.repoPath, buildPrompt(config), pinnedModel());
|
|
21304
21388
|
console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
|
|
21305
21389
|
const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
|
|
21306
|
-
return new Promise((
|
|
21390
|
+
return new Promise((resolve9, reject) => {
|
|
21307
21391
|
const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21308
21392
|
console.log(`[cursor-worker] Process PID: ${proc.pid}`);
|
|
21309
21393
|
const toolsUsed = [];
|
|
@@ -21342,7 +21426,7 @@ async function runCursorSession(config) {
|
|
|
21342
21426
|
const detail = (resultError || stderrOutput).trim();
|
|
21343
21427
|
reject(new Error(`cursor-agent exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21344
21428
|
} else {
|
|
21345
|
-
|
|
21429
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21346
21430
|
}
|
|
21347
21431
|
};
|
|
21348
21432
|
finish.then(settle, settle);
|
|
@@ -21381,10 +21465,11 @@ import { unlinkSync as unlinkSync2 } from "fs";
|
|
|
21381
21465
|
var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
|
|
21382
21466
|
var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
|
|
21383
21467
|
var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
|
|
21384
|
-
function copilotArgs(repoPath, prompt, mcpConfigPath) {
|
|
21468
|
+
function copilotArgs(repoPath, prompt, mcpConfigPath, model) {
|
|
21385
21469
|
return [
|
|
21386
21470
|
"-p",
|
|
21387
21471
|
prompt,
|
|
21472
|
+
...model ? ["--model", model] : [],
|
|
21388
21473
|
"--allow-all-tools",
|
|
21389
21474
|
"--output-format",
|
|
21390
21475
|
"json",
|
|
@@ -21414,9 +21499,9 @@ function readCopilotEvent(line) {
|
|
|
21414
21499
|
}
|
|
21415
21500
|
async function runCopilotSession(config) {
|
|
21416
21501
|
const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
|
|
21417
|
-
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
|
|
21502
|
+
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath, pinnedModel());
|
|
21418
21503
|
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
21419
|
-
return new Promise((
|
|
21504
|
+
return new Promise((resolve9, reject) => {
|
|
21420
21505
|
const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21421
21506
|
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
21422
21507
|
const toolsUsed = [];
|
|
@@ -21453,7 +21538,7 @@ async function runCopilotSession(config) {
|
|
|
21453
21538
|
const detail = (sessionError || stderrOutput).trim();
|
|
21454
21539
|
reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21455
21540
|
} else {
|
|
21456
|
-
|
|
21541
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21457
21542
|
}
|
|
21458
21543
|
});
|
|
21459
21544
|
proc.on("error", (err) => {
|
|
@@ -21487,7 +21572,7 @@ var bin3 = () => process.env.CODEX_BIN ?? "codex";
|
|
|
21487
21572
|
var codexHome = () => process.env.CODEX_HOME ?? join14(homedir5(), ".codex");
|
|
21488
21573
|
var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
|
|
21489
21574
|
var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
|
|
21490
|
-
function codexArgs(repoPath, prompt, profile) {
|
|
21575
|
+
function codexArgs(repoPath, prompt, profile, model) {
|
|
21491
21576
|
return [
|
|
21492
21577
|
"exec",
|
|
21493
21578
|
"--json",
|
|
@@ -21498,6 +21583,7 @@ function codexArgs(repoPath, prompt, profile) {
|
|
|
21498
21583
|
repoPath,
|
|
21499
21584
|
"-p",
|
|
21500
21585
|
profile,
|
|
21586
|
+
...model ? ["-m", model] : [],
|
|
21501
21587
|
prompt
|
|
21502
21588
|
];
|
|
21503
21589
|
}
|
|
@@ -21546,9 +21632,9 @@ async function runCodexSession(config) {
|
|
|
21546
21632
|
} catch {
|
|
21547
21633
|
}
|
|
21548
21634
|
};
|
|
21549
|
-
const args = codexArgs(config.repoPath, buildPrompt(config), profile);
|
|
21635
|
+
const args = codexArgs(config.repoPath, buildPrompt(config), profile, pinnedModel());
|
|
21550
21636
|
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
21551
|
-
return new Promise((
|
|
21637
|
+
return new Promise((resolve9, reject) => {
|
|
21552
21638
|
const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21553
21639
|
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
21554
21640
|
const toolsUsed = [];
|
|
@@ -21582,7 +21668,7 @@ async function runCodexSession(config) {
|
|
|
21582
21668
|
const detail = (sessionError || stderrOutput).trim();
|
|
21583
21669
|
reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21584
21670
|
} else {
|
|
21585
|
-
|
|
21671
|
+
resolve9([...new Set(toolsUsed)]);
|
|
21586
21672
|
}
|
|
21587
21673
|
});
|
|
21588
21674
|
proc.on("error", (err) => {
|
|
@@ -21619,7 +21705,65 @@ var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
|
|
|
21619
21705
|
// ../claude-worker/src/config.ts
|
|
21620
21706
|
var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
|
|
21621
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
|
+
|
|
21622
21765
|
// src/node/spec.ts
|
|
21766
|
+
import { join as join16, resolve as resolve5, sep as sep2 } from "node:path";
|
|
21623
21767
|
var DEFAULT_API2 = "https://api.pitboss.dev";
|
|
21624
21768
|
var MAX_POOL_SIZE = 32;
|
|
21625
21769
|
var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
@@ -21631,7 +21775,14 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
|
|
|
21631
21775
|
function poolMembers(pool) {
|
|
21632
21776
|
return Array.from({ length: pool.max }, (_, i) => {
|
|
21633
21777
|
const name = `${pool.pool}-${i + 1}`;
|
|
21634
|
-
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
|
+
};
|
|
21635
21786
|
});
|
|
21636
21787
|
}
|
|
21637
21788
|
var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
|
|
@@ -21648,6 +21799,18 @@ function specializationOf(a, i) {
|
|
|
21648
21799
|
}
|
|
21649
21800
|
return specialization;
|
|
21650
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
|
+
}
|
|
21651
21814
|
function parsePool(a, i, specDir) {
|
|
21652
21815
|
if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
|
|
21653
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)`);
|
|
@@ -21663,10 +21826,10 @@ function parsePool(a, i, specDir) {
|
|
|
21663
21826
|
if (typeof min !== "number" || !Number.isInteger(min) || min < 0 || min > max) {
|
|
21664
21827
|
throw new Error(`agents[${i}].min must be an integer from 0 to max`);
|
|
21665
21828
|
}
|
|
21666
|
-
const base2 =
|
|
21667
|
-
const worktreeRoot =
|
|
21829
|
+
const base2 = resolve5(specDir, a.base);
|
|
21830
|
+
const worktreeRoot = resolve5(specDir, a.worktreeRoot);
|
|
21668
21831
|
if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
|
|
21669
|
-
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 };
|
|
21670
21833
|
}
|
|
21671
21834
|
function parseSpec(text, specDir) {
|
|
21672
21835
|
let raw;
|
|
@@ -21692,7 +21855,8 @@ function parseSpec(text, specDir) {
|
|
|
21692
21855
|
if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
|
|
21693
21856
|
if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
|
|
21694
21857
|
const runtime = runtimeOf(a, i);
|
|
21695
|
-
|
|
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) });
|
|
21696
21860
|
});
|
|
21697
21861
|
for (const p of pools) {
|
|
21698
21862
|
for (const a of agents) {
|
|
@@ -21719,13 +21883,15 @@ function parseSpec(text, specDir) {
|
|
|
21719
21883
|
}
|
|
21720
21884
|
|
|
21721
21885
|
// src/node/store.ts
|
|
21722
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as
|
|
21723
|
-
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";
|
|
21724
21888
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
21725
|
-
var nodeDir = () =>
|
|
21726
|
-
var credsPath = (name) =>
|
|
21727
|
-
var statePath2 = () =>
|
|
21728
|
-
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);
|
|
21729
21895
|
function writePrivate(path3, value) {
|
|
21730
21896
|
mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
|
|
21731
21897
|
const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
|
|
@@ -21734,7 +21900,7 @@ function writePrivate(path3, value) {
|
|
|
21734
21900
|
}
|
|
21735
21901
|
function readJson(path3) {
|
|
21736
21902
|
if (!existsSync8(path3)) return null;
|
|
21737
|
-
return JSON.parse(
|
|
21903
|
+
return JSON.parse(readFileSync12(path3, "utf-8"));
|
|
21738
21904
|
}
|
|
21739
21905
|
var readCreds = (name) => readJson(credsPath(name));
|
|
21740
21906
|
var writeCreds = (name, creds) => writePrivate(credsPath(name), creds);
|
|
@@ -21743,14 +21909,187 @@ var removeCreds = (name) => {
|
|
|
21743
21909
|
if (existsSync8(path3)) rmSync4(path3);
|
|
21744
21910
|
};
|
|
21745
21911
|
var listStoredAgentNames = () => {
|
|
21746
|
-
const dir =
|
|
21912
|
+
const dir = join17(nodeDir(), "agents");
|
|
21747
21913
|
if (!existsSync8(dir)) return [];
|
|
21748
21914
|
return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
21749
21915
|
};
|
|
21750
21916
|
var readState = () => readJson(statePath2());
|
|
21751
21917
|
var writeState = (state) => writePrivate(statePath2(), state);
|
|
21752
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
|
+
|
|
21753
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
|
+
}
|
|
21754
22093
|
function collectInvites(agentNames, flags, env2) {
|
|
21755
22094
|
const byLower = new Map(agentNames.map((n) => [n.toLowerCase(), n]));
|
|
21756
22095
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -21787,6 +22126,7 @@ function matchInvites(agents, invites) {
|
|
|
21787
22126
|
}
|
|
21788
22127
|
return { matched, ungranted, extra: left, refused };
|
|
21789
22128
|
}
|
|
22129
|
+
var MAX_REQUESTED = 32;
|
|
21790
22130
|
function groupByRepo(agents, originOf2) {
|
|
21791
22131
|
const groups = /* @__PURE__ */ new Map();
|
|
21792
22132
|
for (const agent of agents) {
|
|
@@ -21794,7 +22134,10 @@ function groupByRepo(agents, originOf2) {
|
|
|
21794
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`);
|
|
21795
22135
|
groups.set(remote, [...groups.get(remote) ?? [], agent]);
|
|
21796
22136
|
}
|
|
21797
|
-
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
|
+
})));
|
|
21798
22141
|
}
|
|
21799
22142
|
var originOf = (worktree) => git(["remote", "get-url", "origin"], worktree);
|
|
21800
22143
|
async function abandonMinted(spec, agent, minted, reason) {
|
|
@@ -21900,10 +22243,12 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
21900
22243
|
console.log(source_default.bold(`
|
|
21901
22244
|
Approve ${repoName}`));
|
|
21902
22245
|
console.log(` On the approval screen add these agents as workers:`);
|
|
21903
|
-
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
|
+
}
|
|
21904
22249
|
let grant;
|
|
21905
22250
|
try {
|
|
21906
|
-
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");
|
|
21907
22252
|
} catch (err) {
|
|
21908
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.`));
|
|
21909
22254
|
continue;
|
|
@@ -21943,6 +22288,106 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
21943
22288
|
return have;
|
|
21944
22289
|
}
|
|
21945
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
|
+
|
|
21946
22391
|
// src/node/supervisor.ts
|
|
21947
22392
|
function nextDelay(consecutiveFailures, { baseMs, maxMs }) {
|
|
21948
22393
|
return Math.min(baseMs * 2 ** Math.max(0, consecutiveFailures - 1), maxMs);
|
|
@@ -22067,44 +22512,67 @@ var Supervisor = class {
|
|
|
22067
22512
|
}
|
|
22068
22513
|
};
|
|
22069
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
|
+
|
|
22070
22538
|
// src/node/entries.ts
|
|
22071
|
-
import { existsSync as
|
|
22072
|
-
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";
|
|
22073
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.";
|
|
22074
22542
|
function checkoutWorker(dir) {
|
|
22075
22543
|
return {
|
|
22076
|
-
command:
|
|
22077
|
-
args: [
|
|
22544
|
+
command: join19(dir, "node_modules", ".bin", "tsx"),
|
|
22545
|
+
args: [join19(dir, "src", "index.ts")]
|
|
22078
22546
|
};
|
|
22079
22547
|
}
|
|
22080
22548
|
function checkoutMcp(dir, execPath) {
|
|
22081
22549
|
return {
|
|
22082
22550
|
command: execPath,
|
|
22083
22551
|
args: [
|
|
22084
|
-
|
|
22085
|
-
|
|
22552
|
+
join19(dir, "node_modules", "tsx", "dist", "cli.mjs"),
|
|
22553
|
+
join19(dir, "src", "index.ts")
|
|
22086
22554
|
]
|
|
22087
22555
|
};
|
|
22088
22556
|
}
|
|
22089
22557
|
function walkFor(here, packageName, exists) {
|
|
22090
|
-
for (let dir =
|
|
22091
|
-
const candidate =
|
|
22092
|
-
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;
|
|
22093
22561
|
}
|
|
22094
22562
|
return null;
|
|
22095
22563
|
}
|
|
22096
22564
|
function resolveEventWorker(opts) {
|
|
22097
22565
|
const env2 = opts.env ?? process.env;
|
|
22098
22566
|
const execPath = opts.execPath ?? process.execPath;
|
|
22099
|
-
const exists = opts.exists ??
|
|
22567
|
+
const exists = opts.exists ?? existsSync11;
|
|
22100
22568
|
const override = env2.PITBOSS_EVENT_WORKER;
|
|
22101
22569
|
if (override) {
|
|
22102
|
-
const resolved =
|
|
22570
|
+
const resolved = resolve7(override);
|
|
22103
22571
|
if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
|
|
22104
|
-
if (exists(
|
|
22572
|
+
if (exists(join19(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
|
|
22105
22573
|
throw new Error(MISSING_WORKER);
|
|
22106
22574
|
}
|
|
22107
|
-
const bundled =
|
|
22575
|
+
const bundled = join19(dirname11(opts.here), "event-worker.js");
|
|
22108
22576
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
22109
22577
|
const dir = walkFor(opts.here, "event-worker", exists);
|
|
22110
22578
|
if (dir) return checkoutWorker(dir);
|
|
@@ -22112,8 +22580,8 @@ function resolveEventWorker(opts) {
|
|
|
22112
22580
|
}
|
|
22113
22581
|
function resolveMcpServer(opts) {
|
|
22114
22582
|
const execPath = opts.execPath ?? process.execPath;
|
|
22115
|
-
const exists = opts.exists ??
|
|
22116
|
-
const bundled =
|
|
22583
|
+
const exists = opts.exists ?? existsSync11;
|
|
22584
|
+
const bundled = join19(dirname11(opts.here), "mcp.js");
|
|
22117
22585
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
22118
22586
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
22119
22587
|
if (dir) return checkoutMcp(dir, execPath);
|
|
@@ -22221,176 +22689,41 @@ function loadPruneTargets(names) {
|
|
|
22221
22689
|
});
|
|
22222
22690
|
}
|
|
22223
22691
|
|
|
22224
|
-
// src/node/
|
|
22225
|
-
|
|
22226
|
-
|
|
22227
|
-
|
|
22228
|
-
|
|
22229
|
-
|
|
22230
|
-
|
|
22231
|
-
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
};
|
|
22241
|
-
}
|
|
22242
|
-
function
|
|
22243
|
-
|
|
22244
|
-
const
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
22251
|
-
|
|
22252
|
-
const start = [];
|
|
22253
|
-
const take = (m) => {
|
|
22254
|
-
start.push(m.name);
|
|
22255
|
-
count++;
|
|
22256
|
-
};
|
|
22257
|
-
for (const m of stopped) {
|
|
22258
|
-
if (count >= max) break;
|
|
22259
|
-
if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
|
|
22260
|
-
}
|
|
22261
|
-
const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
|
|
22262
|
-
let demand = pending - running.filter(isIdle).length;
|
|
22263
|
-
for (const m of spare()) {
|
|
22264
|
-
if (demand <= 0 || count >= max) break;
|
|
22265
|
-
take(m);
|
|
22266
|
-
demand--;
|
|
22267
|
-
}
|
|
22268
|
-
for (const m of spare()) {
|
|
22269
|
-
if (count >= min || count >= max) break;
|
|
22270
|
-
take(m);
|
|
22271
|
-
}
|
|
22272
|
-
if (start.length > 0) return { start, stop: [] };
|
|
22273
|
-
const stop = [];
|
|
22274
|
-
let surplus = running.filter(isIdle).length - pending;
|
|
22275
|
-
const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
|
|
22276
|
-
for (const m of eligible) {
|
|
22277
|
-
if (surplus <= 0 || count <= min) break;
|
|
22278
|
-
stop.push(m.name);
|
|
22279
|
-
count--;
|
|
22280
|
-
surplus--;
|
|
22281
|
-
}
|
|
22282
|
-
return { start, stop };
|
|
22283
|
-
}
|
|
22284
|
-
function summarizeTasks(ready, held, members) {
|
|
22285
|
-
const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
|
|
22286
|
-
const counts = Object.fromEntries(
|
|
22287
|
-
members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
|
|
22288
|
-
);
|
|
22289
|
-
const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
|
|
22290
|
-
let readyAssigned = 0;
|
|
22291
|
-
for (const t of ready) {
|
|
22292
|
-
const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22293
|
-
if (!name) continue;
|
|
22294
|
-
counts[name].readyAssigned++;
|
|
22295
|
-
readyAssigned++;
|
|
22296
|
-
}
|
|
22297
|
-
for (const t of held) {
|
|
22298
|
-
const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22299
|
-
if (!name) continue;
|
|
22300
|
-
if (t.status === "assigned") counts[name].assigned++;
|
|
22301
|
-
if (t.status === "in_progress") counts[name].inProgress++;
|
|
22302
|
-
}
|
|
22303
|
-
return { pending, ready: pending + readyAssigned, members: counts };
|
|
22304
|
-
}
|
|
22305
|
-
function poolRootProblem(root, protectedPaths) {
|
|
22306
|
-
for (const p of protectedPaths) {
|
|
22307
|
-
if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
|
|
22308
|
-
}
|
|
22309
|
-
return null;
|
|
22310
|
-
}
|
|
22311
|
-
function memberPathProblem(s) {
|
|
22312
|
-
if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
|
|
22313
|
-
if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
|
|
22314
|
-
if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
|
|
22315
|
-
return null;
|
|
22316
|
-
}
|
|
22317
|
-
function realpathAllowMissing(path3) {
|
|
22318
|
-
const abs = resolve6(path3);
|
|
22319
|
-
if (existsSync10(abs)) return realpathSync4.native(abs);
|
|
22320
|
-
return join18(realpathAllowMissing(dirname10(abs)), basename2(abs));
|
|
22321
|
-
}
|
|
22322
|
-
function run2(cwd, args) {
|
|
22323
|
-
try {
|
|
22324
|
-
return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
22325
|
-
} catch (err) {
|
|
22326
|
-
const stderr = err.stderr?.toString().trim();
|
|
22327
|
-
throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
|
|
22328
|
-
}
|
|
22329
|
-
}
|
|
22330
|
-
var commonDir = (cwd) => {
|
|
22331
|
-
const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
|
|
22332
|
-
return dir ? realpathSync4.native(dir) : null;
|
|
22333
|
-
};
|
|
22334
|
-
function defaultBranch(cwd) {
|
|
22335
|
-
const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
22336
|
-
let ref = head();
|
|
22337
|
-
if (!ref) {
|
|
22338
|
-
git(["remote", "set-head", "origin", "--auto"], cwd);
|
|
22339
|
-
ref = head();
|
|
22340
|
-
}
|
|
22341
|
-
if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
|
|
22342
|
-
return ref.slice("origin/".length);
|
|
22343
|
-
}
|
|
22344
|
-
function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
22345
|
-
if (!existsSync10(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
|
|
22346
|
-
const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
|
|
22347
|
-
const baseCommon = commonDir(pool.base);
|
|
22348
|
-
if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
|
|
22349
|
-
const root = realpathAllowMissing(pool.worktreeRoot);
|
|
22350
|
-
const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
|
|
22351
|
-
if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
|
|
22352
|
-
const members = poolMembers({ ...pool, worktreeRoot: root });
|
|
22353
|
-
for (const m of members) {
|
|
22354
|
-
let symlink = false;
|
|
22355
|
-
try {
|
|
22356
|
-
symlink = lstatSync3(m.worktree).isSymbolicLink();
|
|
22357
|
-
} catch {
|
|
22358
|
-
symlink = false;
|
|
22359
|
-
}
|
|
22360
|
-
const exists = !symlink && existsSync10(m.worktree);
|
|
22361
|
-
const problem = memberPathProblem({
|
|
22362
|
-
path: m.worktree,
|
|
22363
|
-
symlink,
|
|
22364
|
-
exists,
|
|
22365
|
-
baseCommonDir: baseCommon,
|
|
22366
|
-
commonDir: exists ? commonDir(m.worktree) : null,
|
|
22367
|
-
tracked: !exists && !symlink && isTracked(m.worktree)
|
|
22368
|
-
});
|
|
22369
|
-
if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
|
|
22370
|
-
}
|
|
22371
|
-
const missing = members.filter((m) => !existsSync10(m.worktree));
|
|
22372
|
-
if (missing.length > 0) {
|
|
22373
|
-
mkdirSync8(root, { recursive: true });
|
|
22374
|
-
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22375
|
-
const branch = defaultBranch(pool.base);
|
|
22376
|
-
run2(pool.base, ["worktree", "prune"]);
|
|
22377
|
-
for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
|
|
22378
|
-
}
|
|
22379
|
-
return members.map((m) => realpathSync4.native(m.worktree));
|
|
22380
|
-
}
|
|
22381
|
-
function resetWorktree(worktree) {
|
|
22382
|
-
run2(worktree, ["fetch", "-q", "--prune", "origin"]);
|
|
22383
|
-
const target = `origin/${defaultBranch(worktree)}`;
|
|
22384
|
-
run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
|
|
22385
|
-
run2(worktree, ["reset", "-q", "--hard", target]);
|
|
22386
|
-
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;
|
|
22387
22720
|
}
|
|
22388
22721
|
|
|
22389
22722
|
// src/commands/node.ts
|
|
22390
22723
|
var HERE = fileURLToPath3(import.meta.url);
|
|
22391
22724
|
function inspectProcess(pid) {
|
|
22392
22725
|
try {
|
|
22393
|
-
const out =
|
|
22726
|
+
const out = execFileSync8("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
|
|
22394
22727
|
const match = /^(\d+)\s+(.*)$/.exec(out);
|
|
22395
22728
|
return match ? { pgid: Number(match[1]), command: match[2] } : null;
|
|
22396
22729
|
} catch {
|
|
@@ -22414,7 +22747,9 @@ function childSpec(agent, creds, worker, onDemand = false) {
|
|
|
22414
22747
|
REPO_ID: creds.repoId,
|
|
22415
22748
|
REPO_PATH: agent.worktree,
|
|
22416
22749
|
SPECIALIZATION: agent.specialization,
|
|
22417
|
-
WORKER_RUNTIME: agent.runtime
|
|
22750
|
+
WORKER_RUNTIME: agent.runtime,
|
|
22751
|
+
PITBOSS_ACTIVITY_FILE: activityPath(agent.name),
|
|
22752
|
+
PITBOSS_MODEL_FILE: resolve8(modelPath(agent.name))
|
|
22418
22753
|
}
|
|
22419
22754
|
};
|
|
22420
22755
|
}
|
|
@@ -22430,12 +22765,13 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
22430
22765
|
excludeIfUntracked(root, target);
|
|
22431
22766
|
},
|
|
22432
22767
|
run(bin4, args, cwd) {
|
|
22433
|
-
|
|
22768
|
+
execFileSync8(bin4, args, { cwd, stdio: "ignore" });
|
|
22434
22769
|
}
|
|
22435
22770
|
});
|
|
22436
22771
|
}
|
|
22437
22772
|
function spawnWorker(spec) {
|
|
22438
|
-
|
|
22773
|
+
mkdirSync11(dirname12(spec.logPath), { recursive: true, mode: 448 });
|
|
22774
|
+
rmSync6(activityPath(spec.name), { force: true });
|
|
22439
22775
|
const log = openSync(spec.logPath, "a", 384);
|
|
22440
22776
|
try {
|
|
22441
22777
|
const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
|
|
@@ -22482,6 +22818,63 @@ async function reportScale(creds, event) {
|
|
|
22482
22818
|
});
|
|
22483
22819
|
if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
|
|
22484
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
|
+
}
|
|
22485
22878
|
function startPoolLoop(run3, sup, record) {
|
|
22486
22879
|
const { pool, members } = run3;
|
|
22487
22880
|
const { reconcileMs, idleStopMs } = poolTimings();
|
|
@@ -22567,19 +22960,20 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22567
22960
|
const timer = setInterval(() => void tick(), reconcileMs);
|
|
22568
22961
|
return () => clearInterval(timer);
|
|
22569
22962
|
}
|
|
22963
|
+
var MODEL_REFRESH_MS = 15e3;
|
|
22570
22964
|
async function nodeUpCommand(specFile, opts = {}) {
|
|
22571
22965
|
try {
|
|
22572
|
-
const specPath =
|
|
22573
|
-
const specRealpath =
|
|
22574
|
-
const parsed = parseSpec(
|
|
22966
|
+
const specPath = resolve8(specFile);
|
|
22967
|
+
const specRealpath = realpathSync6.native(specPath);
|
|
22968
|
+
const parsed = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
|
|
22575
22969
|
const previous = readState();
|
|
22576
22970
|
if (previous && previous.supervisorPid !== process.pid && isAlive(previous.supervisorPid)) {
|
|
22577
22971
|
throw new Error(`a node is already running (pid ${previous.supervisorPid}, ${previous.specPath}). Stop it first.`);
|
|
22578
22972
|
}
|
|
22579
22973
|
for (const a of parsed.agents) {
|
|
22580
|
-
if (!
|
|
22974
|
+
if (!existsSync12(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
|
|
22581
22975
|
}
|
|
22582
|
-
const plain = parsed.agents.map((a) => ({ ...a, worktree:
|
|
22976
|
+
const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync6.native(a.worktree) }));
|
|
22583
22977
|
const pools = parsed.pools.map((pool) => {
|
|
22584
22978
|
const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
|
|
22585
22979
|
return { pool, members: poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] })) };
|
|
@@ -22609,6 +23003,20 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22609
23003
|
const inviteCodes = collectInvites(spec.agents.map((a) => a.name), opts.invite ?? [], process.env);
|
|
22610
23004
|
const creds = await ensureIdentities(spec, inviteCodes, specRealpath);
|
|
22611
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();
|
|
22612
23020
|
if (runnable.length === 0) throw new Error("no agent has credentials, nothing to start");
|
|
22613
23021
|
const mcp = resolveMcpServer({ here: HERE });
|
|
22614
23022
|
for (const a of runnable) prepareAgent(a, creds.get(a.name), mcp);
|
|
@@ -22647,6 +23055,12 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22647
23055
|
process.on("SIGTERM", shutdown);
|
|
22648
23056
|
process.on("SIGHUP", shutdown);
|
|
22649
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));
|
|
22650
23064
|
for (const { pool, members } of pools) {
|
|
22651
23065
|
const ready = members.filter((m) => creds.has(m.name)).map((agent) => ({ agent, creds: creds.get(agent.name) }));
|
|
22652
23066
|
if (ready.length === 0) {
|
|
@@ -22667,9 +23081,9 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
22667
23081
|
}
|
|
22668
23082
|
async function nodePruneCommand(specFile, opts = {}) {
|
|
22669
23083
|
try {
|
|
22670
|
-
const specPath =
|
|
22671
|
-
const specRealpath =
|
|
22672
|
-
const spec = parseSpec(
|
|
23084
|
+
const specPath = resolve8(specFile);
|
|
23085
|
+
const specRealpath = realpathSync6.native(specPath);
|
|
23086
|
+
const spec = parseSpec(readFileSync15(specPath, "utf-8"), dirname12(specPath));
|
|
22673
23087
|
const { orphans: names, unattributed } = classifyStored(allAgents(spec).map((a) => a.name), specRealpath);
|
|
22674
23088
|
if (names.length === 0) {
|
|
22675
23089
|
console.log("No stored credentials are missing from the spec.");
|
|
@@ -22775,6 +23189,7 @@ var token = program2.command("token").description("Manage your agent's API token
|
|
|
22775
23189
|
token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
|
|
22776
23190
|
token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
|
|
22777
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);
|
|
22778
23193
|
var node = program2.command("node").description("Run this machine's agents headless and keep them running");
|
|
22779
23194
|
node.command("up <spec>").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them)").option(
|
|
22780
23195
|
"--invite <name=code>",
|