@pitboss/cli 0.4.3 → 0.4.5
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 +670 -90
- package/dist/event-worker.js +186 -7
- package/dist/mcp.js +197 -17
- 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((resolve8, 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
|
+
resolve8((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 resolve8;
|
|
17285
17285
|
let reject;
|
|
17286
17286
|
const promise = new Promise((res, rej) => {
|
|
17287
|
-
|
|
17287
|
+
resolve8 = res;
|
|
17288
17288
|
reject = rej;
|
|
17289
17289
|
});
|
|
17290
|
-
return { promise, resolve:
|
|
17290
|
+
return { promise, resolve: resolve8, 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: resolve8, 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
|
+
resolve8(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
|
+
resolve8(value);
|
|
17382
17382
|
}
|
|
17383
17383
|
});
|
|
17384
17384
|
};
|
|
@@ -17463,28 +17463,28 @@ var RemoveFileError = class extends Error {
|
|
|
17463
17463
|
|
|
17464
17464
|
// ../../node_modules/.pnpm/@inquirer+external-editor@3.0.4_@types+node@22.19.17/node_modules/@inquirer/external-editor/dist/parse-editor-command.js
|
|
17465
17465
|
function parseEditorCommand(editor) {
|
|
17466
|
-
let
|
|
17466
|
+
let bin3;
|
|
17467
17467
|
let rest;
|
|
17468
17468
|
if (editor.startsWith('"')) {
|
|
17469
17469
|
const closeQuote = editor.indexOf('"', 1);
|
|
17470
17470
|
if (closeQuote === -1) {
|
|
17471
|
-
|
|
17471
|
+
bin3 = editor.slice(1);
|
|
17472
17472
|
rest = "";
|
|
17473
17473
|
} else {
|
|
17474
|
-
|
|
17474
|
+
bin3 = editor.substring(1, closeQuote);
|
|
17475
17475
|
rest = editor.substring(closeQuote + 1).trim();
|
|
17476
17476
|
}
|
|
17477
17477
|
} else {
|
|
17478
17478
|
const firstSpace = editor.indexOf(" ");
|
|
17479
17479
|
if (firstSpace === -1) {
|
|
17480
|
-
|
|
17480
|
+
bin3 = editor;
|
|
17481
17481
|
rest = "";
|
|
17482
17482
|
} else {
|
|
17483
|
-
|
|
17483
|
+
bin3 = editor.substring(0, firstSpace);
|
|
17484
17484
|
rest = editor.substring(firstSpace + 1).trim();
|
|
17485
17485
|
}
|
|
17486
17486
|
}
|
|
17487
|
-
return { bin:
|
|
17487
|
+
return { bin: bin3, args: rest ? rest.split(/\s+/) : [] };
|
|
17488
17488
|
}
|
|
17489
17489
|
|
|
17490
17490
|
// ../../node_modules/.pnpm/@inquirer+external-editor@3.0.4_@types+node@22.19.17/node_modules/@inquirer/external-editor/dist/index.js
|
|
@@ -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((resolve8, 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
|
+
resolve8();
|
|
17548
17548
|
});
|
|
17549
17549
|
} catch (launchError) {
|
|
17550
17550
|
reject(new LaunchEditorError(launchError));
|
|
@@ -18343,7 +18343,7 @@ async function taskCreateCommand(options) {
|
|
|
18343
18343
|
if (options.reviewBy) {
|
|
18344
18344
|
verifyKind = "reviewer_agent";
|
|
18345
18345
|
} else if (options.verifyKind) {
|
|
18346
|
-
if (!["shell", "file_exists", "thread_concluded", "reviewer_agent", "human_approval"].includes(options.verifyKind)) {
|
|
18346
|
+
if (!["shell", "file_exists", "thread_concluded", "reviewer_agent", "human_approval", "ci_passed"].includes(options.verifyKind)) {
|
|
18347
18347
|
console.error(source_default.red(`Invalid --verify-kind "${options.verifyKind}".`));
|
|
18348
18348
|
process.exit(1);
|
|
18349
18349
|
}
|
|
@@ -18357,6 +18357,9 @@ async function taskCreateCommand(options) {
|
|
|
18357
18357
|
process.exit(1);
|
|
18358
18358
|
}
|
|
18359
18359
|
verifyReviewerId = await resolveAgentRef(client, config.repoId, reviewerRef);
|
|
18360
|
+
} else if (verifyKind === "ci_passed" && !options.verifyPath) {
|
|
18361
|
+
console.error(source_default.red("--verify-kind ci_passed requires --verify-path <sha>."));
|
|
18362
|
+
process.exit(1);
|
|
18360
18363
|
} else if (options.verifyReviewer) {
|
|
18361
18364
|
console.error(source_default.red("--verify-reviewer is only valid with --verify-kind reviewer_agent."));
|
|
18362
18365
|
process.exit(1);
|
|
@@ -18957,10 +18960,10 @@ async function readWake(stream, { idleMs = 1e3, stallMs = 75e3 } = {}) {
|
|
|
18957
18960
|
}
|
|
18958
18961
|
if (next.done) return wake;
|
|
18959
18962
|
buffer += decoder.decode(next.value, { stream: true });
|
|
18960
|
-
let
|
|
18961
|
-
while ((
|
|
18962
|
-
const { id, event } = parseFrame(buffer.slice(0,
|
|
18963
|
-
buffer = buffer.slice(
|
|
18963
|
+
let sep3;
|
|
18964
|
+
while ((sep3 = buffer.indexOf("\n\n")) !== -1) {
|
|
18965
|
+
const { id, event } = parseFrame(buffer.slice(0, sep3));
|
|
18966
|
+
buffer = buffer.slice(sep3 + 2);
|
|
18964
18967
|
if (id && EVENT_ID.test(id)) wake.lastId = id;
|
|
18965
18968
|
if (event?.kind === "replay.truncated" && !wake.first) {
|
|
18966
18969
|
wake.truncated = true;
|
|
@@ -19104,6 +19107,7 @@ function isWorkerType(value) {
|
|
|
19104
19107
|
return typeof value === "string" && WORKER_TYPES.includes(value);
|
|
19105
19108
|
}
|
|
19106
19109
|
var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
|
|
19110
|
+
var POOL_STOPPED_REASON = "pool member stopped by pitboss node";
|
|
19107
19111
|
|
|
19108
19112
|
// src/lib/runtimes.ts
|
|
19109
19113
|
var RUNTIMES2 = RUNTIMES.map((r) => r.value);
|
|
@@ -20340,10 +20344,10 @@ async function consumeEventStream(stream, onEvent) {
|
|
|
20340
20344
|
const { done, value } = await reader.read();
|
|
20341
20345
|
if (done) break;
|
|
20342
20346
|
buffer += decoder.decode(value, { stream: true });
|
|
20343
|
-
let
|
|
20344
|
-
while ((
|
|
20345
|
-
const event = parseSseFrame(buffer.slice(0,
|
|
20346
|
-
buffer = buffer.slice(
|
|
20347
|
+
let sep3;
|
|
20348
|
+
while ((sep3 = buffer.indexOf("\n\n")) !== -1) {
|
|
20349
|
+
const event = parseSseFrame(buffer.slice(0, sep3));
|
|
20350
|
+
buffer = buffer.slice(sep3 + 2);
|
|
20347
20351
|
if (event) onEvent(event);
|
|
20348
20352
|
}
|
|
20349
20353
|
}
|
|
@@ -20596,13 +20600,13 @@ function hookCursorPromptCommand(opts) {
|
|
|
20596
20600
|
}
|
|
20597
20601
|
|
|
20598
20602
|
// src/commands/node.ts
|
|
20599
|
-
import { execFileSync as
|
|
20600
|
-
import { closeSync, existsSync as
|
|
20601
|
-
import { dirname as
|
|
20603
|
+
import { execFileSync as execFileSync7, spawn as spawnChild } from "node:child_process";
|
|
20604
|
+
import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync12, realpathSync as realpathSync5 } from "node:fs";
|
|
20605
|
+
import { dirname as dirname11, resolve as resolve7 } from "node:path";
|
|
20602
20606
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
20603
20607
|
|
|
20604
20608
|
// src/node/spec.ts
|
|
20605
|
-
import { resolve as resolve4 } from "node:path";
|
|
20609
|
+
import { join as join14, resolve as resolve4, sep as sep2 } from "node:path";
|
|
20606
20610
|
|
|
20607
20611
|
// ../claude-worker/src/session.ts
|
|
20608
20612
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -20754,7 +20758,7 @@ var HANDOFF_RULES = `## Handoff discipline
|
|
|
20754
20758
|
relevant paths, branch name, and what you did or found. The next session has the
|
|
20755
20759
|
thread, not your head.
|
|
20756
20760
|
- \`mcp__relai__create_task\` only when a human explicitly asks you to file a **new**
|
|
20757
|
-
piece of work. Say in the description that it is a proposal an orchestrator must
|
|
20761
|
+
piece of work, or when you hold an order's planning task (see Factory orders below). Say in the description that it is a proposal an orchestrator must
|
|
20758
20762
|
commit. Do not attach roundNumber or parentTaskId bookkeeping, and do not use it
|
|
20759
20763
|
to chain reviewer/writer/tester/devops work.`;
|
|
20760
20764
|
function specializationBlock(spec) {
|
|
@@ -20870,6 +20874,78 @@ Handle CI/CD, infrastructure, build/deploy configuration.
|
|
|
20870
20874
|
throw new Error(`Unknown specialization "${spec}" \u2014 valid values: reviewer, architect, writer, tester, devops`);
|
|
20871
20875
|
}
|
|
20872
20876
|
}
|
|
20877
|
+
var ORDER_RULES = `## Factory orders
|
|
20878
|
+
|
|
20879
|
+
An order is a feature request the system builds end to end. Its tasks carry
|
|
20880
|
+
\`metadata.orderId\` and \`metadata.orderRole\`. For a task with \`metadata.orderRole\`, the
|
|
20881
|
+
instructions below override your role rules and the branch rules above. Read the order with
|
|
20882
|
+
\`mcp__relai__get_order\` (id = \`metadata.orderId\`). The order branch is \`order/<orderId>\`.
|
|
20883
|
+
Find the default branch with \`git remote show origin\`.
|
|
20884
|
+
|
|
20885
|
+
**orderRole "planning"** (you are the order's planner):
|
|
20886
|
+
1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it. If the order
|
|
20887
|
+
has no \`title\`, give it a short one (a few words) with \`mcp__relai__update_order\`.
|
|
20888
|
+
2. Create the branch and PR:
|
|
20889
|
+
\`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
|
|
20890
|
+
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
|
|
20891
|
+
then \`gh pr create --draft --base <default> --head order/<orderId> --title "<feature>" --body "Order <orderId>"\`.
|
|
20892
|
+
Record both with \`mcp__relai__update_order\` (\`branch\`, \`prUrl\`).
|
|
20893
|
+
3. Build tasks: \`mcp__relai__create_task\` for each piece of work, with \`epicId\` = the order's
|
|
20894
|
+
\`epicId\`, \`assignedTo: "@auto"\`, and
|
|
20895
|
+
\`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
|
|
20896
|
+
\`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
|
|
20897
|
+
the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
|
|
20898
|
+
\`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
|
|
20899
|
+
Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
|
|
20900
|
+
and independently testable so several agents can work at once. Use \`blockedBy\` only where
|
|
20901
|
+
one task truly needs another's result. Each description is self-contained: what to change,
|
|
20902
|
+
which files, how to test it, and the acceptance criteria.
|
|
20903
|
+
4. Gate tasks, blocked by every build task, \`assignedTo: "@auto"\`,
|
|
20904
|
+
\`metadata: { orderId, orderRole: "gate" }\`:
|
|
20905
|
+
- \`human_approval\` in acceptance: a task "Approve: <feature>" with \`verifyKind: "human_approval"\`.
|
|
20906
|
+
- \`reviewer_agent\` in acceptance: a task "Review: <feature>" with \`verifyKind: "reviewer_agent"\`
|
|
20907
|
+
and \`verifyReviewerId\` set to a reviewer from \`mcp__relai__list_agents\`.
|
|
20908
|
+
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
20909
|
+
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
20910
|
+
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
20911
|
+
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
20912
|
+
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
20913
|
+
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
20914
|
+
the planning task's thread and complete it.
|
|
20915
|
+
|
|
20916
|
+
**orderRole "build"**:
|
|
20917
|
+
- \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
|
|
20918
|
+
test and commit. Do not create another branch or PR.
|
|
20919
|
+
- Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
|
|
20920
|
+
repeating up to 5 times if the push is rejected. Never force-push.
|
|
20921
|
+
- If you drop or change any part of what the goal asked for, even for a good reason, record each one
|
|
20922
|
+
with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
|
|
20923
|
+
"reason": "<why>" }]\` and say so in your handoff. The owner decides whether to accept it.
|
|
20924
|
+
- Hand off on the task's thread with the commit sha and what you tested, then complete.
|
|
20925
|
+
|
|
20926
|
+
**orderRole "gate"**:
|
|
20927
|
+
- Check every build task in the order's Epic is completed, then post the PR link on the task's
|
|
20928
|
+
thread, listing every \`metadata.deviations\` entry from the build tasks, and complete it. The gate decides from there: the owner approves a \`human_approval\`
|
|
20929
|
+
gate on the dashboard and the named reviewer decides a \`reviewer_agent\` gate.
|
|
20930
|
+
- As the reviewer of an order, check each thing the goal asked for. Anything dropped or changed is
|
|
20931
|
+
the owner's call, not yours: if it is recorded in \`metadata.deviations\`, mention it and judge
|
|
20932
|
+
the rest; if it is not recorded, reject and name it so the builder records it. The owner decides
|
|
20933
|
+
on deviations in the approval gate.
|
|
20934
|
+
|
|
20935
|
+
**orderRole "merge"**:
|
|
20936
|
+
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
|
|
20937
|
+
Before merging, replace the PR body with a run report built from
|
|
20938
|
+
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
20939
|
+
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
20940
|
+
completed times, the number of distinct agents that worked on it, the total elapsed time
|
|
20941
|
+
from the planning task's creation, and the CI checks that ran. Use \`gh pr edit <prUrl> --body-file <file>\`.
|
|
20942
|
+
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
20943
|
+
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
20944
|
+
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
20945
|
+
\`gh pr edit <prUrl> --body-file <file>\` (a merged PR still accepts an edit). Post the merge commit
|
|
20946
|
+
on the thread, and complete.
|
|
20947
|
+
3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
|
|
20948
|
+
After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
|
|
20873
20949
|
function buildPrompt(config) {
|
|
20874
20950
|
return [
|
|
20875
20951
|
BASE_TASK_LOOP(config.agentId, config.repoPath),
|
|
@@ -20878,7 +20954,9 @@ function buildPrompt(config) {
|
|
|
20878
20954
|
"",
|
|
20879
20955
|
SAME_TASK_RULES(config.maxTaskRounds),
|
|
20880
20956
|
"",
|
|
20881
|
-
HANDOFF_RULES
|
|
20957
|
+
HANDOFF_RULES,
|
|
20958
|
+
"",
|
|
20959
|
+
ORDER_RULES
|
|
20882
20960
|
].join("\n");
|
|
20883
20961
|
}
|
|
20884
20962
|
|
|
@@ -21020,7 +21098,7 @@ function mcpServerLaunch(here = fileURLToPath2(import.meta.url), exists = exists
|
|
|
21020
21098
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21021
21099
|
return { command: execPath, args: [mcpServerTsxCli(), mcpServerEntry()] };
|
|
21022
21100
|
}
|
|
21023
|
-
function writeMcpConfig(config) {
|
|
21101
|
+
function writeMcpConfig(config, prefix = "claude-worker") {
|
|
21024
21102
|
const { command, args } = mcpServerLaunch();
|
|
21025
21103
|
const mcpConfig = {
|
|
21026
21104
|
mcpServers: {
|
|
@@ -21036,7 +21114,7 @@ function writeMcpConfig(config) {
|
|
|
21036
21114
|
}
|
|
21037
21115
|
}
|
|
21038
21116
|
};
|
|
21039
|
-
const path3 = join12(tmpdir(),
|
|
21117
|
+
const path3 = join12(tmpdir(), `${prefix}-${config.agentId}-${Date.now()}.json`);
|
|
21040
21118
|
writeFileSync6(path3, JSON.stringify(mcpConfig), { mode: 384 });
|
|
21041
21119
|
return path3;
|
|
21042
21120
|
}
|
|
@@ -21045,7 +21123,7 @@ async function runClaudeSession(config) {
|
|
|
21045
21123
|
const mcpConfigPath = writeMcpConfig(config);
|
|
21046
21124
|
const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
|
|
21047
21125
|
try {
|
|
21048
|
-
return await new Promise((
|
|
21126
|
+
return await new Promise((resolve8, reject) => {
|
|
21049
21127
|
const args = [
|
|
21050
21128
|
"--print",
|
|
21051
21129
|
"--mcp-config",
|
|
@@ -21125,7 +21203,7 @@ async function runClaudeSession(config) {
|
|
|
21125
21203
|
const detail = (resultError || stderrOutput || "").trim();
|
|
21126
21204
|
reject(new Error(`claude exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21127
21205
|
} else {
|
|
21128
|
-
|
|
21206
|
+
resolve8(unique);
|
|
21129
21207
|
}
|
|
21130
21208
|
};
|
|
21131
21209
|
finish.then(settle, settle);
|
|
@@ -21205,7 +21283,7 @@ async function runCursorSession(config) {
|
|
|
21205
21283
|
const args = cursorArgs(config.repoPath, buildPrompt(config));
|
|
21206
21284
|
console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
|
|
21207
21285
|
const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
|
|
21208
|
-
return new Promise((
|
|
21286
|
+
return new Promise((resolve8, reject) => {
|
|
21209
21287
|
const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21210
21288
|
console.log(`[cursor-worker] Process PID: ${proc.pid}`);
|
|
21211
21289
|
const toolsUsed = [];
|
|
@@ -21244,7 +21322,7 @@ async function runCursorSession(config) {
|
|
|
21244
21322
|
const detail = (resultError || stderrOutput).trim();
|
|
21245
21323
|
reject(new Error(`cursor-agent exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21246
21324
|
} else {
|
|
21247
|
-
|
|
21325
|
+
resolve8([...new Set(toolsUsed)]);
|
|
21248
21326
|
}
|
|
21249
21327
|
};
|
|
21250
21328
|
finish.then(settle, settle);
|
|
@@ -21275,9 +21353,111 @@ var claudeAdapter = {
|
|
|
21275
21353
|
classifyError: classifySessionError
|
|
21276
21354
|
};
|
|
21277
21355
|
|
|
21356
|
+
// ../claude-worker/src/adapters/copilot.ts
|
|
21357
|
+
import { spawn as spawn4 } from "child_process";
|
|
21358
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
21359
|
+
var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
|
|
21360
|
+
var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
|
|
21361
|
+
var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
|
|
21362
|
+
function copilotArgs(repoPath, prompt, mcpConfigPath) {
|
|
21363
|
+
return [
|
|
21364
|
+
"-p",
|
|
21365
|
+
prompt,
|
|
21366
|
+
"--allow-all-tools",
|
|
21367
|
+
"--output-format",
|
|
21368
|
+
"json",
|
|
21369
|
+
"--no-color",
|
|
21370
|
+
"-C",
|
|
21371
|
+
repoPath,
|
|
21372
|
+
"--additional-mcp-config",
|
|
21373
|
+
`@${mcpConfigPath}`,
|
|
21374
|
+
"--disable-builtin-mcps"
|
|
21375
|
+
];
|
|
21376
|
+
}
|
|
21377
|
+
function readCopilotEvent(line) {
|
|
21378
|
+
let event;
|
|
21379
|
+
try {
|
|
21380
|
+
event = JSON.parse(line);
|
|
21381
|
+
} catch {
|
|
21382
|
+
return {};
|
|
21383
|
+
}
|
|
21384
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
21385
|
+
const data = event.data ?? {};
|
|
21386
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
21387
|
+
if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
|
|
21388
|
+
if (type === "session.error") return { error: message || "copilot session error" };
|
|
21389
|
+
if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
|
|
21390
|
+
if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
|
|
21391
|
+
return {};
|
|
21392
|
+
}
|
|
21393
|
+
async function runCopilotSession(config) {
|
|
21394
|
+
const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
|
|
21395
|
+
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
|
|
21396
|
+
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
21397
|
+
return new Promise((resolve8, reject) => {
|
|
21398
|
+
const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21399
|
+
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
21400
|
+
const toolsUsed = [];
|
|
21401
|
+
let buffer = "";
|
|
21402
|
+
let sessionError = "";
|
|
21403
|
+
let stderrOutput = "";
|
|
21404
|
+
proc.stdout.on("data", (chunk) => {
|
|
21405
|
+
buffer += chunk.toString();
|
|
21406
|
+
const lines = buffer.split("\n");
|
|
21407
|
+
buffer = lines.pop() ?? "";
|
|
21408
|
+
for (const line of lines) {
|
|
21409
|
+
const { tool, error } = readCopilotEvent(line);
|
|
21410
|
+
if (tool) {
|
|
21411
|
+
toolsUsed.push(tool);
|
|
21412
|
+
process.stdout.write(` \u2699 ${tool}
|
|
21413
|
+
`);
|
|
21414
|
+
}
|
|
21415
|
+
if (error) {
|
|
21416
|
+
sessionError = error;
|
|
21417
|
+
console.error(`[copilot-worker] Session failed: ${error}`);
|
|
21418
|
+
}
|
|
21419
|
+
}
|
|
21420
|
+
});
|
|
21421
|
+
proc.stderr.on("data", (chunk) => {
|
|
21422
|
+
stderrOutput += chunk.toString();
|
|
21423
|
+
});
|
|
21424
|
+
proc.on("close", (code, signal) => {
|
|
21425
|
+
try {
|
|
21426
|
+
unlinkSync2(mcpConfigPath);
|
|
21427
|
+
} catch {
|
|
21428
|
+
}
|
|
21429
|
+
console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
21430
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
21431
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
21432
|
+
reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21433
|
+
} else {
|
|
21434
|
+
resolve8([...new Set(toolsUsed)]);
|
|
21435
|
+
}
|
|
21436
|
+
});
|
|
21437
|
+
proc.on("error", (err) => {
|
|
21438
|
+
try {
|
|
21439
|
+
unlinkSync2(mcpConfigPath);
|
|
21440
|
+
} catch {
|
|
21441
|
+
}
|
|
21442
|
+
reject(err);
|
|
21443
|
+
});
|
|
21444
|
+
});
|
|
21445
|
+
}
|
|
21446
|
+
var copilotAdapter = {
|
|
21447
|
+
runtime: "copilot",
|
|
21448
|
+
prepare() {
|
|
21449
|
+
},
|
|
21450
|
+
runSession: runCopilotSession,
|
|
21451
|
+
classifyError: (text) => {
|
|
21452
|
+
if (POLICY_BLOCKS.some((re) => re.test(text))) return "credentials";
|
|
21453
|
+
if (QUOTA.some((re) => re.test(text))) return "quota";
|
|
21454
|
+
return classifySessionError(text);
|
|
21455
|
+
}
|
|
21456
|
+
};
|
|
21457
|
+
|
|
21278
21458
|
// ../claude-worker/src/adapters/index.ts
|
|
21279
|
-
var RUNTIMES3 = ["claude", "cursor"];
|
|
21280
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
|
|
21459
|
+
var RUNTIMES3 = ["claude", "cursor", "copilot"];
|
|
21460
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
|
|
21281
21461
|
function getAdapter(runtime) {
|
|
21282
21462
|
const adapter = ADAPTERS[runtime];
|
|
21283
21463
|
if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
|
|
@@ -21292,10 +21472,53 @@ var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devop
|
|
|
21292
21472
|
|
|
21293
21473
|
// src/node/spec.ts
|
|
21294
21474
|
var DEFAULT_API2 = "https://api.pitboss.dev";
|
|
21475
|
+
var MAX_POOL_SIZE = 32;
|
|
21295
21476
|
var NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
21477
|
+
var POOL_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,59}$/;
|
|
21296
21478
|
function inviteEnvKey(name) {
|
|
21297
21479
|
return `PITBOSS_NODE_INVITE_${name.toUpperCase().replace(/[.-]/g, "_")}`;
|
|
21298
21480
|
}
|
|
21481
|
+
var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(sep2) ? dir : dir + sep2);
|
|
21482
|
+
function poolMembers(pool) {
|
|
21483
|
+
return Array.from({ length: pool.max }, (_, i) => {
|
|
21484
|
+
const name = `${pool.pool}-${i + 1}`;
|
|
21485
|
+
return { name, runtime: pool.runtime, worktree: join14(pool.worktreeRoot, name), specialization: pool.specialization };
|
|
21486
|
+
});
|
|
21487
|
+
}
|
|
21488
|
+
var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
|
|
21489
|
+
function runtimeOf(a, i) {
|
|
21490
|
+
if (!RUNTIMES3.includes(a.runtime)) {
|
|
21491
|
+
throw new Error(`agents[${i}]: runtime "${String(a.runtime)}" is not supported (have: ${RUNTIMES3.join(", ")})`);
|
|
21492
|
+
}
|
|
21493
|
+
return a.runtime;
|
|
21494
|
+
}
|
|
21495
|
+
function specializationOf(a, i) {
|
|
21496
|
+
const specialization = a.specialization === void 0 ? "writer" : a.specialization;
|
|
21497
|
+
if (!VALID_SPECIALIZATIONS.includes(specialization)) {
|
|
21498
|
+
throw new Error(`agents[${i}]: specialization "${String(specialization)}" is not one of ${VALID_SPECIALIZATIONS.join(", ")}`);
|
|
21499
|
+
}
|
|
21500
|
+
return specialization;
|
|
21501
|
+
}
|
|
21502
|
+
function parsePool(a, i, specDir) {
|
|
21503
|
+
if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
|
|
21504
|
+
if (typeof a.pool !== "string" || !POOL_NAME.test(a.pool)) throw new Error(`agents[${i}].pool must be letters, digits, '.', '_' or '-' (at most 60)`);
|
|
21505
|
+
if (typeof a.base !== "string" || !a.base) throw new Error(`agents[${i}].base is required: the clone pool worktrees are created from`);
|
|
21506
|
+
if (typeof a.worktreeRoot !== "string" || !a.worktreeRoot) throw new Error(`agents[${i}].worktreeRoot is required`);
|
|
21507
|
+
const runtime = runtimeOf(a, i);
|
|
21508
|
+
const specialization = specializationOf(a, i);
|
|
21509
|
+
const max = a.max;
|
|
21510
|
+
if (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > MAX_POOL_SIZE) {
|
|
21511
|
+
throw new Error(`agents[${i}].max must be an integer from 1 to ${MAX_POOL_SIZE}`);
|
|
21512
|
+
}
|
|
21513
|
+
const min = a.min === void 0 ? 0 : a.min;
|
|
21514
|
+
if (typeof min !== "number" || !Number.isInteger(min) || min < 0 || min > max) {
|
|
21515
|
+
throw new Error(`agents[${i}].min must be an integer from 0 to max`);
|
|
21516
|
+
}
|
|
21517
|
+
const base2 = resolve4(specDir, a.base);
|
|
21518
|
+
const worktreeRoot = resolve4(specDir, a.worktreeRoot);
|
|
21519
|
+
if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
|
|
21520
|
+
return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, min, max };
|
|
21521
|
+
}
|
|
21299
21522
|
function parseSpec(text, specDir) {
|
|
21300
21523
|
let raw;
|
|
21301
21524
|
try {
|
|
@@ -21309,23 +21532,30 @@ function parseSpec(text, specDir) {
|
|
|
21309
21532
|
throw new Error("api must be https (plain http only for localhost)");
|
|
21310
21533
|
}
|
|
21311
21534
|
if (!Array.isArray(body.agents) || body.agents.length === 0) throw new Error("spec needs at least one agent");
|
|
21312
|
-
const agents =
|
|
21535
|
+
const agents = [];
|
|
21536
|
+
const pools = [];
|
|
21537
|
+
body.agents.forEach((entry, i) => {
|
|
21313
21538
|
const a = entry ?? {};
|
|
21539
|
+
if (a.pool !== void 0) {
|
|
21540
|
+
pools.push(parsePool(a, i, specDir));
|
|
21541
|
+
return;
|
|
21542
|
+
}
|
|
21314
21543
|
if (typeof a.name !== "string" || !NAME.test(a.name)) throw new Error(`agents[${i}].name must be letters, digits, '.', '_' or '-'`);
|
|
21315
21544
|
if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
|
|
21316
|
-
|
|
21317
|
-
|
|
21318
|
-
}
|
|
21319
|
-
const specialization = a.specialization === void 0 ? "writer" : a.specialization;
|
|
21320
|
-
if (!VALID_SPECIALIZATIONS.includes(specialization)) {
|
|
21321
|
-
throw new Error(`agents[${i}]: specialization "${String(specialization)}" is not one of ${VALID_SPECIALIZATIONS.join(", ")}`);
|
|
21322
|
-
}
|
|
21323
|
-
return { name: a.name, runtime: a.runtime, worktree: resolve4(specDir, a.worktree), specialization };
|
|
21545
|
+
const runtime = runtimeOf(a, i);
|
|
21546
|
+
agents.push({ name: a.name, runtime, worktree: resolve4(specDir, a.worktree), specialization: specializationOf(a, i) });
|
|
21324
21547
|
});
|
|
21548
|
+
for (const p of pools) {
|
|
21549
|
+
for (const a of agents) {
|
|
21550
|
+
if (isWithin(p.worktreeRoot, a.worktree)) {
|
|
21551
|
+
throw new Error(`pool "${p.pool}" worktreeRoot ${p.worktreeRoot} is inside agent "${a.name}"'s worktree`);
|
|
21552
|
+
}
|
|
21553
|
+
}
|
|
21554
|
+
}
|
|
21325
21555
|
const names = /* @__PURE__ */ new Set();
|
|
21326
21556
|
const worktrees = /* @__PURE__ */ new Map();
|
|
21327
21557
|
const envKeys = /* @__PURE__ */ new Map();
|
|
21328
|
-
for (const a of agents) {
|
|
21558
|
+
for (const a of allAgents({ api, agents, pools })) {
|
|
21329
21559
|
if (names.has(a.name.toLowerCase())) throw new Error(`agent name "${a.name}" appears twice`);
|
|
21330
21560
|
names.add(a.name.toLowerCase());
|
|
21331
21561
|
const envKey = inviteEnvKey(a.name);
|
|
@@ -21336,17 +21566,17 @@ function parseSpec(text, specDir) {
|
|
|
21336
21566
|
if (other) throw new Error(`agents "${other}" and "${a.name}" share the worktree ${a.worktree}; each needs its own`);
|
|
21337
21567
|
worktrees.set(a.worktree, a.name);
|
|
21338
21568
|
}
|
|
21339
|
-
return { api: api.replace(/\/+$/, ""), agents };
|
|
21569
|
+
return { api: api.replace(/\/+$/, ""), agents, pools };
|
|
21340
21570
|
}
|
|
21341
21571
|
|
|
21342
21572
|
// src/node/store.ts
|
|
21343
21573
|
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync11, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
21344
|
-
import { dirname as dirname8, join as
|
|
21574
|
+
import { dirname as dirname8, join as join15 } from "node:path";
|
|
21345
21575
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
21346
|
-
var nodeDir = () =>
|
|
21347
|
-
var credsPath = (name) =>
|
|
21348
|
-
var statePath2 = () =>
|
|
21349
|
-
var logPath = (name) =>
|
|
21576
|
+
var nodeDir = () => join15(configDir(), "node");
|
|
21577
|
+
var credsPath = (name) => join15(nodeDir(), "agents", `${name}.json`);
|
|
21578
|
+
var statePath2 = () => join15(nodeDir(), "state.json");
|
|
21579
|
+
var logPath = (name) => join15(nodeDir(), "logs", `${name}.log`);
|
|
21350
21580
|
function writePrivate(path3, value) {
|
|
21351
21581
|
mkdirSync6(dirname8(path3), { recursive: true, mode: 448 });
|
|
21352
21582
|
const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
|
|
@@ -21364,7 +21594,7 @@ var removeCreds = (name) => {
|
|
|
21364
21594
|
if (existsSync8(path3)) rmSync4(path3);
|
|
21365
21595
|
};
|
|
21366
21596
|
var listStoredAgentNames = () => {
|
|
21367
|
-
const dir =
|
|
21597
|
+
const dir = join15(nodeDir(), "agents");
|
|
21368
21598
|
if (!existsSync8(dir)) return [];
|
|
21369
21599
|
return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
21370
21600
|
};
|
|
@@ -21577,14 +21807,45 @@ var Supervisor = class {
|
|
|
21577
21807
|
failures: 0,
|
|
21578
21808
|
startedMs: 0,
|
|
21579
21809
|
timer: null,
|
|
21580
|
-
|
|
21810
|
+
held: spec.onDemand === true,
|
|
21811
|
+
state: { pid: null, status: spec.onDemand ? "stopped" : "starting", restarts: 0, startedAt: null, lastExit: null, nextStartAt: null }
|
|
21581
21812
|
}));
|
|
21582
21813
|
}
|
|
21583
21814
|
opts;
|
|
21584
21815
|
slots;
|
|
21585
21816
|
stopping = false;
|
|
21586
21817
|
start() {
|
|
21587
|
-
for (const slot of this.slots) this.launch(slot);
|
|
21818
|
+
for (const slot of this.slots) if (!slot.held) this.launch(slot);
|
|
21819
|
+
}
|
|
21820
|
+
isActive(name) {
|
|
21821
|
+
return this.slot(name).held === false;
|
|
21822
|
+
}
|
|
21823
|
+
startAgent(name) {
|
|
21824
|
+
const slot = this.slot(name);
|
|
21825
|
+
if (!slot.held || this.stopping) return;
|
|
21826
|
+
slot.held = false;
|
|
21827
|
+
slot.failures = 0;
|
|
21828
|
+
this.launch(slot);
|
|
21829
|
+
}
|
|
21830
|
+
async stopAgent(name) {
|
|
21831
|
+
const slot = this.slot(name);
|
|
21832
|
+
slot.held = true;
|
|
21833
|
+
if (slot.timer) clearTimeout(slot.timer);
|
|
21834
|
+
slot.timer = null;
|
|
21835
|
+
const child = slot.child;
|
|
21836
|
+
if (!child) {
|
|
21837
|
+
slot.state = { ...slot.state, pid: null, status: "stopped", nextStartAt: null };
|
|
21838
|
+
this.emit();
|
|
21839
|
+
return;
|
|
21840
|
+
}
|
|
21841
|
+
const exited = new Promise((done) => child.on("exit", () => done()));
|
|
21842
|
+
child.kill("SIGTERM");
|
|
21843
|
+
await exited;
|
|
21844
|
+
}
|
|
21845
|
+
slot(name) {
|
|
21846
|
+
const slot = this.slots.find((s) => s.spec.name === name);
|
|
21847
|
+
if (!slot) throw new Error(`no agent named ${name}`);
|
|
21848
|
+
return slot;
|
|
21588
21849
|
}
|
|
21589
21850
|
snapshot() {
|
|
21590
21851
|
return Object.fromEntries(this.slots.map((s) => [s.spec.name, { ...s.state }]));
|
|
@@ -21634,8 +21895,9 @@ var Supervisor = class {
|
|
|
21634
21895
|
slot.child = null;
|
|
21635
21896
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
21636
21897
|
const lastExit = { code, signal, at, ...error ? { error } : {} };
|
|
21637
|
-
if (this.stopping) {
|
|
21898
|
+
if (this.stopping || slot.held) {
|
|
21638
21899
|
slot.state = { ...slot.state, pid: null, status: "stopped", lastExit, nextStartAt: null };
|
|
21900
|
+
if (slot.held && !this.stopping) this.emit();
|
|
21639
21901
|
return;
|
|
21640
21902
|
}
|
|
21641
21903
|
slot.failures = slot.startedMs && Date.now() - slot.startedMs >= this.opts.stableMs ? 1 : slot.failures + 1;
|
|
@@ -21658,27 +21920,27 @@ var Supervisor = class {
|
|
|
21658
21920
|
|
|
21659
21921
|
// src/node/entries.ts
|
|
21660
21922
|
import { existsSync as existsSync9 } from "node:fs";
|
|
21661
|
-
import { dirname as dirname9, join as
|
|
21923
|
+
import { dirname as dirname9, join as join16, resolve as resolve5 } from "node:path";
|
|
21662
21924
|
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.";
|
|
21663
21925
|
function checkoutWorker(dir) {
|
|
21664
21926
|
return {
|
|
21665
|
-
command:
|
|
21666
|
-
args: [
|
|
21927
|
+
command: join16(dir, "node_modules", ".bin", "tsx"),
|
|
21928
|
+
args: [join16(dir, "src", "index.ts")]
|
|
21667
21929
|
};
|
|
21668
21930
|
}
|
|
21669
21931
|
function checkoutMcp(dir, execPath) {
|
|
21670
21932
|
return {
|
|
21671
21933
|
command: execPath,
|
|
21672
21934
|
args: [
|
|
21673
|
-
|
|
21674
|
-
|
|
21935
|
+
join16(dir, "node_modules", "tsx", "dist", "cli.mjs"),
|
|
21936
|
+
join16(dir, "src", "index.ts")
|
|
21675
21937
|
]
|
|
21676
21938
|
};
|
|
21677
21939
|
}
|
|
21678
21940
|
function walkFor(here, packageName, exists) {
|
|
21679
21941
|
for (let dir = dirname9(here); dirname9(dir) !== dir; dir = dirname9(dir)) {
|
|
21680
|
-
const candidate =
|
|
21681
|
-
if (exists(
|
|
21942
|
+
const candidate = join16(dir, packageName);
|
|
21943
|
+
if (exists(join16(candidate, "src", "index.ts"))) return candidate;
|
|
21682
21944
|
}
|
|
21683
21945
|
return null;
|
|
21684
21946
|
}
|
|
@@ -21690,10 +21952,10 @@ function resolveEventWorker(opts) {
|
|
|
21690
21952
|
if (override) {
|
|
21691
21953
|
const resolved = resolve5(override);
|
|
21692
21954
|
if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
|
|
21693
|
-
if (exists(
|
|
21955
|
+
if (exists(join16(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
|
|
21694
21956
|
throw new Error(MISSING_WORKER);
|
|
21695
21957
|
}
|
|
21696
|
-
const bundled =
|
|
21958
|
+
const bundled = join16(dirname9(opts.here), "event-worker.js");
|
|
21697
21959
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21698
21960
|
const dir = walkFor(opts.here, "event-worker", exists);
|
|
21699
21961
|
if (dir) return checkoutWorker(dir);
|
|
@@ -21702,7 +21964,7 @@ function resolveEventWorker(opts) {
|
|
|
21702
21964
|
function resolveMcpServer(opts) {
|
|
21703
21965
|
const execPath = opts.execPath ?? process.execPath;
|
|
21704
21966
|
const exists = opts.exists ?? existsSync9;
|
|
21705
|
-
const bundled =
|
|
21967
|
+
const bundled = join16(dirname9(opts.here), "mcp.js");
|
|
21706
21968
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21707
21969
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
21708
21970
|
if (dir) return checkoutMcp(dir, execPath);
|
|
@@ -21722,7 +21984,19 @@ function describeState(state, alive = isAlive) {
|
|
|
21722
21984
|
const lastExit = a.lastExit ? a.lastExit.signal ? `signal ${a.lastExit.signal}` : a.lastExit.error ?? `code ${a.lastExit.code}` : null;
|
|
21723
21985
|
return { name, status, pid: a.pid, restarts: a.restarts, lastExit };
|
|
21724
21986
|
});
|
|
21725
|
-
|
|
21987
|
+
const byName = new Map(agents.map((a) => [a.name, a]));
|
|
21988
|
+
const pools = Object.entries(state.pools ?? {}).map(([name, p]) => ({
|
|
21989
|
+
name,
|
|
21990
|
+
members: p.members,
|
|
21991
|
+
min: p.min,
|
|
21992
|
+
max: p.max,
|
|
21993
|
+
ready: p.ready,
|
|
21994
|
+
checkedAt: p.checkedAt,
|
|
21995
|
+
desiredMax: p.desiredMax ?? null,
|
|
21996
|
+
error: p.error ?? null,
|
|
21997
|
+
running: p.members.filter((m) => byName.get(m)?.status === "running").length
|
|
21998
|
+
}));
|
|
21999
|
+
return { supervised, agents, pools };
|
|
21726
22000
|
}
|
|
21727
22001
|
function orphanedWorkers(state, alive, inspect) {
|
|
21728
22002
|
if (alive(state.supervisorPid)) return [];
|
|
@@ -21798,18 +22072,183 @@ function loadPruneTargets(names) {
|
|
|
21798
22072
|
});
|
|
21799
22073
|
}
|
|
21800
22074
|
|
|
22075
|
+
// src/node/pool.ts
|
|
22076
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
22077
|
+
import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as mkdirSync7, realpathSync as realpathSync4 } from "node:fs";
|
|
22078
|
+
import { basename as basename2, dirname as dirname10, join as join17, resolve as resolve6 } from "node:path";
|
|
22079
|
+
var IDLE_STOP_MS = 10 * 6e4;
|
|
22080
|
+
var RECONCILE_MS = 15e3;
|
|
22081
|
+
var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
|
|
22082
|
+
var REMARK_AFTER_MS = 24 * 60 * 6e4;
|
|
22083
|
+
function positiveMs(raw, fallback2) {
|
|
22084
|
+
const n = Number(raw);
|
|
22085
|
+
return raw && Number.isInteger(n) && n > 0 ? n : fallback2;
|
|
22086
|
+
}
|
|
22087
|
+
function poolTimings(env2 = process.env) {
|
|
22088
|
+
return {
|
|
22089
|
+
reconcileMs: positiveMs(env2.PITBOSS_POOL_RECONCILE_MS, RECONCILE_MS),
|
|
22090
|
+
idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
|
|
22091
|
+
};
|
|
22092
|
+
}
|
|
22093
|
+
function effectiveBounds(spec, desired) {
|
|
22094
|
+
if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
|
|
22095
|
+
const max = Math.min(desired.max, spec.max);
|
|
22096
|
+
return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
|
|
22097
|
+
}
|
|
22098
|
+
function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
22099
|
+
const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
|
|
22100
|
+
const running = members.filter((m) => m.running);
|
|
22101
|
+
const stopped = members.filter((m) => !m.running);
|
|
22102
|
+
let count = running.length;
|
|
22103
|
+
const start = [];
|
|
22104
|
+
const take = (m) => {
|
|
22105
|
+
start.push(m.name);
|
|
22106
|
+
count++;
|
|
22107
|
+
};
|
|
22108
|
+
for (const m of stopped) {
|
|
22109
|
+
if (count >= max) break;
|
|
22110
|
+
if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
|
|
22111
|
+
}
|
|
22112
|
+
const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
|
|
22113
|
+
let demand = pending - running.filter(isIdle).length;
|
|
22114
|
+
for (const m of spare()) {
|
|
22115
|
+
if (demand <= 0 || count >= max) break;
|
|
22116
|
+
take(m);
|
|
22117
|
+
demand--;
|
|
22118
|
+
}
|
|
22119
|
+
for (const m of spare()) {
|
|
22120
|
+
if (count >= min || count >= max) break;
|
|
22121
|
+
take(m);
|
|
22122
|
+
}
|
|
22123
|
+
if (start.length > 0) return { start, stop: [] };
|
|
22124
|
+
const stop = [];
|
|
22125
|
+
let surplus = running.filter(isIdle).length - pending;
|
|
22126
|
+
const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
|
|
22127
|
+
for (const m of eligible) {
|
|
22128
|
+
if (surplus <= 0 || count <= min) break;
|
|
22129
|
+
stop.push(m.name);
|
|
22130
|
+
count--;
|
|
22131
|
+
surplus--;
|
|
22132
|
+
}
|
|
22133
|
+
return { start, stop };
|
|
22134
|
+
}
|
|
22135
|
+
function summarizeTasks(ready, held, members) {
|
|
22136
|
+
const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
|
|
22137
|
+
const counts = Object.fromEntries(
|
|
22138
|
+
members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
|
|
22139
|
+
);
|
|
22140
|
+
const pending = ready.filter((t) => t.status === "pending" && t.autoAssign).length;
|
|
22141
|
+
let readyAssigned = 0;
|
|
22142
|
+
for (const t of ready) {
|
|
22143
|
+
const name = t.status === "assigned" && t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22144
|
+
if (!name) continue;
|
|
22145
|
+
counts[name].readyAssigned++;
|
|
22146
|
+
readyAssigned++;
|
|
22147
|
+
}
|
|
22148
|
+
for (const t of held) {
|
|
22149
|
+
const name = t.assignedTo ? nameOf.get(t.assignedTo) : void 0;
|
|
22150
|
+
if (!name) continue;
|
|
22151
|
+
if (t.status === "assigned") counts[name].assigned++;
|
|
22152
|
+
if (t.status === "in_progress") counts[name].inProgress++;
|
|
22153
|
+
}
|
|
22154
|
+
return { pending, ready: pending + readyAssigned, members: counts };
|
|
22155
|
+
}
|
|
22156
|
+
function poolRootProblem(root, protectedPaths) {
|
|
22157
|
+
for (const p of protectedPaths) {
|
|
22158
|
+
if (isWithin(root, p.path)) return `worktreeRoot ${root} is inside ${p.label} ${p.path}; pool worktrees never go inside a checkout`;
|
|
22159
|
+
}
|
|
22160
|
+
return null;
|
|
22161
|
+
}
|
|
22162
|
+
function memberPathProblem(s) {
|
|
22163
|
+
if (s.symlink) return `${s.path} is a symlink; refusing to use it as a pool worktree`;
|
|
22164
|
+
if (s.exists && s.commonDir !== s.baseCommonDir) return `${s.path} exists and is not a worktree of the pool's base clone`;
|
|
22165
|
+
if (!s.exists && s.tracked) return `${s.path} is tracked by git; refusing to create a pool worktree there`;
|
|
22166
|
+
return null;
|
|
22167
|
+
}
|
|
22168
|
+
function realpathAllowMissing(path3) {
|
|
22169
|
+
const abs = resolve6(path3);
|
|
22170
|
+
if (existsSync10(abs)) return realpathSync4.native(abs);
|
|
22171
|
+
return join17(realpathAllowMissing(dirname10(abs)), basename2(abs));
|
|
22172
|
+
}
|
|
22173
|
+
function run2(cwd, args) {
|
|
22174
|
+
try {
|
|
22175
|
+
return execFileSync6("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
22176
|
+
} catch (err) {
|
|
22177
|
+
const stderr = err.stderr?.toString().trim();
|
|
22178
|
+
throw new Error(`git ${args.join(" ")} in ${cwd} failed${stderr ? `: ${stderr}` : ""}`);
|
|
22179
|
+
}
|
|
22180
|
+
}
|
|
22181
|
+
var commonDir = (cwd) => {
|
|
22182
|
+
const dir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
|
|
22183
|
+
return dir ? realpathSync4.native(dir) : null;
|
|
22184
|
+
};
|
|
22185
|
+
function defaultBranch(cwd) {
|
|
22186
|
+
const head = () => git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
22187
|
+
let ref = head();
|
|
22188
|
+
if (!ref) {
|
|
22189
|
+
git(["remote", "set-head", "origin", "--auto"], cwd);
|
|
22190
|
+
ref = head();
|
|
22191
|
+
}
|
|
22192
|
+
if (!ref || !ref.startsWith("origin/")) throw new Error(`${cwd}: cannot tell origin's default branch; run git remote set-head origin --auto`);
|
|
22193
|
+
return ref.slice("origin/".length);
|
|
22194
|
+
}
|
|
22195
|
+
function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
22196
|
+
if (!existsSync10(pool.base)) throw new Error(`pool ${pool.pool}: base ${pool.base} does not exist`);
|
|
22197
|
+
const baseTop = git(["rev-parse", "--show-toplevel"], pool.base);
|
|
22198
|
+
const baseCommon = commonDir(pool.base);
|
|
22199
|
+
if (!baseTop || !baseCommon) throw new Error(`pool ${pool.pool}: base ${pool.base} is not a git clone`);
|
|
22200
|
+
const root = realpathAllowMissing(pool.worktreeRoot);
|
|
22201
|
+
const rootProblem = poolRootProblem(root, [{ label: "base", path: realpathSync4.native(baseTop) }, ...otherWorktrees]);
|
|
22202
|
+
if (rootProblem) throw new Error(`pool ${pool.pool}: ${rootProblem}`);
|
|
22203
|
+
const members = poolMembers({ ...pool, worktreeRoot: root });
|
|
22204
|
+
for (const m of members) {
|
|
22205
|
+
let symlink = false;
|
|
22206
|
+
try {
|
|
22207
|
+
symlink = lstatSync3(m.worktree).isSymbolicLink();
|
|
22208
|
+
} catch {
|
|
22209
|
+
symlink = false;
|
|
22210
|
+
}
|
|
22211
|
+
const exists = !symlink && existsSync10(m.worktree);
|
|
22212
|
+
const problem = memberPathProblem({
|
|
22213
|
+
path: m.worktree,
|
|
22214
|
+
symlink,
|
|
22215
|
+
exists,
|
|
22216
|
+
baseCommonDir: baseCommon,
|
|
22217
|
+
commonDir: exists ? commonDir(m.worktree) : null,
|
|
22218
|
+
tracked: !exists && !symlink && isTracked(m.worktree)
|
|
22219
|
+
});
|
|
22220
|
+
if (problem) throw new Error(`pool ${pool.pool}: ${problem}`);
|
|
22221
|
+
}
|
|
22222
|
+
const missing = members.filter((m) => !existsSync10(m.worktree));
|
|
22223
|
+
if (missing.length > 0) {
|
|
22224
|
+
mkdirSync7(root, { recursive: true });
|
|
22225
|
+
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22226
|
+
const branch = defaultBranch(pool.base);
|
|
22227
|
+
run2(pool.base, ["worktree", "prune"]);
|
|
22228
|
+
for (const m of missing) run2(pool.base, ["worktree", "add", "-q", "--detach", m.worktree, `origin/${branch}`]);
|
|
22229
|
+
}
|
|
22230
|
+
return members.map((m) => realpathSync4.native(m.worktree));
|
|
22231
|
+
}
|
|
22232
|
+
function resetWorktree(worktree) {
|
|
22233
|
+
run2(worktree, ["fetch", "-q", "--prune", "origin"]);
|
|
22234
|
+
const target = `origin/${defaultBranch(worktree)}`;
|
|
22235
|
+
run2(worktree, ["checkout", "-q", "--detach", "--force", target]);
|
|
22236
|
+
run2(worktree, ["reset", "-q", "--hard", target]);
|
|
22237
|
+
run2(worktree, ["clean", "-fdq"]);
|
|
22238
|
+
}
|
|
22239
|
+
|
|
21801
22240
|
// src/commands/node.ts
|
|
21802
22241
|
var HERE = fileURLToPath3(import.meta.url);
|
|
21803
22242
|
function inspectProcess(pid) {
|
|
21804
22243
|
try {
|
|
21805
|
-
const out =
|
|
22244
|
+
const out = execFileSync7("ps", ["-o", "pgid=,command=", "-p", String(pid)], { encoding: "utf-8" }).trim();
|
|
21806
22245
|
const match = /^(\d+)\s+(.*)$/.exec(out);
|
|
21807
22246
|
return match ? { pgid: Number(match[1]), command: match[2] } : null;
|
|
21808
22247
|
} catch {
|
|
21809
22248
|
return null;
|
|
21810
22249
|
}
|
|
21811
22250
|
}
|
|
21812
|
-
function childSpec(agent, creds, worker) {
|
|
22251
|
+
function childSpec(agent, creds, worker, onDemand = false) {
|
|
21813
22252
|
const { ANTHROPIC_API_KEY: _key, ANTHROPIC_AUTH_TOKEN: _tok, ...inherited } = process.env;
|
|
21814
22253
|
return {
|
|
21815
22254
|
name: agent.name,
|
|
@@ -21817,6 +22256,7 @@ function childSpec(agent, creds, worker) {
|
|
|
21817
22256
|
args: worker.args,
|
|
21818
22257
|
cwd: agent.worktree,
|
|
21819
22258
|
logPath: logPath(agent.name),
|
|
22259
|
+
onDemand,
|
|
21820
22260
|
env: {
|
|
21821
22261
|
...inherited,
|
|
21822
22262
|
API_URL: creds.api,
|
|
@@ -21840,13 +22280,13 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
21840
22280
|
writeMergedJson(target, (existing) => mergeMcpServer(existing, name, entry), root);
|
|
21841
22281
|
excludeIfUntracked(root, target);
|
|
21842
22282
|
},
|
|
21843
|
-
run(
|
|
21844
|
-
|
|
22283
|
+
run(bin3, args, cwd) {
|
|
22284
|
+
execFileSync7(bin3, args, { cwd, stdio: "ignore" });
|
|
21845
22285
|
}
|
|
21846
22286
|
});
|
|
21847
22287
|
}
|
|
21848
22288
|
function spawnWorker(spec) {
|
|
21849
|
-
|
|
22289
|
+
mkdirSync8(dirname11(spec.logPath), { recursive: true, mode: 448 });
|
|
21850
22290
|
const log = openSync(spec.logPath, "a", 384);
|
|
21851
22291
|
try {
|
|
21852
22292
|
const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
|
|
@@ -21865,19 +22305,138 @@ function spawnWorker(spec) {
|
|
|
21865
22305
|
closeSync(log);
|
|
21866
22306
|
}
|
|
21867
22307
|
}
|
|
22308
|
+
async function fetchTasks(creds, query) {
|
|
22309
|
+
const res = await getJson(`${creds.api}/tasks?repoId=${encodeURIComponent(creds.repoId)}&${query}`, creds.token);
|
|
22310
|
+
if (res.status !== 200 || !Array.isArray(res.payload.data)) throw new Error(`GET /tasks?${query} returned HTTP ${res.status}`);
|
|
22311
|
+
return res.payload.data;
|
|
22312
|
+
}
|
|
22313
|
+
async function fetchPoolSize(creds, name) {
|
|
22314
|
+
const res = await getJson(`${creds.api}/repos/${encodeURIComponent(creds.repoId)}/pools`, creds.token);
|
|
22315
|
+
if (res.status !== 200 || !Array.isArray(res.payload.data)) return void 0;
|
|
22316
|
+
const row = res.payload.data.find((p) => p.name === name);
|
|
22317
|
+
return row ? { min: row.min, max: row.max } : void 0;
|
|
22318
|
+
}
|
|
22319
|
+
async function setAvailable(creds, available) {
|
|
22320
|
+
const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason: POOL_STOPPED_REASON };
|
|
22321
|
+
const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/unavailable`, {
|
|
22322
|
+
method: "PUT",
|
|
22323
|
+
headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
|
|
22324
|
+
body: JSON.stringify(body)
|
|
22325
|
+
});
|
|
22326
|
+
if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/unavailable returned HTTP ${res.status}`);
|
|
22327
|
+
}
|
|
22328
|
+
async function reportScale(creds, event) {
|
|
22329
|
+
const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/pool-events`, {
|
|
22330
|
+
method: "POST",
|
|
22331
|
+
headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
|
|
22332
|
+
body: JSON.stringify(event)
|
|
22333
|
+
});
|
|
22334
|
+
if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
|
|
22335
|
+
}
|
|
22336
|
+
function startPoolLoop(run3, sup, record) {
|
|
22337
|
+
const { pool, members } = run3;
|
|
22338
|
+
const { reconcileMs, idleStopMs } = poolTimings();
|
|
22339
|
+
const lastBusy = /* @__PURE__ */ new Map();
|
|
22340
|
+
const markedAt = /* @__PURE__ */ new Map();
|
|
22341
|
+
const state = { members: members.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null };
|
|
22342
|
+
const log = (line) => console.log(`${(/* @__PURE__ */ new Date()).toISOString()} pool ${pool.pool}: ${line}`);
|
|
22343
|
+
let busy = false;
|
|
22344
|
+
const markStopped = async (member) => {
|
|
22345
|
+
try {
|
|
22346
|
+
await setAvailable(member.creds, false);
|
|
22347
|
+
markedAt.set(member.agent.name, Date.now());
|
|
22348
|
+
} catch (err) {
|
|
22349
|
+
log(`could not mark ${member.agent.name} unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
22350
|
+
}
|
|
22351
|
+
};
|
|
22352
|
+
const report = async (member, action, reason, ready) => {
|
|
22353
|
+
const running = members.filter((m) => sup.isActive(m.agent.name)).length;
|
|
22354
|
+
try {
|
|
22355
|
+
await reportScale(member.creds, { pool: pool.pool, action, reason, ready, running, max: pool.max });
|
|
22356
|
+
} catch (err) {
|
|
22357
|
+
log(`could not report ${action} ${member.agent.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
22358
|
+
}
|
|
22359
|
+
};
|
|
22360
|
+
const tick = async () => {
|
|
22361
|
+
if (busy) return;
|
|
22362
|
+
busy = true;
|
|
22363
|
+
try {
|
|
22364
|
+
for (const m of members) {
|
|
22365
|
+
if (!sup.isActive(m.agent.name) && Date.now() - (markedAt.get(m.agent.name) ?? 0) >= REMARK_AFTER_MS) await markStopped(m);
|
|
22366
|
+
}
|
|
22367
|
+
const reader = members.find((m) => sup.isActive(m.agent.name)) ?? members[0];
|
|
22368
|
+
const [ready, held] = await Promise.all([
|
|
22369
|
+
fetchTasks(reader.creds, "ready=true"),
|
|
22370
|
+
fetchTasks(reader.creds, "status=assigned,in_progress")
|
|
22371
|
+
]);
|
|
22372
|
+
const summary = summarizeTasks(ready, held, members.map((m) => ({ name: m.agent.name, agentId: m.creds.agentId })));
|
|
22373
|
+
const now = Date.now();
|
|
22374
|
+
const views = members.map(({ agent }) => {
|
|
22375
|
+
const counts = summary.members[agent.name];
|
|
22376
|
+
if (counts.assigned + counts.inProgress > 0) lastBusy.set(agent.name, now);
|
|
22377
|
+
return { name: agent.name, running: sup.isActive(agent.name), ...counts, idleMs: now - (lastBusy.get(agent.name) ?? now) };
|
|
22378
|
+
});
|
|
22379
|
+
const bounds = effectiveBounds(pool, await fetchPoolSize(reader.creds, pool.pool));
|
|
22380
|
+
if (bounds.max !== state.max || bounds.min !== state.min) {
|
|
22381
|
+
log(`size min ${bounds.min} max ${bounds.max}${bounds.capped ? ` (API asks for ${bounds.desiredMax}, capped at the ${pool.max} provisioned members)` : ""}`);
|
|
22382
|
+
}
|
|
22383
|
+
Object.assign(state, { min: bounds.min, max: bounds.max, desiredMax: bounds.desiredMax });
|
|
22384
|
+
const decision = reconcilePool({ pending: summary.pending, members: views, min: bounds.min, max: bounds.max, idleStopMs });
|
|
22385
|
+
for (const name of decision.start) {
|
|
22386
|
+
const member = members.find((m) => m.agent.name === name);
|
|
22387
|
+
try {
|
|
22388
|
+
if (summary.members[name].inProgress === 0) resetWorktree(member.agent.worktree);
|
|
22389
|
+
await setAvailable(member.creds, true);
|
|
22390
|
+
markedAt.delete(name);
|
|
22391
|
+
} catch (err) {
|
|
22392
|
+
log(`not starting ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
22393
|
+
continue;
|
|
22394
|
+
}
|
|
22395
|
+
lastBusy.set(name, Date.now());
|
|
22396
|
+
sup.startAgent(name);
|
|
22397
|
+
log(`started ${name} (ready ${summary.ready})`);
|
|
22398
|
+
await report(member, "started", `ready ${summary.ready}`, summary.ready);
|
|
22399
|
+
}
|
|
22400
|
+
for (const name of decision.stop) {
|
|
22401
|
+
const member = members.find((m) => m.agent.name === name);
|
|
22402
|
+
await sup.stopAgent(name);
|
|
22403
|
+
await markStopped(member);
|
|
22404
|
+
log(`stopped ${name} after ${Math.round(idleStopMs / 1e3)}s idle`);
|
|
22405
|
+
await report(member, "stopped", `idle ${Math.round(idleStopMs / 1e3)}s`, summary.ready);
|
|
22406
|
+
}
|
|
22407
|
+
Object.assign(state, { ready: summary.ready, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: void 0 });
|
|
22408
|
+
} catch (err) {
|
|
22409
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22410
|
+
if (state.error !== message) log(message);
|
|
22411
|
+
Object.assign(state, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: message });
|
|
22412
|
+
} finally {
|
|
22413
|
+
record(pool.pool, { ...state });
|
|
22414
|
+
busy = false;
|
|
22415
|
+
}
|
|
22416
|
+
};
|
|
22417
|
+
void tick();
|
|
22418
|
+
const timer = setInterval(() => void tick(), reconcileMs);
|
|
22419
|
+
return () => clearInterval(timer);
|
|
22420
|
+
}
|
|
21868
22421
|
async function nodeUpCommand(specFile, opts = {}) {
|
|
21869
22422
|
try {
|
|
21870
|
-
const specPath =
|
|
21871
|
-
const specRealpath =
|
|
21872
|
-
const parsed = parseSpec(readFileSync12(specPath, "utf-8"),
|
|
22423
|
+
const specPath = resolve7(specFile);
|
|
22424
|
+
const specRealpath = realpathSync5.native(specPath);
|
|
22425
|
+
const parsed = parseSpec(readFileSync12(specPath, "utf-8"), dirname11(specPath));
|
|
21873
22426
|
const previous = readState();
|
|
21874
22427
|
if (previous && previous.supervisorPid !== process.pid && isAlive(previous.supervisorPid)) {
|
|
21875
22428
|
throw new Error(`a node is already running (pid ${previous.supervisorPid}, ${previous.specPath}). Stop it first.`);
|
|
21876
22429
|
}
|
|
21877
22430
|
for (const a of parsed.agents) {
|
|
21878
|
-
if (!
|
|
22431
|
+
if (!existsSync11(a.worktree)) throw new Error(`${a.name}: worktree ${a.worktree} does not exist`);
|
|
21879
22432
|
}
|
|
21880
|
-
const
|
|
22433
|
+
const plain = parsed.agents.map((a) => ({ ...a, worktree: realpathSync5.native(a.worktree) }));
|
|
22434
|
+
const pools = parsed.pools.map((pool) => {
|
|
22435
|
+
const paths = ensurePoolWorktrees(pool, plain.map((a) => ({ label: `agent ${a.name}'s worktree`, path: a.worktree })));
|
|
22436
|
+
return { pool, members: poolMembers(pool).map((m, i) => ({ ...m, worktree: paths[i] })) };
|
|
22437
|
+
});
|
|
22438
|
+
const spec = { ...parsed, agents: [...plain, ...pools.flatMap((p) => p.members)] };
|
|
22439
|
+
const pooled = new Set(pools.flatMap((p) => p.members.map((m) => m.name)));
|
|
21881
22440
|
const seen = /* @__PURE__ */ new Map();
|
|
21882
22441
|
for (const a of spec.agents) {
|
|
21883
22442
|
const other = seen.get(a.worktree);
|
|
@@ -21894,6 +22453,7 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
21894
22453
|
}
|
|
21895
22454
|
}
|
|
21896
22455
|
}
|
|
22456
|
+
const poolStates = {};
|
|
21897
22457
|
writeState({ ...base2, agents: {} });
|
|
21898
22458
|
logStoredOrphans(spec.agents.map((a) => a.name), specRealpath);
|
|
21899
22459
|
const worker = resolveEventWorker({ here: HERE });
|
|
@@ -21904,13 +22464,13 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
21904
22464
|
const mcp = resolveMcpServer({ here: HERE });
|
|
21905
22465
|
for (const a of runnable) prepareAgent(a, creds.get(a.name), mcp);
|
|
21906
22466
|
const last = /* @__PURE__ */ new Map();
|
|
21907
|
-
const sup = new Supervisor(runnable.map((a) => childSpec(a, creds.get(a.name), worker)), {
|
|
22467
|
+
const sup = new Supervisor(runnable.map((a) => childSpec(a, creds.get(a.name), worker, pooled.has(a.name))), {
|
|
21908
22468
|
spawn: spawnWorker,
|
|
21909
22469
|
baseMs: 1e3,
|
|
21910
22470
|
maxMs: 6e4,
|
|
21911
22471
|
stableMs: 6e4,
|
|
21912
22472
|
onChange: (agents) => {
|
|
21913
|
-
writeState({ ...base2, agents });
|
|
22473
|
+
writeState({ ...base2, agents, pools: poolStates });
|
|
21914
22474
|
for (const [name, s] of Object.entries(agents)) {
|
|
21915
22475
|
const line = s.status === "running" ? `running pid ${s.pid}` : s.status === "backoff" ? `exited (${s.lastExit?.signal ?? s.lastExit?.error ?? `code ${s.lastExit?.code}`}), restart ${s.restarts} at ${s.nextStartAt}` : s.status;
|
|
21916
22476
|
if (last.get(name) === line) continue;
|
|
@@ -21919,13 +22479,17 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
21919
22479
|
}
|
|
21920
22480
|
}
|
|
21921
22481
|
});
|
|
22482
|
+
const plainCount = runnable.filter((a) => !pooled.has(a.name)).length;
|
|
22483
|
+
const poolNote = pools.length > 0 ? `, ${pools.length} pool(s)` : "";
|
|
21922
22484
|
console.log(source_default.bold(`
|
|
21923
|
-
pitboss node: ${
|
|
22485
|
+
pitboss node: ${plainCount} agent(s)${poolNote}`) + source_default.dim(` state ${statePath2()}
|
|
21924
22486
|
`));
|
|
22487
|
+
const stopLoops = [];
|
|
21925
22488
|
let stopping = false;
|
|
21926
22489
|
const shutdown = async () => {
|
|
21927
22490
|
if (stopping) return;
|
|
21928
22491
|
stopping = true;
|
|
22492
|
+
for (const stopLoop of stopLoops) stopLoop();
|
|
21929
22493
|
console.log(source_default.dim("\n stopping agents\u2026"));
|
|
21930
22494
|
await sup.stop();
|
|
21931
22495
|
process.exit(0);
|
|
@@ -21934,6 +22498,18 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
21934
22498
|
process.on("SIGTERM", shutdown);
|
|
21935
22499
|
process.on("SIGHUP", shutdown);
|
|
21936
22500
|
sup.start();
|
|
22501
|
+
for (const { pool, members } of pools) {
|
|
22502
|
+
const ready = members.filter((m) => creds.has(m.name)).map((agent) => ({ agent, creds: creds.get(agent.name) }));
|
|
22503
|
+
if (ready.length === 0) {
|
|
22504
|
+
console.log(source_default.yellow(` ! pool ${pool.pool}: no member has credentials; it will not scale`));
|
|
22505
|
+
continue;
|
|
22506
|
+
}
|
|
22507
|
+
poolStates[pool.pool] = { members: ready.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null };
|
|
22508
|
+
stopLoops.push(startPoolLoop({ pool, members: ready }, sup, (name, state) => {
|
|
22509
|
+
poolStates[name] = state;
|
|
22510
|
+
writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
|
|
22511
|
+
}));
|
|
22512
|
+
}
|
|
21937
22513
|
} catch (err) {
|
|
21938
22514
|
console.error(source_default.red(`
|
|
21939
22515
|
${err instanceof Error ? err.message : String(err)}`));
|
|
@@ -21942,10 +22518,10 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
21942
22518
|
}
|
|
21943
22519
|
async function nodePruneCommand(specFile, opts = {}) {
|
|
21944
22520
|
try {
|
|
21945
|
-
const specPath =
|
|
21946
|
-
const specRealpath =
|
|
21947
|
-
const spec = parseSpec(readFileSync12(specPath, "utf-8"),
|
|
21948
|
-
const { orphans: names, unattributed } = classifyStored(spec.
|
|
22521
|
+
const specPath = resolve7(specFile);
|
|
22522
|
+
const specRealpath = realpathSync5.native(specPath);
|
|
22523
|
+
const spec = parseSpec(readFileSync12(specPath, "utf-8"), dirname11(specPath));
|
|
22524
|
+
const { orphans: names, unattributed } = classifyStored(allAgents(spec).map((a) => a.name), specRealpath);
|
|
21949
22525
|
if (names.length === 0) {
|
|
21950
22526
|
console.log("No stored credentials are missing from the spec.");
|
|
21951
22527
|
if (unattributed.length > 0) console.log(`unattributed, not pruned: ${unattributed.join(", ")}`);
|
|
@@ -21989,9 +22565,9 @@ function nodeStatusCommand(opts) {
|
|
|
21989
22565
|
console.log("No node has run on this machine.");
|
|
21990
22566
|
return;
|
|
21991
22567
|
}
|
|
21992
|
-
const { supervised, agents } = describeState(state);
|
|
22568
|
+
const { supervised, agents, pools } = describeState(state);
|
|
21993
22569
|
if (opts.json) {
|
|
21994
|
-
console.log(JSON.stringify({ supervisorPid: state.supervisorPid, supervised, specPath: state.specPath, agents }, null, 2));
|
|
22570
|
+
console.log(JSON.stringify({ supervisorPid: state.supervisorPid, supervised, specPath: state.specPath, agents, pools }, null, 2));
|
|
21995
22571
|
return;
|
|
21996
22572
|
}
|
|
21997
22573
|
console.log(`${supervised ? source_default.green("supervisor up") : source_default.red("supervisor down")} pid ${state.supervisorPid} ${source_default.dim(state.specPath)}`);
|
|
@@ -21999,6 +22575,10 @@ function nodeStatusCommand(opts) {
|
|
|
21999
22575
|
const color = a.status === "running" ? source_default.green : a.status === "backoff" ? source_default.yellow : source_default.red;
|
|
22000
22576
|
console.log(` ${a.name.padEnd(16)} ${color(a.status.padEnd(12))} pid ${String(a.pid ?? "-").padEnd(7)} restarts ${a.restarts}${a.lastExit ? source_default.dim(` last exit ${a.lastExit}`) : ""}`);
|
|
22001
22577
|
}
|
|
22578
|
+
for (const p of pools) {
|
|
22579
|
+
const ready = p.ready === null ? "-" : String(p.ready);
|
|
22580
|
+
console.log(` pool ${p.name.padEnd(11)} running ${p.running}/${p.max} (min ${p.min})${p.desiredMax !== null && p.desiredMax > p.max ? source_default.yellow(` desired ${p.desiredMax}, capped`) : ""} ready ${ready} members ${p.members.join(", ")}${p.error ? source_default.red(` ${p.error}`) : ""}`);
|
|
22581
|
+
}
|
|
22002
22582
|
}
|
|
22003
22583
|
|
|
22004
22584
|
// src/index.ts
|
|
@@ -22018,7 +22598,7 @@ hook.command("cursor-stop").description("Cursor stop hook").option("--dir <path>
|
|
|
22018
22598
|
hook.command("cursor-prompt").description("Cursor beforeSubmitPrompt hook").option("--dir <path>", "The agent's folder").action(hookCursorPromptCommand);
|
|
22019
22599
|
program2.command("tasks").description("List tasks assigned to you (default: assigned + in_progress)").option("-a, --all", "Show all tasks in the project, not just yours").option("-s, --status <status>", "Filter by status (comma-separated: pending,assigned,in_progress,pending_verification,completed,blocked)").action(tasksCommand);
|
|
22020
22600
|
var task = program2.command("task").description("Create or update a task");
|
|
22021
|
-
task.command("create").description("Create a new task").option("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("-p, --priority <priority>", "low|normal|high|urgent").option("--to <agent>", "Assign to an agent (id or name)").option("--domains <list>", "Comma-separated domain tags").option("--specialization <s>", "Required specialization").option("-v, --verify <cmd>", "Shell command that must exit 0 to gate the `completed` transition (API must set ALLOW_SHELL_VERIFY=1)").option("--verify-cwd <path>", "Working directory for --verify (defaults to API server cwd)").option("--verify-kind <kind>", "Verifier kind: shell|file_exists|thread_concluded|reviewer_agent|human_approval").option("--verify-path <path>", "Path that must exist (--verify-kind file_exists)").option("--verify-thread <id>", "Thread that must be concluded (--verify-kind thread_concluded)").option("--verify-reviewer <agent>", "Agent (id or name) who must approve (--verify-kind reviewer_agent)").option("--review-by <agent>", "Shorthand for --verify-kind reviewer_agent --verify-reviewer <agent>").action(taskCreateCommand);
|
|
22601
|
+
task.command("create").description("Create a new task").option("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("-p, --priority <priority>", "low|normal|high|urgent").option("--to <agent>", "Assign to an agent (id or name)").option("--domains <list>", "Comma-separated domain tags").option("--specialization <s>", "Required specialization").option("-v, --verify <cmd>", "Shell command that must exit 0 to gate the `completed` transition (API must set ALLOW_SHELL_VERIFY=1)").option("--verify-cwd <path>", "Working directory for --verify (defaults to API server cwd)").option("--verify-kind <kind>", "Verifier kind: shell|file_exists|thread_concluded|reviewer_agent|human_approval|ci_passed").option("--verify-path <path>", "Path that must exist (--verify-kind file_exists) or commit sha (--verify-kind ci_passed)").option("--verify-thread <id>", "Thread that must be concluded (--verify-kind thread_concluded)").option("--verify-reviewer <agent>", "Agent (id or name) who must approve (--verify-kind reviewer_agent)").option("--review-by <agent>", "Shorthand for --verify-kind reviewer_agent --verify-reviewer <agent>").action(taskCreateCommand);
|
|
22022
22602
|
task.command("start <id>").description("Mark a task as in_progress").option("-n, --note <note>", "Optional note to attach").action((id, options) => taskUpdateCommand(id, "in_progress", options));
|
|
22023
22603
|
task.command("done <id>").description("Mark a task as completed").option("-n, --note <note>", "Optional note to attach").action((id, options) => taskUpdateCommand(id, "completed", options));
|
|
22024
22604
|
task.command("block <id>").description("Mark a task as blocked").option("-n, --note <note>", "Describe what's blocking you").action((id, options) => taskUpdateCommand(id, "blocked", options));
|