@pitboss/cli 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +355 -50
- package/dist/event-worker.js +277 -9
- package/dist/mcp.js +62 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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 bin4;
|
|
17467
17467
|
let rest;
|
|
17468
17468
|
if (editor.startsWith('"')) {
|
|
17469
17469
|
const closeQuote = editor.indexOf('"', 1);
|
|
17470
17470
|
if (closeQuote === -1) {
|
|
17471
|
-
|
|
17471
|
+
bin4 = editor.slice(1);
|
|
17472
17472
|
rest = "";
|
|
17473
17473
|
} else {
|
|
17474
|
-
|
|
17474
|
+
bin4 = 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
|
+
bin4 = editor;
|
|
17481
17481
|
rest = "";
|
|
17482
17482
|
} else {
|
|
17483
|
-
|
|
17483
|
+
bin4 = editor.substring(0, firstSpace);
|
|
17484
17484
|
rest = editor.substring(firstSpace + 1).trim();
|
|
17485
17485
|
}
|
|
17486
17486
|
}
|
|
17487
|
-
return { bin:
|
|
17487
|
+
return { bin: bin4, 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
|
|
@@ -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 POOL_STOPPED_REASON = "pool member stopped by pitboss node";
|
|
19110
19111
|
|
|
19111
19112
|
// src/lib/runtimes.ts
|
|
19112
19113
|
var RUNTIMES2 = RUNTIMES.map((r) => r.value);
|
|
@@ -20600,12 +20601,12 @@ function hookCursorPromptCommand(opts) {
|
|
|
20600
20601
|
|
|
20601
20602
|
// src/commands/node.ts
|
|
20602
20603
|
import { execFileSync as execFileSync7, spawn as spawnChild } from "node:child_process";
|
|
20603
|
-
import { closeSync, existsSync as existsSync11, mkdirSync as
|
|
20604
|
+
import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync12, realpathSync as realpathSync5 } from "node:fs";
|
|
20604
20605
|
import { dirname as dirname11, resolve as resolve7 } from "node:path";
|
|
20605
20606
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
20606
20607
|
|
|
20607
20608
|
// src/node/spec.ts
|
|
20608
|
-
import { join as
|
|
20609
|
+
import { join as join15, resolve as resolve4, sep as sep2 } from "node:path";
|
|
20609
20610
|
|
|
20610
20611
|
// ../claude-worker/src/session.ts
|
|
20611
20612
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -20882,7 +20883,11 @@ instructions below override your role rules and the branch rules above. Read the
|
|
|
20882
20883
|
Find the default branch with \`git remote show origin\`.
|
|
20883
20884
|
|
|
20884
20885
|
**orderRole "planning"** (you are the order's planner):
|
|
20885
|
-
1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it.
|
|
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
|
+
If the order has an \`issueUrl\`, read it with \`gh issue view <issueUrl> --comments\` and treat the
|
|
20889
|
+
issue as the spec; the order's \`feature\` text, when it says more than "Resolve the GitHub issue",
|
|
20890
|
+
narrows or adds to it. Put \`Closes #<n>\` (the issue number) in the PR body so the merge closes it.
|
|
20886
20891
|
2. Create the branch and PR:
|
|
20887
20892
|
\`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
|
|
20888
20893
|
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
|
|
@@ -20892,7 +20897,9 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20892
20897
|
\`epicId\`, \`assignedTo: "@auto"\`, and
|
|
20893
20898
|
\`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
|
|
20894
20899
|
\`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
|
|
20895
|
-
the router never hands out a task whose tag no agent matches.
|
|
20900
|
+
the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
|
|
20901
|
+
\`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
|
|
20902
|
+
Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
|
|
20896
20903
|
and independently testable so several agents can work at once. Use \`blockedBy\` only where
|
|
20897
20904
|
one task truly needs another's result. Each description is self-contained: what to change,
|
|
20898
20905
|
which files, how to test it, and the acceptance criteria.
|
|
@@ -20904,24 +20911,55 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20904
20911
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
20905
20912
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
20906
20913
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
20907
|
-
|
|
20914
|
+
If the order has \`pairBuilds\`, every build task needs \`verifyKind: "reviewer_agent"\` and a
|
|
20915
|
+
\`verifyReviewerId\` on a different \`workerType\` from the writers (see \`mcp__relai__list_agents\`);
|
|
20916
|
+
the API refuses a paired build task without one and routes it away from the reviewer's vendor.
|
|
20917
|
+
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
20918
|
+
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
20919
|
+
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
20920
|
+
the planning task's thread and complete it.
|
|
20921
|
+
|
|
20922
|
+
**orderRole "plan_review"** (assigned to the planner, reviewed by another agent):
|
|
20923
|
+
- After the task graph is written, complete each plan review task. That hands the plan to its
|
|
20924
|
+
reviewer. Every build task waits until all plan reviews are approved.
|
|
20925
|
+
- On a reject the task comes back to you with the reviewer's note: revise the plan under the Epic
|
|
20926
|
+
(edit, add or cancel tasks), reply on the task's thread with what changed, and complete it again.
|
|
20927
|
+
- As the reviewer of a plan review: read the goal and every task under its Epic. Approve when the
|
|
20928
|
+
tasks cover the goal, are small and independent, and their \`blockedBy\` order is right; otherwise
|
|
20929
|
+
reject with exactly what to change.
|
|
20908
20930
|
|
|
20909
20931
|
**orderRole "build"**:
|
|
20910
20932
|
- \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
|
|
20911
20933
|
test and commit. Do not create another branch or PR.
|
|
20912
20934
|
- Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
|
|
20913
20935
|
repeating up to 5 times if the push is rejected. Never force-push.
|
|
20936
|
+
- If you drop or change any part of what the goal asked for, even for a good reason, record each one
|
|
20937
|
+
with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
|
|
20938
|
+
"reason": "<why>" }]\` and say so in your handoff. The owner decides whether to accept it.
|
|
20914
20939
|
- Hand off on the task's thread with the commit sha and what you tested, then complete.
|
|
20915
20940
|
|
|
20916
20941
|
**orderRole "gate"**:
|
|
20917
20942
|
- Check every build task in the order's Epic is completed, then post the PR link on the task's
|
|
20918
|
-
thread and complete it. The gate decides from there: the owner approves a \`human_approval\`
|
|
20943
|
+
thread, listing every \`metadata.deviations\` entry from the build tasks, and complete it. The gate decides from there: the owner approves a \`human_approval\`
|
|
20919
20944
|
gate on the dashboard and the named reviewer decides a \`reviewer_agent\` gate.
|
|
20945
|
+
- As the reviewer of an order, check each thing the goal asked for. Anything dropped or changed is
|
|
20946
|
+
the owner's call, not yours: if it is recorded in \`metadata.deviations\`, mention it and judge
|
|
20947
|
+
the rest; if it is not recorded, reject and name it so the builder records it. The owner decides
|
|
20948
|
+
on deviations in the approval gate.
|
|
20920
20949
|
|
|
20921
20950
|
**orderRole "merge"**:
|
|
20922
20951
|
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
|
|
20952
|
+
Before merging, replace the PR body with a run report built from
|
|
20953
|
+
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
20954
|
+
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
20955
|
+
completed times, the number of distinct agents that worked on it, the total elapsed time
|
|
20956
|
+
from the planning task's creation, and the CI checks that ran. Keep any \`Closes #<n>\` line. Use
|
|
20957
|
+
\`gh pr edit <prUrl> --body-file <file>\`.
|
|
20923
20958
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
20924
|
-
gh pr view --json headRefOid
|
|
20959
|
+
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
20960
|
+
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
20961
|
+
\`gh pr edit <prUrl> --body-file <file>\` (a merged PR still accepts an edit). Post the merge commit
|
|
20962
|
+
on the thread, and complete.
|
|
20925
20963
|
3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
|
|
20926
20964
|
After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
|
|
20927
20965
|
function buildPrompt(config) {
|
|
@@ -21076,7 +21114,7 @@ function mcpServerLaunch(here = fileURLToPath2(import.meta.url), exists = exists
|
|
|
21076
21114
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21077
21115
|
return { command: execPath, args: [mcpServerTsxCli(), mcpServerEntry()] };
|
|
21078
21116
|
}
|
|
21079
|
-
function writeMcpConfig(config) {
|
|
21117
|
+
function writeMcpConfig(config, prefix = "claude-worker") {
|
|
21080
21118
|
const { command, args } = mcpServerLaunch();
|
|
21081
21119
|
const mcpConfig = {
|
|
21082
21120
|
mcpServers: {
|
|
@@ -21092,7 +21130,7 @@ function writeMcpConfig(config) {
|
|
|
21092
21130
|
}
|
|
21093
21131
|
}
|
|
21094
21132
|
};
|
|
21095
|
-
const path3 = join12(tmpdir(),
|
|
21133
|
+
const path3 = join12(tmpdir(), `${prefix}-${config.agentId}-${Date.now()}.json`);
|
|
21096
21134
|
writeFileSync6(path3, JSON.stringify(mcpConfig), { mode: 384 });
|
|
21097
21135
|
return path3;
|
|
21098
21136
|
}
|
|
@@ -21310,6 +21348,7 @@ async function runCursorSession(config) {
|
|
|
21310
21348
|
}
|
|
21311
21349
|
var cursorAdapter = {
|
|
21312
21350
|
runtime: "cursor",
|
|
21351
|
+
workerType: "cursor",
|
|
21313
21352
|
prepare(ctx) {
|
|
21314
21353
|
ctx.writeMcpServer(join13(ctx.worktree, ".cursor", "mcp.json"), "relai", {
|
|
21315
21354
|
command: ctx.mcp.command,
|
|
@@ -21325,15 +21364,245 @@ var cursorAdapter = {
|
|
|
21325
21364
|
// ../claude-worker/src/adapters/claude.ts
|
|
21326
21365
|
var claudeAdapter = {
|
|
21327
21366
|
runtime: "claude",
|
|
21367
|
+
workerType: "claude",
|
|
21328
21368
|
prepare() {
|
|
21329
21369
|
},
|
|
21330
21370
|
runSession: runClaudeSession,
|
|
21331
21371
|
classifyError: classifySessionError
|
|
21332
21372
|
};
|
|
21333
21373
|
|
|
21374
|
+
// ../claude-worker/src/adapters/copilot.ts
|
|
21375
|
+
import { spawn as spawn4 } from "child_process";
|
|
21376
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
21377
|
+
var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
|
|
21378
|
+
var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
|
|
21379
|
+
var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
|
|
21380
|
+
function copilotArgs(repoPath, prompt, mcpConfigPath) {
|
|
21381
|
+
return [
|
|
21382
|
+
"-p",
|
|
21383
|
+
prompt,
|
|
21384
|
+
"--allow-all-tools",
|
|
21385
|
+
"--output-format",
|
|
21386
|
+
"json",
|
|
21387
|
+
"--no-color",
|
|
21388
|
+
"-C",
|
|
21389
|
+
repoPath,
|
|
21390
|
+
"--additional-mcp-config",
|
|
21391
|
+
`@${mcpConfigPath}`,
|
|
21392
|
+
"--disable-builtin-mcps"
|
|
21393
|
+
];
|
|
21394
|
+
}
|
|
21395
|
+
function readCopilotEvent(line) {
|
|
21396
|
+
let event;
|
|
21397
|
+
try {
|
|
21398
|
+
event = JSON.parse(line);
|
|
21399
|
+
} catch {
|
|
21400
|
+
return {};
|
|
21401
|
+
}
|
|
21402
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
21403
|
+
const data = event.data ?? {};
|
|
21404
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
21405
|
+
if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
|
|
21406
|
+
if (type === "session.error") return { error: message || "copilot session error" };
|
|
21407
|
+
if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
|
|
21408
|
+
if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
|
|
21409
|
+
return {};
|
|
21410
|
+
}
|
|
21411
|
+
async function runCopilotSession(config) {
|
|
21412
|
+
const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
|
|
21413
|
+
const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath);
|
|
21414
|
+
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
21415
|
+
return new Promise((resolve8, reject) => {
|
|
21416
|
+
const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21417
|
+
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
21418
|
+
const toolsUsed = [];
|
|
21419
|
+
let buffer = "";
|
|
21420
|
+
let sessionError = "";
|
|
21421
|
+
let stderrOutput = "";
|
|
21422
|
+
proc.stdout.on("data", (chunk) => {
|
|
21423
|
+
buffer += chunk.toString();
|
|
21424
|
+
const lines = buffer.split("\n");
|
|
21425
|
+
buffer = lines.pop() ?? "";
|
|
21426
|
+
for (const line of lines) {
|
|
21427
|
+
const { tool, error } = readCopilotEvent(line);
|
|
21428
|
+
if (tool) {
|
|
21429
|
+
toolsUsed.push(tool);
|
|
21430
|
+
process.stdout.write(` \u2699 ${tool}
|
|
21431
|
+
`);
|
|
21432
|
+
}
|
|
21433
|
+
if (error) {
|
|
21434
|
+
sessionError = error;
|
|
21435
|
+
console.error(`[copilot-worker] Session failed: ${error}`);
|
|
21436
|
+
}
|
|
21437
|
+
}
|
|
21438
|
+
});
|
|
21439
|
+
proc.stderr.on("data", (chunk) => {
|
|
21440
|
+
stderrOutput += chunk.toString();
|
|
21441
|
+
});
|
|
21442
|
+
proc.on("close", (code, signal) => {
|
|
21443
|
+
try {
|
|
21444
|
+
unlinkSync2(mcpConfigPath);
|
|
21445
|
+
} catch {
|
|
21446
|
+
}
|
|
21447
|
+
console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
21448
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
21449
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
21450
|
+
reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21451
|
+
} else {
|
|
21452
|
+
resolve8([...new Set(toolsUsed)]);
|
|
21453
|
+
}
|
|
21454
|
+
});
|
|
21455
|
+
proc.on("error", (err) => {
|
|
21456
|
+
try {
|
|
21457
|
+
unlinkSync2(mcpConfigPath);
|
|
21458
|
+
} catch {
|
|
21459
|
+
}
|
|
21460
|
+
reject(err);
|
|
21461
|
+
});
|
|
21462
|
+
});
|
|
21463
|
+
}
|
|
21464
|
+
var copilotAdapter = {
|
|
21465
|
+
runtime: "copilot",
|
|
21466
|
+
workerType: "copilot",
|
|
21467
|
+
prepare() {
|
|
21468
|
+
},
|
|
21469
|
+
runSession: runCopilotSession,
|
|
21470
|
+
classifyError: (text) => {
|
|
21471
|
+
if (POLICY_BLOCKS.some((re) => re.test(text))) return "credentials";
|
|
21472
|
+
if (QUOTA.some((re) => re.test(text))) return "quota";
|
|
21473
|
+
return classifySessionError(text);
|
|
21474
|
+
}
|
|
21475
|
+
};
|
|
21476
|
+
|
|
21477
|
+
// ../claude-worker/src/adapters/codex.ts
|
|
21478
|
+
import { spawn as spawn5 } from "child_process";
|
|
21479
|
+
import { chmodSync, mkdirSync as mkdirSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
21480
|
+
import { homedir as homedir5 } from "os";
|
|
21481
|
+
import { join as join14 } from "path";
|
|
21482
|
+
var bin3 = () => process.env.CODEX_BIN ?? "codex";
|
|
21483
|
+
var codexHome = () => process.env.CODEX_HOME ?? join14(homedir5(), ".codex");
|
|
21484
|
+
var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
|
|
21485
|
+
var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
|
|
21486
|
+
function codexArgs(repoPath, prompt, profile) {
|
|
21487
|
+
return [
|
|
21488
|
+
"exec",
|
|
21489
|
+
"--json",
|
|
21490
|
+
"--ephemeral",
|
|
21491
|
+
"--skip-git-repo-check",
|
|
21492
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
21493
|
+
"-C",
|
|
21494
|
+
repoPath,
|
|
21495
|
+
"-p",
|
|
21496
|
+
profile,
|
|
21497
|
+
prompt
|
|
21498
|
+
];
|
|
21499
|
+
}
|
|
21500
|
+
var tomlString = (v) => JSON.stringify(v);
|
|
21501
|
+
function codexProfileToml(server, env2) {
|
|
21502
|
+
return [
|
|
21503
|
+
"[mcp_servers.relai]",
|
|
21504
|
+
`command = ${tomlString(server.command)}`,
|
|
21505
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
21506
|
+
"",
|
|
21507
|
+
"[mcp_servers.relai.env]",
|
|
21508
|
+
...Object.entries(env2).map(([k, v]) => `${k} = ${tomlString(v)}`),
|
|
21509
|
+
""
|
|
21510
|
+
].join("\n");
|
|
21511
|
+
}
|
|
21512
|
+
function readCodexEvent(line) {
|
|
21513
|
+
let event;
|
|
21514
|
+
try {
|
|
21515
|
+
event = JSON.parse(line);
|
|
21516
|
+
} catch {
|
|
21517
|
+
return {};
|
|
21518
|
+
}
|
|
21519
|
+
if (event.type === "item.started" && event.item) {
|
|
21520
|
+
if (event.item.type === "command_execution") return { tool: "shell" };
|
|
21521
|
+
if (event.item.type === "mcp_tool_call") return { tool: `${String(event.item.server ?? "mcp")}-${String(event.item.tool ?? "tool")}` };
|
|
21522
|
+
return {};
|
|
21523
|
+
}
|
|
21524
|
+
if (event.type === "turn.failed") return { error: String(event.error?.message ?? "codex turn failed") };
|
|
21525
|
+
if (event.type === "error") return { error: String(event.message ?? "codex error") };
|
|
21526
|
+
return {};
|
|
21527
|
+
}
|
|
21528
|
+
async function runCodexSession(config) {
|
|
21529
|
+
const profile = `pitboss-${config.agentId}`;
|
|
21530
|
+
mkdirSync6(codexHome(), { recursive: true, mode: 448 });
|
|
21531
|
+
const profilePath = join14(codexHome(), `${profile}.config.toml`);
|
|
21532
|
+
writeFileSync7(profilePath, codexProfileToml(mcpServerLaunch(), {
|
|
21533
|
+
API_URL: config.apiUrl,
|
|
21534
|
+
API_SECRET: config.apiSecret,
|
|
21535
|
+
AGENT_ID: config.agentId,
|
|
21536
|
+
REPO_ID: config.repoId
|
|
21537
|
+
}), { mode: 384 });
|
|
21538
|
+
chmodSync(profilePath, 384);
|
|
21539
|
+
const cleanup = () => {
|
|
21540
|
+
try {
|
|
21541
|
+
unlinkSync3(profilePath);
|
|
21542
|
+
} catch {
|
|
21543
|
+
}
|
|
21544
|
+
};
|
|
21545
|
+
const args = codexArgs(config.repoPath, buildPrompt(config), profile);
|
|
21546
|
+
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
21547
|
+
return new Promise((resolve8, reject) => {
|
|
21548
|
+
const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21549
|
+
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
21550
|
+
const toolsUsed = [];
|
|
21551
|
+
let buffer = "";
|
|
21552
|
+
let sessionError = "";
|
|
21553
|
+
let stderrOutput = "";
|
|
21554
|
+
proc.stdout.on("data", (chunk) => {
|
|
21555
|
+
buffer += chunk.toString();
|
|
21556
|
+
const lines = buffer.split("\n");
|
|
21557
|
+
buffer = lines.pop() ?? "";
|
|
21558
|
+
for (const line of lines) {
|
|
21559
|
+
const { tool, error } = readCodexEvent(line);
|
|
21560
|
+
if (tool) {
|
|
21561
|
+
toolsUsed.push(tool);
|
|
21562
|
+
process.stdout.write(` \u2699 ${tool}
|
|
21563
|
+
`);
|
|
21564
|
+
}
|
|
21565
|
+
if (error) {
|
|
21566
|
+
sessionError = error;
|
|
21567
|
+
console.error(`[codex-worker] Session failed: ${error}`);
|
|
21568
|
+
}
|
|
21569
|
+
}
|
|
21570
|
+
});
|
|
21571
|
+
proc.stderr.on("data", (chunk) => {
|
|
21572
|
+
stderrOutput += chunk.toString();
|
|
21573
|
+
});
|
|
21574
|
+
proc.on("close", (code, signal) => {
|
|
21575
|
+
cleanup();
|
|
21576
|
+
console.log(`[codex-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
21577
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
21578
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
21579
|
+
reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21580
|
+
} else {
|
|
21581
|
+
resolve8([...new Set(toolsUsed)]);
|
|
21582
|
+
}
|
|
21583
|
+
});
|
|
21584
|
+
proc.on("error", (err) => {
|
|
21585
|
+
cleanup();
|
|
21586
|
+
reject(err);
|
|
21587
|
+
});
|
|
21588
|
+
});
|
|
21589
|
+
}
|
|
21590
|
+
var codexAdapter = {
|
|
21591
|
+
runtime: "codex",
|
|
21592
|
+
workerType: "gpt",
|
|
21593
|
+
prepare() {
|
|
21594
|
+
},
|
|
21595
|
+
runSession: runCodexSession,
|
|
21596
|
+
classifyError: (text) => {
|
|
21597
|
+
if (QUOTA2.some((re) => re.test(text))) return "quota";
|
|
21598
|
+
if (CREDENTIALS.some((re) => re.test(text))) return "credentials";
|
|
21599
|
+
return classifySessionError(text);
|
|
21600
|
+
}
|
|
21601
|
+
};
|
|
21602
|
+
|
|
21334
21603
|
// ../claude-worker/src/adapters/index.ts
|
|
21335
|
-
var RUNTIMES3 = ["claude", "cursor"];
|
|
21336
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
|
|
21604
|
+
var RUNTIMES3 = ["claude", "cursor", "copilot", "codex"];
|
|
21605
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter, codex: codexAdapter };
|
|
21337
21606
|
function getAdapter(runtime) {
|
|
21338
21607
|
const adapter = ADAPTERS[runtime];
|
|
21339
21608
|
if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
|
|
@@ -21358,7 +21627,7 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
|
|
|
21358
21627
|
function poolMembers(pool) {
|
|
21359
21628
|
return Array.from({ length: pool.max }, (_, i) => {
|
|
21360
21629
|
const name = `${pool.pool}-${i + 1}`;
|
|
21361
|
-
return { name, runtime: pool.runtime, worktree:
|
|
21630
|
+
return { name, runtime: pool.runtime, worktree: join15(pool.worktreeRoot, name), specialization: pool.specialization };
|
|
21362
21631
|
});
|
|
21363
21632
|
}
|
|
21364
21633
|
var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
|
|
@@ -21446,17 +21715,17 @@ function parseSpec(text, specDir) {
|
|
|
21446
21715
|
}
|
|
21447
21716
|
|
|
21448
21717
|
// src/node/store.ts
|
|
21449
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
21450
|
-
import { dirname as dirname8, join as
|
|
21718
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync11, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
|
|
21719
|
+
import { dirname as dirname8, join as join16 } from "node:path";
|
|
21451
21720
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
21452
|
-
var nodeDir = () =>
|
|
21453
|
-
var credsPath = (name) =>
|
|
21454
|
-
var statePath2 = () =>
|
|
21455
|
-
var logPath = (name) =>
|
|
21721
|
+
var nodeDir = () => join16(configDir(), "node");
|
|
21722
|
+
var credsPath = (name) => join16(nodeDir(), "agents", `${name}.json`);
|
|
21723
|
+
var statePath2 = () => join16(nodeDir(), "state.json");
|
|
21724
|
+
var logPath = (name) => join16(nodeDir(), "logs", `${name}.log`);
|
|
21456
21725
|
function writePrivate(path3, value) {
|
|
21457
|
-
|
|
21726
|
+
mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
|
|
21458
21727
|
const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
|
|
21459
|
-
|
|
21728
|
+
writeFileSync8(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
21460
21729
|
renameSync4(tmp, path3);
|
|
21461
21730
|
}
|
|
21462
21731
|
function readJson(path3) {
|
|
@@ -21470,7 +21739,7 @@ var removeCreds = (name) => {
|
|
|
21470
21739
|
if (existsSync8(path3)) rmSync4(path3);
|
|
21471
21740
|
};
|
|
21472
21741
|
var listStoredAgentNames = () => {
|
|
21473
|
-
const dir =
|
|
21742
|
+
const dir = join16(nodeDir(), "agents");
|
|
21474
21743
|
if (!existsSync8(dir)) return [];
|
|
21475
21744
|
return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
21476
21745
|
};
|
|
@@ -21552,7 +21821,7 @@ async function redeemInvite(spec, agent, code, have, specPath) {
|
|
|
21552
21821
|
name: agent.name,
|
|
21553
21822
|
role: "worker",
|
|
21554
21823
|
specialization: agent.specialization,
|
|
21555
|
-
workerType: agent.runtime,
|
|
21824
|
+
workerType: getAdapter(agent.runtime).workerType,
|
|
21556
21825
|
domains: [],
|
|
21557
21826
|
repoPath
|
|
21558
21827
|
});
|
|
@@ -21645,7 +21914,7 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
21645
21914
|
name: agent.name,
|
|
21646
21915
|
role: invite.role,
|
|
21647
21916
|
specialization: agent.specialization,
|
|
21648
|
-
workerType: agent.runtime,
|
|
21917
|
+
workerType: getAdapter(agent.runtime).workerType,
|
|
21649
21918
|
domains: invite.domains ?? [],
|
|
21650
21919
|
repoPath
|
|
21651
21920
|
});
|
|
@@ -21796,27 +22065,27 @@ var Supervisor = class {
|
|
|
21796
22065
|
|
|
21797
22066
|
// src/node/entries.ts
|
|
21798
22067
|
import { existsSync as existsSync9 } from "node:fs";
|
|
21799
|
-
import { dirname as dirname9, join as
|
|
22068
|
+
import { dirname as dirname9, join as join17, resolve as resolve5 } from "node:path";
|
|
21800
22069
|
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.";
|
|
21801
22070
|
function checkoutWorker(dir) {
|
|
21802
22071
|
return {
|
|
21803
|
-
command:
|
|
21804
|
-
args: [
|
|
22072
|
+
command: join17(dir, "node_modules", ".bin", "tsx"),
|
|
22073
|
+
args: [join17(dir, "src", "index.ts")]
|
|
21805
22074
|
};
|
|
21806
22075
|
}
|
|
21807
22076
|
function checkoutMcp(dir, execPath) {
|
|
21808
22077
|
return {
|
|
21809
22078
|
command: execPath,
|
|
21810
22079
|
args: [
|
|
21811
|
-
|
|
21812
|
-
|
|
22080
|
+
join17(dir, "node_modules", "tsx", "dist", "cli.mjs"),
|
|
22081
|
+
join17(dir, "src", "index.ts")
|
|
21813
22082
|
]
|
|
21814
22083
|
};
|
|
21815
22084
|
}
|
|
21816
22085
|
function walkFor(here, packageName, exists) {
|
|
21817
22086
|
for (let dir = dirname9(here); dirname9(dir) !== dir; dir = dirname9(dir)) {
|
|
21818
|
-
const candidate =
|
|
21819
|
-
if (exists(
|
|
22087
|
+
const candidate = join17(dir, packageName);
|
|
22088
|
+
if (exists(join17(candidate, "src", "index.ts"))) return candidate;
|
|
21820
22089
|
}
|
|
21821
22090
|
return null;
|
|
21822
22091
|
}
|
|
@@ -21828,10 +22097,10 @@ function resolveEventWorker(opts) {
|
|
|
21828
22097
|
if (override) {
|
|
21829
22098
|
const resolved = resolve5(override);
|
|
21830
22099
|
if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
|
|
21831
|
-
if (exists(
|
|
22100
|
+
if (exists(join17(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
|
|
21832
22101
|
throw new Error(MISSING_WORKER);
|
|
21833
22102
|
}
|
|
21834
|
-
const bundled =
|
|
22103
|
+
const bundled = join17(dirname9(opts.here), "event-worker.js");
|
|
21835
22104
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21836
22105
|
const dir = walkFor(opts.here, "event-worker", exists);
|
|
21837
22106
|
if (dir) return checkoutWorker(dir);
|
|
@@ -21840,7 +22109,7 @@ function resolveEventWorker(opts) {
|
|
|
21840
22109
|
function resolveMcpServer(opts) {
|
|
21841
22110
|
const execPath = opts.execPath ?? process.execPath;
|
|
21842
22111
|
const exists = opts.exists ?? existsSync9;
|
|
21843
|
-
const bundled =
|
|
22112
|
+
const bundled = join17(dirname9(opts.here), "mcp.js");
|
|
21844
22113
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21845
22114
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
21846
22115
|
if (dir) return checkoutMcp(dir, execPath);
|
|
@@ -21868,6 +22137,7 @@ function describeState(state, alive = isAlive) {
|
|
|
21868
22137
|
max: p.max,
|
|
21869
22138
|
ready: p.ready,
|
|
21870
22139
|
checkedAt: p.checkedAt,
|
|
22140
|
+
desiredMax: p.desiredMax ?? null,
|
|
21871
22141
|
error: p.error ?? null,
|
|
21872
22142
|
running: p.members.filter((m) => byName.get(m)?.status === "running").length
|
|
21873
22143
|
}));
|
|
@@ -21949,8 +22219,8 @@ function loadPruneTargets(names) {
|
|
|
21949
22219
|
|
|
21950
22220
|
// src/node/pool.ts
|
|
21951
22221
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
21952
|
-
import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as
|
|
21953
|
-
import { basename as basename2, dirname as dirname10, join as
|
|
22222
|
+
import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as mkdirSync8, realpathSync as realpathSync4 } from "node:fs";
|
|
22223
|
+
import { basename as basename2, dirname as dirname10, join as join18, resolve as resolve6 } from "node:path";
|
|
21954
22224
|
var IDLE_STOP_MS = 10 * 6e4;
|
|
21955
22225
|
var RECONCILE_MS = 15e3;
|
|
21956
22226
|
var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
|
|
@@ -21965,6 +22235,11 @@ function poolTimings(env2 = process.env) {
|
|
|
21965
22235
|
idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
|
|
21966
22236
|
};
|
|
21967
22237
|
}
|
|
22238
|
+
function effectiveBounds(spec, desired) {
|
|
22239
|
+
if (!desired) return { min: spec.min, max: spec.max, desiredMax: null, capped: false };
|
|
22240
|
+
const max = Math.min(desired.max, spec.max);
|
|
22241
|
+
return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
|
|
22242
|
+
}
|
|
21968
22243
|
function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
21969
22244
|
const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
|
|
21970
22245
|
const running = members.filter((m) => m.running);
|
|
@@ -22038,7 +22313,7 @@ function memberPathProblem(s) {
|
|
|
22038
22313
|
function realpathAllowMissing(path3) {
|
|
22039
22314
|
const abs = resolve6(path3);
|
|
22040
22315
|
if (existsSync10(abs)) return realpathSync4.native(abs);
|
|
22041
|
-
return
|
|
22316
|
+
return join18(realpathAllowMissing(dirname10(abs)), basename2(abs));
|
|
22042
22317
|
}
|
|
22043
22318
|
function run2(cwd, args) {
|
|
22044
22319
|
try {
|
|
@@ -22091,7 +22366,7 @@ function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
|
22091
22366
|
}
|
|
22092
22367
|
const missing = members.filter((m) => !existsSync10(m.worktree));
|
|
22093
22368
|
if (missing.length > 0) {
|
|
22094
|
-
|
|
22369
|
+
mkdirSync8(root, { recursive: true });
|
|
22095
22370
|
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22096
22371
|
const branch = defaultBranch(pool.base);
|
|
22097
22372
|
run2(pool.base, ["worktree", "prune"]);
|
|
@@ -22150,13 +22425,13 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
22150
22425
|
writeMergedJson(target, (existing) => mergeMcpServer(existing, name, entry), root);
|
|
22151
22426
|
excludeIfUntracked(root, target);
|
|
22152
22427
|
},
|
|
22153
|
-
run(
|
|
22154
|
-
execFileSync7(
|
|
22428
|
+
run(bin4, args, cwd) {
|
|
22429
|
+
execFileSync7(bin4, args, { cwd, stdio: "ignore" });
|
|
22155
22430
|
}
|
|
22156
22431
|
});
|
|
22157
22432
|
}
|
|
22158
22433
|
function spawnWorker(spec) {
|
|
22159
|
-
|
|
22434
|
+
mkdirSync9(dirname11(spec.logPath), { recursive: true, mode: 448 });
|
|
22160
22435
|
const log = openSync(spec.logPath, "a", 384);
|
|
22161
22436
|
try {
|
|
22162
22437
|
const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
|
|
@@ -22180,8 +22455,14 @@ async function fetchTasks(creds, query) {
|
|
|
22180
22455
|
if (res.status !== 200 || !Array.isArray(res.payload.data)) throw new Error(`GET /tasks?${query} returned HTTP ${res.status}`);
|
|
22181
22456
|
return res.payload.data;
|
|
22182
22457
|
}
|
|
22458
|
+
async function fetchPoolSize(creds, name) {
|
|
22459
|
+
const res = await getJson(`${creds.api}/repos/${encodeURIComponent(creds.repoId)}/pools`, creds.token);
|
|
22460
|
+
if (res.status !== 200 || !Array.isArray(res.payload.data)) return void 0;
|
|
22461
|
+
const row = res.payload.data.find((p) => p.name === name);
|
|
22462
|
+
return row ? { min: row.min, max: row.max } : void 0;
|
|
22463
|
+
}
|
|
22183
22464
|
async function setAvailable(creds, available) {
|
|
22184
|
-
const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason:
|
|
22465
|
+
const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason: POOL_STOPPED_REASON };
|
|
22185
22466
|
const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/unavailable`, {
|
|
22186
22467
|
method: "PUT",
|
|
22187
22468
|
headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
|
|
@@ -22189,6 +22470,14 @@ async function setAvailable(creds, available) {
|
|
|
22189
22470
|
});
|
|
22190
22471
|
if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/unavailable returned HTTP ${res.status}`);
|
|
22191
22472
|
}
|
|
22473
|
+
async function reportScale(creds, event) {
|
|
22474
|
+
const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/pool-events`, {
|
|
22475
|
+
method: "POST",
|
|
22476
|
+
headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
|
|
22477
|
+
body: JSON.stringify(event)
|
|
22478
|
+
});
|
|
22479
|
+
if (!res.ok) throw new Error(`POST /agents/${creds.agentId}/pool-events returned HTTP ${res.status}`);
|
|
22480
|
+
}
|
|
22192
22481
|
function startPoolLoop(run3, sup, record) {
|
|
22193
22482
|
const { pool, members } = run3;
|
|
22194
22483
|
const { reconcileMs, idleStopMs } = poolTimings();
|
|
@@ -22205,6 +22494,14 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22205
22494
|
log(`could not mark ${member.agent.name} unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
22206
22495
|
}
|
|
22207
22496
|
};
|
|
22497
|
+
const report = async (member, action, reason, ready) => {
|
|
22498
|
+
const running = members.filter((m) => sup.isActive(m.agent.name)).length;
|
|
22499
|
+
try {
|
|
22500
|
+
await reportScale(member.creds, { pool: pool.pool, action, reason, ready, running, max: pool.max });
|
|
22501
|
+
} catch (err) {
|
|
22502
|
+
log(`could not report ${action} ${member.agent.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
22503
|
+
}
|
|
22504
|
+
};
|
|
22208
22505
|
const tick = async () => {
|
|
22209
22506
|
if (busy) return;
|
|
22210
22507
|
busy = true;
|
|
@@ -22224,7 +22521,12 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22224
22521
|
if (counts.assigned + counts.inProgress > 0) lastBusy.set(agent.name, now);
|
|
22225
22522
|
return { name: agent.name, running: sup.isActive(agent.name), ...counts, idleMs: now - (lastBusy.get(agent.name) ?? now) };
|
|
22226
22523
|
});
|
|
22227
|
-
const
|
|
22524
|
+
const bounds = effectiveBounds(pool, await fetchPoolSize(reader.creds, pool.pool));
|
|
22525
|
+
if (bounds.max !== state.max || bounds.min !== state.min) {
|
|
22526
|
+
log(`size min ${bounds.min} max ${bounds.max}${bounds.capped ? ` (API asks for ${bounds.desiredMax}, capped at the ${pool.max} provisioned members)` : ""}`);
|
|
22527
|
+
}
|
|
22528
|
+
Object.assign(state, { min: bounds.min, max: bounds.max, desiredMax: bounds.desiredMax });
|
|
22529
|
+
const decision = reconcilePool({ pending: summary.pending, members: views, min: bounds.min, max: bounds.max, idleStopMs });
|
|
22228
22530
|
for (const name of decision.start) {
|
|
22229
22531
|
const member = members.find((m) => m.agent.name === name);
|
|
22230
22532
|
try {
|
|
@@ -22238,11 +22540,14 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22238
22540
|
lastBusy.set(name, Date.now());
|
|
22239
22541
|
sup.startAgent(name);
|
|
22240
22542
|
log(`started ${name} (ready ${summary.ready})`);
|
|
22543
|
+
await report(member, "started", `ready ${summary.ready}`, summary.ready);
|
|
22241
22544
|
}
|
|
22242
22545
|
for (const name of decision.stop) {
|
|
22546
|
+
const member = members.find((m) => m.agent.name === name);
|
|
22243
22547
|
await sup.stopAgent(name);
|
|
22244
|
-
await markStopped(
|
|
22548
|
+
await markStopped(member);
|
|
22245
22549
|
log(`stopped ${name} after ${Math.round(idleStopMs / 1e3)}s idle`);
|
|
22550
|
+
await report(member, "stopped", `idle ${Math.round(idleStopMs / 1e3)}s`, summary.ready);
|
|
22246
22551
|
}
|
|
22247
22552
|
Object.assign(state, { ready: summary.ready, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: void 0 });
|
|
22248
22553
|
} catch (err) {
|
|
@@ -22417,7 +22722,7 @@ function nodeStatusCommand(opts) {
|
|
|
22417
22722
|
}
|
|
22418
22723
|
for (const p of pools) {
|
|
22419
22724
|
const ready = p.ready === null ? "-" : String(p.ready);
|
|
22420
|
-
console.log(` pool ${p.name.padEnd(11)} running ${p.running}/${p.max} (min ${p.min}) ready ${ready} members ${p.members.join(", ")}${p.error ? source_default.red(` ${p.error}`) : ""}`);
|
|
22725
|
+
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}`) : ""}`);
|
|
22421
22726
|
}
|
|
22422
22727
|
}
|
|
22423
22728
|
|
package/dist/event-worker.js
CHANGED
|
@@ -275,7 +275,11 @@ instructions below override your role rules and the branch rules above. Read the
|
|
|
275
275
|
Find the default branch with \`git remote show origin\`.
|
|
276
276
|
|
|
277
277
|
**orderRole "planning"** (you are the order's planner):
|
|
278
|
-
1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it.
|
|
278
|
+
1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it. If the order
|
|
279
|
+
has no \`title\`, give it a short one (a few words) with \`mcp__relai__update_order\`.
|
|
280
|
+
If the order has an \`issueUrl\`, read it with \`gh issue view <issueUrl> --comments\` and treat the
|
|
281
|
+
issue as the spec; the order's \`feature\` text, when it says more than "Resolve the GitHub issue",
|
|
282
|
+
narrows or adds to it. Put \`Closes #<n>\` (the issue number) in the PR body so the merge closes it.
|
|
279
283
|
2. Create the branch and PR:
|
|
280
284
|
\`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
|
|
281
285
|
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
|
|
@@ -285,7 +289,9 @@ Find the default branch with \`git remote show origin\`.
|
|
|
285
289
|
\`epicId\`, \`assignedTo: "@auto"\`, and
|
|
286
290
|
\`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
|
|
287
291
|
\`specialization\` or \`domains\` only to values agents in \`mcp__relai__list_agents\` declare:
|
|
288
|
-
the router never hands out a task whose tag no agent matches.
|
|
292
|
+
the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
|
|
293
|
+
\`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
|
|
294
|
+
Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
|
|
289
295
|
and independently testable so several agents can work at once. Use \`blockedBy\` only where
|
|
290
296
|
one task truly needs another's result. Each description is self-contained: what to change,
|
|
291
297
|
which files, how to test it, and the acceptance criteria.
|
|
@@ -297,24 +303,55 @@ Find the default branch with \`git remote show origin\`.
|
|
|
297
303
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
298
304
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
299
305
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
300
|
-
|
|
306
|
+
If the order has \`pairBuilds\`, every build task needs \`verifyKind: "reviewer_agent"\` and a
|
|
307
|
+
\`verifyReviewerId\` on a different \`workerType\` from the writers (see \`mcp__relai__list_agents\`);
|
|
308
|
+
the API refuses a paired build task without one and routes it away from the reviewer's vendor.
|
|
309
|
+
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
310
|
+
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
311
|
+
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
312
|
+
the planning task's thread and complete it.
|
|
313
|
+
|
|
314
|
+
**orderRole "plan_review"** (assigned to the planner, reviewed by another agent):
|
|
315
|
+
- After the task graph is written, complete each plan review task. That hands the plan to its
|
|
316
|
+
reviewer. Every build task waits until all plan reviews are approved.
|
|
317
|
+
- On a reject the task comes back to you with the reviewer's note: revise the plan under the Epic
|
|
318
|
+
(edit, add or cancel tasks), reply on the task's thread with what changed, and complete it again.
|
|
319
|
+
- As the reviewer of a plan review: read the goal and every task under its Epic. Approve when the
|
|
320
|
+
tasks cover the goal, are small and independent, and their \`blockedBy\` order is right; otherwise
|
|
321
|
+
reject with exactly what to change.
|
|
301
322
|
|
|
302
323
|
**orderRole "build"**:
|
|
303
324
|
- \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
|
|
304
325
|
test and commit. Do not create another branch or PR.
|
|
305
326
|
- Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
|
|
306
327
|
repeating up to 5 times if the push is rejected. Never force-push.
|
|
328
|
+
- If you drop or change any part of what the goal asked for, even for a good reason, record each one
|
|
329
|
+
with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
|
|
330
|
+
"reason": "<why>" }]\` and say so in your handoff. The owner decides whether to accept it.
|
|
307
331
|
- Hand off on the task's thread with the commit sha and what you tested, then complete.
|
|
308
332
|
|
|
309
333
|
**orderRole "gate"**:
|
|
310
334
|
- Check every build task in the order's Epic is completed, then post the PR link on the task's
|
|
311
|
-
thread and complete it. The gate decides from there: the owner approves a \`human_approval\`
|
|
335
|
+
thread, listing every \`metadata.deviations\` entry from the build tasks, and complete it. The gate decides from there: the owner approves a \`human_approval\`
|
|
312
336
|
gate on the dashboard and the named reviewer decides a \`reviewer_agent\` gate.
|
|
337
|
+
- As the reviewer of an order, check each thing the goal asked for. Anything dropped or changed is
|
|
338
|
+
the owner's call, not yours: if it is recorded in \`metadata.deviations\`, mention it and judge
|
|
339
|
+
the rest; if it is not recorded, reject and name it so the builder records it. The owner decides
|
|
340
|
+
on deviations in the approval gate.
|
|
313
341
|
|
|
314
342
|
**orderRole "merge"**:
|
|
315
343
|
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
|
|
344
|
+
Before merging, replace the PR body with a run report built from
|
|
345
|
+
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
346
|
+
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
347
|
+
completed times, the number of distinct agents that worked on it, the total elapsed time
|
|
348
|
+
from the planning task's creation, and the CI checks that ran. Keep any \`Closes #<n>\` line. Use
|
|
349
|
+
\`gh pr edit <prUrl> --body-file <file>\`.
|
|
316
350
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
317
|
-
gh pr view --json headRefOid
|
|
351
|
+
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
352
|
+
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
353
|
+
\`gh pr edit <prUrl> --body-file <file>\` (a merged PR still accepts an edit). Post the merge commit
|
|
354
|
+
on the thread, and complete.
|
|
318
355
|
3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
|
|
319
356
|
After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
|
|
320
357
|
function buildPrompt(config2) {
|
|
@@ -469,7 +506,7 @@ function mcpServerLaunch(here = fileURLToPath(import.meta.url), exists = existsS
|
|
|
469
506
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
470
507
|
return { command: execPath, args: [mcpServerTsxCli(), mcpServerEntry()] };
|
|
471
508
|
}
|
|
472
|
-
function writeMcpConfig(config2) {
|
|
509
|
+
function writeMcpConfig(config2, prefix = "claude-worker") {
|
|
473
510
|
const { command, args } = mcpServerLaunch();
|
|
474
511
|
const mcpConfig = {
|
|
475
512
|
mcpServers: {
|
|
@@ -485,7 +522,7 @@ function writeMcpConfig(config2) {
|
|
|
485
522
|
}
|
|
486
523
|
}
|
|
487
524
|
};
|
|
488
|
-
const path = join(tmpdir(),
|
|
525
|
+
const path = join(tmpdir(), `${prefix}-${config2.agentId}-${Date.now()}.json`);
|
|
489
526
|
writeFileSync(path, JSON.stringify(mcpConfig), { mode: 384 });
|
|
490
527
|
return path;
|
|
491
528
|
}
|
|
@@ -703,6 +740,7 @@ async function runCursorSession(config2) {
|
|
|
703
740
|
}
|
|
704
741
|
var cursorAdapter = {
|
|
705
742
|
runtime: "cursor",
|
|
743
|
+
workerType: "cursor",
|
|
706
744
|
prepare(ctx) {
|
|
707
745
|
ctx.writeMcpServer(join2(ctx.worktree, ".cursor", "mcp.json"), "relai", {
|
|
708
746
|
command: ctx.mcp.command,
|
|
@@ -718,15 +756,245 @@ var cursorAdapter = {
|
|
|
718
756
|
// ../claude-worker/src/adapters/claude.ts
|
|
719
757
|
var claudeAdapter = {
|
|
720
758
|
runtime: "claude",
|
|
759
|
+
workerType: "claude",
|
|
721
760
|
prepare() {
|
|
722
761
|
},
|
|
723
762
|
runSession: runClaudeSession,
|
|
724
763
|
classifyError: classifySessionError
|
|
725
764
|
};
|
|
726
765
|
|
|
766
|
+
// ../claude-worker/src/adapters/copilot.ts
|
|
767
|
+
import { spawn as spawn3 } from "child_process";
|
|
768
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
769
|
+
var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
|
|
770
|
+
var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
|
|
771
|
+
var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
|
|
772
|
+
function copilotArgs(repoPath, prompt, mcpConfigPath) {
|
|
773
|
+
return [
|
|
774
|
+
"-p",
|
|
775
|
+
prompt,
|
|
776
|
+
"--allow-all-tools",
|
|
777
|
+
"--output-format",
|
|
778
|
+
"json",
|
|
779
|
+
"--no-color",
|
|
780
|
+
"-C",
|
|
781
|
+
repoPath,
|
|
782
|
+
"--additional-mcp-config",
|
|
783
|
+
`@${mcpConfigPath}`,
|
|
784
|
+
"--disable-builtin-mcps"
|
|
785
|
+
];
|
|
786
|
+
}
|
|
787
|
+
function readCopilotEvent(line) {
|
|
788
|
+
let event;
|
|
789
|
+
try {
|
|
790
|
+
event = JSON.parse(line);
|
|
791
|
+
} catch {
|
|
792
|
+
return {};
|
|
793
|
+
}
|
|
794
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
795
|
+
const data = event.data ?? {};
|
|
796
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
797
|
+
if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
|
|
798
|
+
if (type === "session.error") return { error: message || "copilot session error" };
|
|
799
|
+
if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
|
|
800
|
+
if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
|
|
801
|
+
return {};
|
|
802
|
+
}
|
|
803
|
+
async function runCopilotSession(config2) {
|
|
804
|
+
const mcpConfigPath = writeMcpConfig(config2, "copilot-worker");
|
|
805
|
+
const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath);
|
|
806
|
+
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
807
|
+
return new Promise((resolve, reject) => {
|
|
808
|
+
const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
809
|
+
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
810
|
+
const toolsUsed = [];
|
|
811
|
+
let buffer = "";
|
|
812
|
+
let sessionError = "";
|
|
813
|
+
let stderrOutput = "";
|
|
814
|
+
proc.stdout.on("data", (chunk) => {
|
|
815
|
+
buffer += chunk.toString();
|
|
816
|
+
const lines = buffer.split("\n");
|
|
817
|
+
buffer = lines.pop() ?? "";
|
|
818
|
+
for (const line of lines) {
|
|
819
|
+
const { tool, error } = readCopilotEvent(line);
|
|
820
|
+
if (tool) {
|
|
821
|
+
toolsUsed.push(tool);
|
|
822
|
+
process.stdout.write(` \u2699 ${tool}
|
|
823
|
+
`);
|
|
824
|
+
}
|
|
825
|
+
if (error) {
|
|
826
|
+
sessionError = error;
|
|
827
|
+
console.error(`[copilot-worker] Session failed: ${error}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
proc.stderr.on("data", (chunk) => {
|
|
832
|
+
stderrOutput += chunk.toString();
|
|
833
|
+
});
|
|
834
|
+
proc.on("close", (code, signal) => {
|
|
835
|
+
try {
|
|
836
|
+
unlinkSync2(mcpConfigPath);
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
840
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
841
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
842
|
+
reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
843
|
+
} else {
|
|
844
|
+
resolve([...new Set(toolsUsed)]);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
proc.on("error", (err) => {
|
|
848
|
+
try {
|
|
849
|
+
unlinkSync2(mcpConfigPath);
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
reject(err);
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
var copilotAdapter = {
|
|
857
|
+
runtime: "copilot",
|
|
858
|
+
workerType: "copilot",
|
|
859
|
+
prepare() {
|
|
860
|
+
},
|
|
861
|
+
runSession: runCopilotSession,
|
|
862
|
+
classifyError: (text) => {
|
|
863
|
+
if (POLICY_BLOCKS.some((re) => re.test(text))) return "credentials";
|
|
864
|
+
if (QUOTA.some((re) => re.test(text))) return "quota";
|
|
865
|
+
return classifySessionError(text);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// ../claude-worker/src/adapters/codex.ts
|
|
870
|
+
import { spawn as spawn4 } from "child_process";
|
|
871
|
+
import { chmodSync, mkdirSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
872
|
+
import { homedir } from "os";
|
|
873
|
+
import { join as join3 } from "path";
|
|
874
|
+
var bin3 = () => process.env.CODEX_BIN ?? "codex";
|
|
875
|
+
var codexHome = () => process.env.CODEX_HOME ?? join3(homedir(), ".codex");
|
|
876
|
+
var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
|
|
877
|
+
var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
|
|
878
|
+
function codexArgs(repoPath, prompt, profile) {
|
|
879
|
+
return [
|
|
880
|
+
"exec",
|
|
881
|
+
"--json",
|
|
882
|
+
"--ephemeral",
|
|
883
|
+
"--skip-git-repo-check",
|
|
884
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
885
|
+
"-C",
|
|
886
|
+
repoPath,
|
|
887
|
+
"-p",
|
|
888
|
+
profile,
|
|
889
|
+
prompt
|
|
890
|
+
];
|
|
891
|
+
}
|
|
892
|
+
var tomlString = (v) => JSON.stringify(v);
|
|
893
|
+
function codexProfileToml(server, env) {
|
|
894
|
+
return [
|
|
895
|
+
"[mcp_servers.relai]",
|
|
896
|
+
`command = ${tomlString(server.command)}`,
|
|
897
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
898
|
+
"",
|
|
899
|
+
"[mcp_servers.relai.env]",
|
|
900
|
+
...Object.entries(env).map(([k, v]) => `${k} = ${tomlString(v)}`),
|
|
901
|
+
""
|
|
902
|
+
].join("\n");
|
|
903
|
+
}
|
|
904
|
+
function readCodexEvent(line) {
|
|
905
|
+
let event;
|
|
906
|
+
try {
|
|
907
|
+
event = JSON.parse(line);
|
|
908
|
+
} catch {
|
|
909
|
+
return {};
|
|
910
|
+
}
|
|
911
|
+
if (event.type === "item.started" && event.item) {
|
|
912
|
+
if (event.item.type === "command_execution") return { tool: "shell" };
|
|
913
|
+
if (event.item.type === "mcp_tool_call") return { tool: `${String(event.item.server ?? "mcp")}-${String(event.item.tool ?? "tool")}` };
|
|
914
|
+
return {};
|
|
915
|
+
}
|
|
916
|
+
if (event.type === "turn.failed") return { error: String(event.error?.message ?? "codex turn failed") };
|
|
917
|
+
if (event.type === "error") return { error: String(event.message ?? "codex error") };
|
|
918
|
+
return {};
|
|
919
|
+
}
|
|
920
|
+
async function runCodexSession(config2) {
|
|
921
|
+
const profile = `pitboss-${config2.agentId}`;
|
|
922
|
+
mkdirSync(codexHome(), { recursive: true, mode: 448 });
|
|
923
|
+
const profilePath = join3(codexHome(), `${profile}.config.toml`);
|
|
924
|
+
writeFileSync2(profilePath, codexProfileToml(mcpServerLaunch(), {
|
|
925
|
+
API_URL: config2.apiUrl,
|
|
926
|
+
API_SECRET: config2.apiSecret,
|
|
927
|
+
AGENT_ID: config2.agentId,
|
|
928
|
+
REPO_ID: config2.repoId
|
|
929
|
+
}), { mode: 384 });
|
|
930
|
+
chmodSync(profilePath, 384);
|
|
931
|
+
const cleanup = () => {
|
|
932
|
+
try {
|
|
933
|
+
unlinkSync3(profilePath);
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
const args = codexArgs(config2.repoPath, buildPrompt(config2), profile);
|
|
938
|
+
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
939
|
+
return new Promise((resolve, reject) => {
|
|
940
|
+
const proc = spawn4(bin3(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
941
|
+
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
942
|
+
const toolsUsed = [];
|
|
943
|
+
let buffer = "";
|
|
944
|
+
let sessionError = "";
|
|
945
|
+
let stderrOutput = "";
|
|
946
|
+
proc.stdout.on("data", (chunk) => {
|
|
947
|
+
buffer += chunk.toString();
|
|
948
|
+
const lines = buffer.split("\n");
|
|
949
|
+
buffer = lines.pop() ?? "";
|
|
950
|
+
for (const line of lines) {
|
|
951
|
+
const { tool, error } = readCodexEvent(line);
|
|
952
|
+
if (tool) {
|
|
953
|
+
toolsUsed.push(tool);
|
|
954
|
+
process.stdout.write(` \u2699 ${tool}
|
|
955
|
+
`);
|
|
956
|
+
}
|
|
957
|
+
if (error) {
|
|
958
|
+
sessionError = error;
|
|
959
|
+
console.error(`[codex-worker] Session failed: ${error}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
proc.stderr.on("data", (chunk) => {
|
|
964
|
+
stderrOutput += chunk.toString();
|
|
965
|
+
});
|
|
966
|
+
proc.on("close", (code, signal) => {
|
|
967
|
+
cleanup();
|
|
968
|
+
console.log(`[codex-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
969
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
970
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
971
|
+
reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
972
|
+
} else {
|
|
973
|
+
resolve([...new Set(toolsUsed)]);
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
proc.on("error", (err) => {
|
|
977
|
+
cleanup();
|
|
978
|
+
reject(err);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
var codexAdapter = {
|
|
983
|
+
runtime: "codex",
|
|
984
|
+
workerType: "gpt",
|
|
985
|
+
prepare() {
|
|
986
|
+
},
|
|
987
|
+
runSession: runCodexSession,
|
|
988
|
+
classifyError: (text) => {
|
|
989
|
+
if (QUOTA2.some((re) => re.test(text))) return "quota";
|
|
990
|
+
if (CREDENTIALS.some((re) => re.test(text))) return "credentials";
|
|
991
|
+
return classifySessionError(text);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
|
|
727
995
|
// ../claude-worker/src/adapters/index.ts
|
|
728
|
-
var RUNTIMES = ["claude", "cursor"];
|
|
729
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
|
|
996
|
+
var RUNTIMES = ["claude", "cursor", "copilot", "codex"];
|
|
997
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter, codex: codexAdapter };
|
|
730
998
|
function getAdapter(runtime) {
|
|
731
999
|
const adapter = ADAPTERS[runtime];
|
|
732
1000
|
if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
|
package/dist/mcp.js
CHANGED
|
@@ -12433,6 +12433,12 @@ var ApiClient = class {
|
|
|
12433
12433
|
archiveThread(threadId) {
|
|
12434
12434
|
return this.request("PUT", `/threads/${threadId}/archive`, {});
|
|
12435
12435
|
}
|
|
12436
|
+
listPools(repoId) {
|
|
12437
|
+
return this.request("GET", `/repos/${encodeURIComponent(repoId)}/pools`);
|
|
12438
|
+
}
|
|
12439
|
+
setPoolSize(repoId, name, body) {
|
|
12440
|
+
return this.request("PUT", `/repos/${encodeURIComponent(repoId)}/pools/${encodeURIComponent(name)}`, body);
|
|
12441
|
+
}
|
|
12436
12442
|
createOrder(body) {
|
|
12437
12443
|
return this.request("POST", "/orders", body);
|
|
12438
12444
|
}
|
|
@@ -12530,15 +12536,24 @@ function buildCreateOrderTool(client, repoId, opts) {
|
|
|
12530
12536
|
description: "Create a factory order: a request to change a repo, plus the gates that make it done. Creates the order in planning, its Epic, and a planning task for the repo's orchestrator. " + ORDER_GATE_NOTE + (opts.ownerScoped ? " As the owner's agent you may pass repoId to file in any repo your owner owns; it defaults to your home repo." : ""),
|
|
12531
12537
|
inputSchema: external_exports.object({
|
|
12532
12538
|
repoId: external_exports.string().optional().describe(opts.ownerScoped ? "Repo to file the order in: any repo your owner owns. Defaults to your home repo." : "Repo to file the order in. Defaults to this agent's home repo."),
|
|
12533
|
-
feature: external_exports.string().min(1).describe("What to build, in the requester's words."),
|
|
12539
|
+
feature: external_exports.string().min(1).optional().describe("What to build, in the requester's words. Optional when issueUrl is given."),
|
|
12540
|
+
issueUrl: external_exports.string().optional().describe("A GitHub issue in this repo to build from (https://github.com/<owner>/<repo>/issues/<n>). The planner reads it, and the merge closes it."),
|
|
12541
|
+
planReviewers: external_exports.number().int().min(0).max(3).optional().describe("How many other agents must approve the plan before building starts (0 to 3). Reviewers are picked by the API, preferring reviewer agents on another vendor."),
|
|
12542
|
+
pairBuilds: external_exports.boolean().optional().describe("Pair every build task with a reviewer on a different vendor: each build carries its own review gate, and the builder is never on the reviewer's vendor."),
|
|
12543
|
+
title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal. The planner writes one when omitted."),
|
|
12534
12544
|
acceptance: external_exports.array(external_exports.enum(["ci_passed", "human_approval", "reviewer_agent"])).optional().describe("Gates that must all pass. Defaults to human_approval."),
|
|
12535
12545
|
budget: external_exports.record(external_exports.unknown()).optional().describe("Optional ceilings. Stored only; not enforced.")
|
|
12536
12546
|
}),
|
|
12537
12547
|
handler: async (input) => {
|
|
12548
|
+
if (!input.feature && !input.issueUrl) return mcpFail("create_order needs feature or issueUrl");
|
|
12538
12549
|
try {
|
|
12539
12550
|
const created = await client.createOrder({
|
|
12540
12551
|
repoId: targetRepo(input),
|
|
12541
|
-
feature: input.feature,
|
|
12552
|
+
...input.feature ? { feature: input.feature } : {},
|
|
12553
|
+
...input.issueUrl ? { issueUrl: input.issueUrl } : {},
|
|
12554
|
+
...input.planReviewers ? { planReviewers: input.planReviewers } : {},
|
|
12555
|
+
...input.pairBuilds ? { pairBuilds: true } : {},
|
|
12556
|
+
...input.title ? { title: input.title } : {},
|
|
12542
12557
|
acceptance: input.acceptance,
|
|
12543
12558
|
budget: input.budget
|
|
12544
12559
|
});
|
|
@@ -12553,15 +12568,17 @@ function buildCreateOrderTool(client, repoId, opts) {
|
|
|
12553
12568
|
function buildUpdateOrderTool(client) {
|
|
12554
12569
|
return {
|
|
12555
12570
|
name: "update_order",
|
|
12556
|
-
description: "Record the order's branch and PR, or cancel it. For the repo orchestrator and the order's planner. " + ORDER_GATE_NOTE,
|
|
12571
|
+
description: "Record the order's short title, branch and PR, or cancel it. For the repo orchestrator and the order's planner. " + ORDER_GATE_NOTE,
|
|
12557
12572
|
inputSchema: external_exports.object({
|
|
12558
12573
|
id: external_exports.string().describe("The order id (ord_\u2026)."),
|
|
12574
|
+
title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal."),
|
|
12559
12575
|
branch: external_exports.string().min(1).optional().describe("The order's branch."),
|
|
12560
12576
|
prUrl: external_exports.string().url().optional().describe("The order's pull request URL."),
|
|
12561
12577
|
cancel: external_exports.boolean().optional().describe("Set true to cancel the order.")
|
|
12562
12578
|
}),
|
|
12563
12579
|
handler: async (input) => {
|
|
12564
12580
|
const body = {};
|
|
12581
|
+
if (input.title !== void 0) body.title = input.title;
|
|
12565
12582
|
if (input.branch !== void 0) body.branch = input.branch;
|
|
12566
12583
|
if (input.prUrl !== void 0) body.prUrl = input.prUrl;
|
|
12567
12584
|
if (input.cancel) body.status = "cancelled";
|
|
@@ -12574,6 +12591,46 @@ function buildUpdateOrderTool(client) {
|
|
|
12574
12591
|
}
|
|
12575
12592
|
};
|
|
12576
12593
|
}
|
|
12594
|
+
var POOL_NOTE = "A pool is a set of pre-approved workers that pitboss node starts and stops to match the ready work. The size set here is a target: the node never runs more members than it provisioned for the pool.";
|
|
12595
|
+
function buildListPoolsTool(client, repoId, opts) {
|
|
12596
|
+
const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
|
|
12597
|
+
return {
|
|
12598
|
+
name: "list_pools",
|
|
12599
|
+
description: "List the worker pool sizes set for a repo. " + POOL_NOTE,
|
|
12600
|
+
inputSchema: external_exports.object({
|
|
12601
|
+
repoId: external_exports.string().optional().describe("Repo to read. Defaults to this agent's home repo.")
|
|
12602
|
+
}),
|
|
12603
|
+
handler: async (input) => {
|
|
12604
|
+
try {
|
|
12605
|
+
const data = await client.listPools(targetRepo(input));
|
|
12606
|
+
return { content: [{ type: "text", text: JSON.stringify({ data }, null, 2) }] };
|
|
12607
|
+
} catch (err) {
|
|
12608
|
+
return mcpFail(err);
|
|
12609
|
+
}
|
|
12610
|
+
}
|
|
12611
|
+
};
|
|
12612
|
+
}
|
|
12613
|
+
function buildSetPoolSizeTool(client, repoId, opts) {
|
|
12614
|
+
const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
|
|
12615
|
+
return {
|
|
12616
|
+
name: "set_pool_size",
|
|
12617
|
+
description: "Set how many workers a repo's pool may run (max) and keep warm (min). " + POOL_NOTE + (opts.ownerScoped ? " As the owner's agent you may pass repoId for any repo your owner owns." : ""),
|
|
12618
|
+
inputSchema: external_exports.object({
|
|
12619
|
+
repoId: external_exports.string().optional().describe(opts.ownerScoped ? "Repo the pool belongs to. Defaults to your home repo." : "Ignored; the pool is in this agent's repo."),
|
|
12620
|
+
pool: external_exports.string().min(1).describe("The pool name from the node spec, e.g. 'ni-pool'."),
|
|
12621
|
+
min: external_exports.number().int().min(0).max(32).optional().describe("Members to keep running when idle. Defaults to 0."),
|
|
12622
|
+
max: external_exports.number().int().min(1).max(32).describe("Most members to run at once.")
|
|
12623
|
+
}),
|
|
12624
|
+
handler: async (input) => {
|
|
12625
|
+
try {
|
|
12626
|
+
const row = await client.setPoolSize(targetRepo(input), input.pool, { min: input.min ?? 0, max: input.max });
|
|
12627
|
+
return { content: [{ type: "text", text: JSON.stringify(row, null, 2) }] };
|
|
12628
|
+
} catch (err) {
|
|
12629
|
+
return mcpFail(err);
|
|
12630
|
+
}
|
|
12631
|
+
}
|
|
12632
|
+
};
|
|
12633
|
+
}
|
|
12577
12634
|
function resolveAgentRef(candidates, ref, scope) {
|
|
12578
12635
|
const available = candidates.length ? ` Available: ${candidates.map((a) => `${a.name} (${a.id})`).join(", ")}` : ` ${scope} has no agents.`;
|
|
12579
12636
|
if (ref.startsWith("agent_")) {
|
|
@@ -13169,6 +13226,8 @@ ${version.body}`
|
|
|
13169
13226
|
...buildOrderReadTools(client, repoId, opts),
|
|
13170
13227
|
...opts.ownerScoped || opts.role === "orchestrator" ? [buildCreateOrderTool(client, repoId, opts)] : [],
|
|
13171
13228
|
...opts.role === "orchestrator" || (opts.tier ?? 0) >= 2 ? [buildUpdateOrderTool(client)] : [],
|
|
13229
|
+
buildListPoolsTool(client, repoId, opts),
|
|
13230
|
+
...opts.ownerScoped || opts.role === "orchestrator" ? [buildSetPoolSizeTool(client, repoId, opts)] : [],
|
|
13172
13231
|
// Additive, never a swap: an owner-scoped agent is still an agent and needs
|
|
13173
13232
|
// everything above. buildOperatorTools assumes no agent identity, and its
|
|
13174
13233
|
// create_task bypasses propose/commit.
|