@pitboss/cli 0.4.4 → 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 +181 -21
- package/dist/event-worker.js +132 -9
- package/dist/mcp.js +53 -1
- 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 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
|
|
@@ -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);
|
|
@@ -20882,7 +20883,8 @@ 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\`.
|
|
20886
20888
|
2. Create the branch and PR:
|
|
20887
20889
|
\`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
|
|
20888
20890
|
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
|
|
@@ -20892,7 +20894,9 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20892
20894
|
\`epicId\`, \`assignedTo: "@auto"\`, and
|
|
20893
20895
|
\`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
|
|
20894
20896
|
\`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.
|
|
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
|
|
20896
20900
|
and independently testable so several agents can work at once. Use \`blockedBy\` only where
|
|
20897
20901
|
one task truly needs another's result. Each description is self-contained: what to change,
|
|
20898
20902
|
which files, how to test it, and the acceptance criteria.
|
|
@@ -20904,24 +20908,42 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20904
20908
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
20905
20909
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
20906
20910
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
20907
|
-
6.
|
|
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.
|
|
20908
20915
|
|
|
20909
20916
|
**orderRole "build"**:
|
|
20910
20917
|
- \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
|
|
20911
20918
|
test and commit. Do not create another branch or PR.
|
|
20912
20919
|
- Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
|
|
20913
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.
|
|
20914
20924
|
- Hand off on the task's thread with the commit sha and what you tested, then complete.
|
|
20915
20925
|
|
|
20916
20926
|
**orderRole "gate"**:
|
|
20917
20927
|
- 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\`
|
|
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\`
|
|
20919
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.
|
|
20920
20934
|
|
|
20921
20935
|
**orderRole "merge"**:
|
|
20922
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>\`.
|
|
20923
20942
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
20924
|
-
gh pr view --json headRefOid
|
|
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.
|
|
20925
20947
|
3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
|
|
20926
20948
|
After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
|
|
20927
20949
|
function buildPrompt(config) {
|
|
@@ -21076,7 +21098,7 @@ function mcpServerLaunch(here = fileURLToPath2(import.meta.url), exists = exists
|
|
|
21076
21098
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21077
21099
|
return { command: execPath, args: [mcpServerTsxCli(), mcpServerEntry()] };
|
|
21078
21100
|
}
|
|
21079
|
-
function writeMcpConfig(config) {
|
|
21101
|
+
function writeMcpConfig(config, prefix = "claude-worker") {
|
|
21080
21102
|
const { command, args } = mcpServerLaunch();
|
|
21081
21103
|
const mcpConfig = {
|
|
21082
21104
|
mcpServers: {
|
|
@@ -21092,7 +21114,7 @@ function writeMcpConfig(config) {
|
|
|
21092
21114
|
}
|
|
21093
21115
|
}
|
|
21094
21116
|
};
|
|
21095
|
-
const path3 = join12(tmpdir(),
|
|
21117
|
+
const path3 = join12(tmpdir(), `${prefix}-${config.agentId}-${Date.now()}.json`);
|
|
21096
21118
|
writeFileSync6(path3, JSON.stringify(mcpConfig), { mode: 384 });
|
|
21097
21119
|
return path3;
|
|
21098
21120
|
}
|
|
@@ -21331,9 +21353,111 @@ var claudeAdapter = {
|
|
|
21331
21353
|
classifyError: classifySessionError
|
|
21332
21354
|
};
|
|
21333
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
|
+
|
|
21334
21458
|
// ../claude-worker/src/adapters/index.ts
|
|
21335
|
-
var RUNTIMES3 = ["claude", "cursor"];
|
|
21336
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
|
|
21459
|
+
var RUNTIMES3 = ["claude", "cursor", "copilot"];
|
|
21460
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
|
|
21337
21461
|
function getAdapter(runtime) {
|
|
21338
21462
|
const adapter = ADAPTERS[runtime];
|
|
21339
21463
|
if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
|
|
@@ -21868,6 +21992,7 @@ function describeState(state, alive = isAlive) {
|
|
|
21868
21992
|
max: p.max,
|
|
21869
21993
|
ready: p.ready,
|
|
21870
21994
|
checkedAt: p.checkedAt,
|
|
21995
|
+
desiredMax: p.desiredMax ?? null,
|
|
21871
21996
|
error: p.error ?? null,
|
|
21872
21997
|
running: p.members.filter((m) => byName.get(m)?.status === "running").length
|
|
21873
21998
|
}));
|
|
@@ -21965,6 +22090,11 @@ function poolTimings(env2 = process.env) {
|
|
|
21965
22090
|
idleStopMs: positiveMs(env2.PITBOSS_POOL_IDLE_STOP_MS, IDLE_STOP_MS)
|
|
21966
22091
|
};
|
|
21967
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
|
+
}
|
|
21968
22098
|
function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
21969
22099
|
const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
|
|
21970
22100
|
const running = members.filter((m) => m.running);
|
|
@@ -22150,8 +22280,8 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
22150
22280
|
writeMergedJson(target, (existing) => mergeMcpServer(existing, name, entry), root);
|
|
22151
22281
|
excludeIfUntracked(root, target);
|
|
22152
22282
|
},
|
|
22153
|
-
run(
|
|
22154
|
-
execFileSync7(
|
|
22283
|
+
run(bin3, args, cwd) {
|
|
22284
|
+
execFileSync7(bin3, args, { cwd, stdio: "ignore" });
|
|
22155
22285
|
}
|
|
22156
22286
|
});
|
|
22157
22287
|
}
|
|
@@ -22180,8 +22310,14 @@ async function fetchTasks(creds, query) {
|
|
|
22180
22310
|
if (res.status !== 200 || !Array.isArray(res.payload.data)) throw new Error(`GET /tasks?${query} returned HTTP ${res.status}`);
|
|
22181
22311
|
return res.payload.data;
|
|
22182
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
|
+
}
|
|
22183
22319
|
async function setAvailable(creds, available) {
|
|
22184
|
-
const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason:
|
|
22320
|
+
const body = available ? { until: null } : { until: new Date(Date.now() + UNAVAILABLE_FOR_MS).toISOString(), reason: POOL_STOPPED_REASON };
|
|
22185
22321
|
const res = await fetch(`${creds.api}/agents/${encodeURIComponent(creds.agentId)}/unavailable`, {
|
|
22186
22322
|
method: "PUT",
|
|
22187
22323
|
headers: { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json" },
|
|
@@ -22189,6 +22325,14 @@ async function setAvailable(creds, available) {
|
|
|
22189
22325
|
});
|
|
22190
22326
|
if (!res.ok) throw new Error(`PUT /agents/${creds.agentId}/unavailable returned HTTP ${res.status}`);
|
|
22191
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
|
+
}
|
|
22192
22336
|
function startPoolLoop(run3, sup, record) {
|
|
22193
22337
|
const { pool, members } = run3;
|
|
22194
22338
|
const { reconcileMs, idleStopMs } = poolTimings();
|
|
@@ -22205,6 +22349,14 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22205
22349
|
log(`could not mark ${member.agent.name} unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
22206
22350
|
}
|
|
22207
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
|
+
};
|
|
22208
22360
|
const tick = async () => {
|
|
22209
22361
|
if (busy) return;
|
|
22210
22362
|
busy = true;
|
|
@@ -22224,7 +22376,12 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22224
22376
|
if (counts.assigned + counts.inProgress > 0) lastBusy.set(agent.name, now);
|
|
22225
22377
|
return { name: agent.name, running: sup.isActive(agent.name), ...counts, idleMs: now - (lastBusy.get(agent.name) ?? now) };
|
|
22226
22378
|
});
|
|
22227
|
-
const
|
|
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 });
|
|
22228
22385
|
for (const name of decision.start) {
|
|
22229
22386
|
const member = members.find((m) => m.agent.name === name);
|
|
22230
22387
|
try {
|
|
@@ -22238,11 +22395,14 @@ function startPoolLoop(run3, sup, record) {
|
|
|
22238
22395
|
lastBusy.set(name, Date.now());
|
|
22239
22396
|
sup.startAgent(name);
|
|
22240
22397
|
log(`started ${name} (ready ${summary.ready})`);
|
|
22398
|
+
await report(member, "started", `ready ${summary.ready}`, summary.ready);
|
|
22241
22399
|
}
|
|
22242
22400
|
for (const name of decision.stop) {
|
|
22401
|
+
const member = members.find((m) => m.agent.name === name);
|
|
22243
22402
|
await sup.stopAgent(name);
|
|
22244
|
-
await markStopped(
|
|
22403
|
+
await markStopped(member);
|
|
22245
22404
|
log(`stopped ${name} after ${Math.round(idleStopMs / 1e3)}s idle`);
|
|
22405
|
+
await report(member, "stopped", `idle ${Math.round(idleStopMs / 1e3)}s`, summary.ready);
|
|
22246
22406
|
}
|
|
22247
22407
|
Object.assign(state, { ready: summary.ready, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: void 0 });
|
|
22248
22408
|
} catch (err) {
|
|
@@ -22417,7 +22577,7 @@ function nodeStatusCommand(opts) {
|
|
|
22417
22577
|
}
|
|
22418
22578
|
for (const p of pools) {
|
|
22419
22579
|
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}`) : ""}`);
|
|
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}`) : ""}`);
|
|
22421
22581
|
}
|
|
22422
22582
|
}
|
|
22423
22583
|
|
package/dist/event-worker.js
CHANGED
|
@@ -275,7 +275,8 @@ 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\`.
|
|
279
280
|
2. Create the branch and PR:
|
|
280
281
|
\`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
|
|
281
282
|
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
|
|
@@ -285,7 +286,9 @@ Find the default branch with \`git remote show origin\`.
|
|
|
285
286
|
\`epicId\`, \`assignedTo: "@auto"\`, and
|
|
286
287
|
\`metadata: { orderId, orderRole: "build", branchName: "order/<orderId>" }\`. Set
|
|
287
288
|
\`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.
|
|
289
|
+
the router never hands out a task whose tag no agent matches. When the repo has \`writer\` agents, set
|
|
290
|
+
\`specialization: "writer"\` on build tasks, so they wait for a writer instead of going to a reviewer.
|
|
291
|
+
Never name a build task's assignee as the goal's reviewer; the API refuses it. Make them small
|
|
289
292
|
and independently testable so several agents can work at once. Use \`blockedBy\` only where
|
|
290
293
|
one task truly needs another's result. Each description is self-contained: what to change,
|
|
291
294
|
which files, how to test it, and the acceptance criteria.
|
|
@@ -297,24 +300,42 @@ Find the default branch with \`git remote show origin\`.
|
|
|
297
300
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
298
301
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
299
302
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
300
|
-
6.
|
|
303
|
+
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
304
|
+
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
305
|
+
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
306
|
+
the planning task's thread and complete it.
|
|
301
307
|
|
|
302
308
|
**orderRole "build"**:
|
|
303
309
|
- \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
|
|
304
310
|
test and commit. Do not create another branch or PR.
|
|
305
311
|
- Push with \`git pull --rebase origin order/<orderId> && git push origin HEAD:order/<orderId>\`,
|
|
306
312
|
repeating up to 5 times if the push is rejected. Never force-push.
|
|
313
|
+
- If you drop or change any part of what the goal asked for, even for a good reason, record each one
|
|
314
|
+
with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
|
|
315
|
+
"reason": "<why>" }]\` and say so in your handoff. The owner decides whether to accept it.
|
|
307
316
|
- Hand off on the task's thread with the commit sha and what you tested, then complete.
|
|
308
317
|
|
|
309
318
|
**orderRole "gate"**:
|
|
310
319
|
- 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\`
|
|
320
|
+
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
321
|
gate on the dashboard and the named reviewer decides a \`reviewer_agent\` gate.
|
|
322
|
+
- As the reviewer of an order, check each thing the goal asked for. Anything dropped or changed is
|
|
323
|
+
the owner's call, not yours: if it is recorded in \`metadata.deviations\`, mention it and judge
|
|
324
|
+
the rest; if it is not recorded, reject and name it so the builder records it. The owner decides
|
|
325
|
+
on deviations in the approval gate.
|
|
313
326
|
|
|
314
327
|
**orderRole "merge"**:
|
|
315
328
|
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
|
|
329
|
+
Before merging, replace the PR body with a run report built from
|
|
330
|
+
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
331
|
+
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
332
|
+
completed times, the number of distinct agents that worked on it, the total elapsed time
|
|
333
|
+
from the planning task's creation, and the CI checks that ran. Use \`gh pr edit <prUrl> --body-file <file>\`.
|
|
316
334
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
317
|
-
gh pr view --json headRefOid
|
|
335
|
+
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
336
|
+
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
337
|
+
\`gh pr edit <prUrl> --body-file <file>\` (a merged PR still accepts an edit). Post the merge commit
|
|
338
|
+
on the thread, and complete.
|
|
318
339
|
3. A check failed: fix it on \`order/<orderId>\` as a build task would, push, and go back to step 1.
|
|
319
340
|
After 3 failed rounds, block the task with \`blockedReason\` naming the failing checks.`;
|
|
320
341
|
function buildPrompt(config2) {
|
|
@@ -469,7 +490,7 @@ function mcpServerLaunch(here = fileURLToPath(import.meta.url), exists = existsS
|
|
|
469
490
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
470
491
|
return { command: execPath, args: [mcpServerTsxCli(), mcpServerEntry()] };
|
|
471
492
|
}
|
|
472
|
-
function writeMcpConfig(config2) {
|
|
493
|
+
function writeMcpConfig(config2, prefix = "claude-worker") {
|
|
473
494
|
const { command, args } = mcpServerLaunch();
|
|
474
495
|
const mcpConfig = {
|
|
475
496
|
mcpServers: {
|
|
@@ -485,7 +506,7 @@ function writeMcpConfig(config2) {
|
|
|
485
506
|
}
|
|
486
507
|
}
|
|
487
508
|
};
|
|
488
|
-
const path = join(tmpdir(),
|
|
509
|
+
const path = join(tmpdir(), `${prefix}-${config2.agentId}-${Date.now()}.json`);
|
|
489
510
|
writeFileSync(path, JSON.stringify(mcpConfig), { mode: 384 });
|
|
490
511
|
return path;
|
|
491
512
|
}
|
|
@@ -724,9 +745,111 @@ var claudeAdapter = {
|
|
|
724
745
|
classifyError: classifySessionError
|
|
725
746
|
};
|
|
726
747
|
|
|
748
|
+
// ../claude-worker/src/adapters/copilot.ts
|
|
749
|
+
import { spawn as spawn3 } from "child_process";
|
|
750
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
751
|
+
var bin2 = () => process.env.COPILOT_BIN ?? "copilot";
|
|
752
|
+
var POLICY_BLOCKS = [/access denied by policy/i, /third-party mcp servers are disabled/i];
|
|
753
|
+
var QUOTA = [/premium request/i, /monthly (?:limit|allowance|quota)/i];
|
|
754
|
+
function copilotArgs(repoPath, prompt, mcpConfigPath) {
|
|
755
|
+
return [
|
|
756
|
+
"-p",
|
|
757
|
+
prompt,
|
|
758
|
+
"--allow-all-tools",
|
|
759
|
+
"--output-format",
|
|
760
|
+
"json",
|
|
761
|
+
"--no-color",
|
|
762
|
+
"-C",
|
|
763
|
+
repoPath,
|
|
764
|
+
"--additional-mcp-config",
|
|
765
|
+
`@${mcpConfigPath}`,
|
|
766
|
+
"--disable-builtin-mcps"
|
|
767
|
+
];
|
|
768
|
+
}
|
|
769
|
+
function readCopilotEvent(line) {
|
|
770
|
+
let event;
|
|
771
|
+
try {
|
|
772
|
+
event = JSON.parse(line);
|
|
773
|
+
} catch {
|
|
774
|
+
return {};
|
|
775
|
+
}
|
|
776
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
777
|
+
const data = event.data ?? {};
|
|
778
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
779
|
+
if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
|
|
780
|
+
if (type === "session.error") return { error: message || "copilot session error" };
|
|
781
|
+
if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
|
|
782
|
+
if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
|
|
783
|
+
return {};
|
|
784
|
+
}
|
|
785
|
+
async function runCopilotSession(config2) {
|
|
786
|
+
const mcpConfigPath = writeMcpConfig(config2, "copilot-worker");
|
|
787
|
+
const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath);
|
|
788
|
+
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
789
|
+
return new Promise((resolve, reject) => {
|
|
790
|
+
const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
791
|
+
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
792
|
+
const toolsUsed = [];
|
|
793
|
+
let buffer = "";
|
|
794
|
+
let sessionError = "";
|
|
795
|
+
let stderrOutput = "";
|
|
796
|
+
proc.stdout.on("data", (chunk) => {
|
|
797
|
+
buffer += chunk.toString();
|
|
798
|
+
const lines = buffer.split("\n");
|
|
799
|
+
buffer = lines.pop() ?? "";
|
|
800
|
+
for (const line of lines) {
|
|
801
|
+
const { tool, error } = readCopilotEvent(line);
|
|
802
|
+
if (tool) {
|
|
803
|
+
toolsUsed.push(tool);
|
|
804
|
+
process.stdout.write(` \u2699 ${tool}
|
|
805
|
+
`);
|
|
806
|
+
}
|
|
807
|
+
if (error) {
|
|
808
|
+
sessionError = error;
|
|
809
|
+
console.error(`[copilot-worker] Session failed: ${error}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
proc.stderr.on("data", (chunk) => {
|
|
814
|
+
stderrOutput += chunk.toString();
|
|
815
|
+
});
|
|
816
|
+
proc.on("close", (code, signal) => {
|
|
817
|
+
try {
|
|
818
|
+
unlinkSync2(mcpConfigPath);
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
822
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
823
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
824
|
+
reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
825
|
+
} else {
|
|
826
|
+
resolve([...new Set(toolsUsed)]);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
proc.on("error", (err) => {
|
|
830
|
+
try {
|
|
831
|
+
unlinkSync2(mcpConfigPath);
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
reject(err);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
var copilotAdapter = {
|
|
839
|
+
runtime: "copilot",
|
|
840
|
+
prepare() {
|
|
841
|
+
},
|
|
842
|
+
runSession: runCopilotSession,
|
|
843
|
+
classifyError: (text) => {
|
|
844
|
+
if (POLICY_BLOCKS.some((re) => re.test(text))) return "credentials";
|
|
845
|
+
if (QUOTA.some((re) => re.test(text))) return "quota";
|
|
846
|
+
return classifySessionError(text);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
|
|
727
850
|
// ../claude-worker/src/adapters/index.ts
|
|
728
|
-
var RUNTIMES = ["claude", "cursor"];
|
|
729
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter };
|
|
851
|
+
var RUNTIMES = ["claude", "cursor", "copilot"];
|
|
852
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
|
|
730
853
|
function getAdapter(runtime) {
|
|
731
854
|
const adapter = ADAPTERS[runtime];
|
|
732
855
|
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
|
}
|
|
@@ -12531,6 +12537,7 @@ function buildCreateOrderTool(client, repoId, opts) {
|
|
|
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
12539
|
feature: external_exports.string().min(1).describe("What to build, in the requester's words."),
|
|
12540
|
+
title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal. The planner writes one when omitted."),
|
|
12534
12541
|
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
12542
|
budget: external_exports.record(external_exports.unknown()).optional().describe("Optional ceilings. Stored only; not enforced.")
|
|
12536
12543
|
}),
|
|
@@ -12539,6 +12546,7 @@ function buildCreateOrderTool(client, repoId, opts) {
|
|
|
12539
12546
|
const created = await client.createOrder({
|
|
12540
12547
|
repoId: targetRepo(input),
|
|
12541
12548
|
feature: input.feature,
|
|
12549
|
+
...input.title ? { title: input.title } : {},
|
|
12542
12550
|
acceptance: input.acceptance,
|
|
12543
12551
|
budget: input.budget
|
|
12544
12552
|
});
|
|
@@ -12553,15 +12561,17 @@ function buildCreateOrderTool(client, repoId, opts) {
|
|
|
12553
12561
|
function buildUpdateOrderTool(client) {
|
|
12554
12562
|
return {
|
|
12555
12563
|
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,
|
|
12564
|
+
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
12565
|
inputSchema: external_exports.object({
|
|
12558
12566
|
id: external_exports.string().describe("The order id (ord_\u2026)."),
|
|
12567
|
+
title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal."),
|
|
12559
12568
|
branch: external_exports.string().min(1).optional().describe("The order's branch."),
|
|
12560
12569
|
prUrl: external_exports.string().url().optional().describe("The order's pull request URL."),
|
|
12561
12570
|
cancel: external_exports.boolean().optional().describe("Set true to cancel the order.")
|
|
12562
12571
|
}),
|
|
12563
12572
|
handler: async (input) => {
|
|
12564
12573
|
const body = {};
|
|
12574
|
+
if (input.title !== void 0) body.title = input.title;
|
|
12565
12575
|
if (input.branch !== void 0) body.branch = input.branch;
|
|
12566
12576
|
if (input.prUrl !== void 0) body.prUrl = input.prUrl;
|
|
12567
12577
|
if (input.cancel) body.status = "cancelled";
|
|
@@ -12574,6 +12584,46 @@ function buildUpdateOrderTool(client) {
|
|
|
12574
12584
|
}
|
|
12575
12585
|
};
|
|
12576
12586
|
}
|
|
12587
|
+
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.";
|
|
12588
|
+
function buildListPoolsTool(client, repoId, opts) {
|
|
12589
|
+
const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
|
|
12590
|
+
return {
|
|
12591
|
+
name: "list_pools",
|
|
12592
|
+
description: "List the worker pool sizes set for a repo. " + POOL_NOTE,
|
|
12593
|
+
inputSchema: external_exports.object({
|
|
12594
|
+
repoId: external_exports.string().optional().describe("Repo to read. Defaults to this agent's home repo.")
|
|
12595
|
+
}),
|
|
12596
|
+
handler: async (input) => {
|
|
12597
|
+
try {
|
|
12598
|
+
const data = await client.listPools(targetRepo(input));
|
|
12599
|
+
return { content: [{ type: "text", text: JSON.stringify({ data }, null, 2) }] };
|
|
12600
|
+
} catch (err) {
|
|
12601
|
+
return mcpFail(err);
|
|
12602
|
+
}
|
|
12603
|
+
}
|
|
12604
|
+
};
|
|
12605
|
+
}
|
|
12606
|
+
function buildSetPoolSizeTool(client, repoId, opts) {
|
|
12607
|
+
const targetRepo = (input) => opts.ownerScoped && input.repoId || repoId;
|
|
12608
|
+
return {
|
|
12609
|
+
name: "set_pool_size",
|
|
12610
|
+
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." : ""),
|
|
12611
|
+
inputSchema: external_exports.object({
|
|
12612
|
+
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."),
|
|
12613
|
+
pool: external_exports.string().min(1).describe("The pool name from the node spec, e.g. 'ni-pool'."),
|
|
12614
|
+
min: external_exports.number().int().min(0).max(32).optional().describe("Members to keep running when idle. Defaults to 0."),
|
|
12615
|
+
max: external_exports.number().int().min(1).max(32).describe("Most members to run at once.")
|
|
12616
|
+
}),
|
|
12617
|
+
handler: async (input) => {
|
|
12618
|
+
try {
|
|
12619
|
+
const row = await client.setPoolSize(targetRepo(input), input.pool, { min: input.min ?? 0, max: input.max });
|
|
12620
|
+
return { content: [{ type: "text", text: JSON.stringify(row, null, 2) }] };
|
|
12621
|
+
} catch (err) {
|
|
12622
|
+
return mcpFail(err);
|
|
12623
|
+
}
|
|
12624
|
+
}
|
|
12625
|
+
};
|
|
12626
|
+
}
|
|
12577
12627
|
function resolveAgentRef(candidates, ref, scope) {
|
|
12578
12628
|
const available = candidates.length ? ` Available: ${candidates.map((a) => `${a.name} (${a.id})`).join(", ")}` : ` ${scope} has no agents.`;
|
|
12579
12629
|
if (ref.startsWith("agent_")) {
|
|
@@ -13169,6 +13219,8 @@ ${version.body}`
|
|
|
13169
13219
|
...buildOrderReadTools(client, repoId, opts),
|
|
13170
13220
|
...opts.ownerScoped || opts.role === "orchestrator" ? [buildCreateOrderTool(client, repoId, opts)] : [],
|
|
13171
13221
|
...opts.role === "orchestrator" || (opts.tier ?? 0) >= 2 ? [buildUpdateOrderTool(client)] : [],
|
|
13222
|
+
buildListPoolsTool(client, repoId, opts),
|
|
13223
|
+
...opts.ownerScoped || opts.role === "orchestrator" ? [buildSetPoolSizeTool(client, repoId, opts)] : [],
|
|
13172
13224
|
// Additive, never a swap: an owner-scoped agent is still an agent and needs
|
|
13173
13225
|
// everything above. buildOperatorTools assumes no agent identity, and its
|
|
13174
13226
|
// create_task bypasses propose/commit.
|