@pitboss/cli 0.4.5 → 0.4.7
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 +195 -46
- package/dist/event-worker.js +158 -9
- package/dist/mcp.js +47 -15
- 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
|
|
@@ -20601,12 +20601,12 @@ function hookCursorPromptCommand(opts) {
|
|
|
20601
20601
|
|
|
20602
20602
|
// src/commands/node.ts
|
|
20603
20603
|
import { execFileSync as execFileSync7, spawn as spawnChild } from "node:child_process";
|
|
20604
|
-
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";
|
|
20605
20605
|
import { dirname as dirname11, resolve as resolve7 } from "node:path";
|
|
20606
20606
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
20607
20607
|
|
|
20608
20608
|
// src/node/spec.ts
|
|
20609
|
-
import { join as
|
|
20609
|
+
import { join as join15, resolve as resolve4, sep as sep2 } from "node:path";
|
|
20610
20610
|
|
|
20611
20611
|
// ../claude-worker/src/session.ts
|
|
20612
20612
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -20885,10 +20885,15 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20885
20885
|
**orderRole "planning"** (you are the order's planner):
|
|
20886
20886
|
1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it. If the order
|
|
20887
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.
|
|
20888
20891
|
2. Create the branch and PR:
|
|
20889
|
-
\`git fetch origin && git checkout
|
|
20890
|
-
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push
|
|
20892
|
+
\`git fetch origin && git checkout --detach origin/<default>\`,
|
|
20893
|
+
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push origin HEAD:refs/heads/order/<orderId>\`,
|
|
20891
20894
|
then \`gh pr create --draft --base <default> --head order/<orderId> --title "<feature>" --body "Order <orderId>"\`.
|
|
20895
|
+
Work detached and never check out \`order/<orderId>\` as a local branch: every agent's worktree
|
|
20896
|
+
shares one repository, and git lets only one worktree hold a branch.
|
|
20892
20897
|
Record both with \`mcp__relai__update_order\` (\`branch\`, \`prUrl\`).
|
|
20893
20898
|
3. Build tasks: \`mcp__relai__create_task\` for each piece of work, with \`epicId\` = the order's
|
|
20894
20899
|
\`epicId\`, \`assignedTo: "@auto"\`, and
|
|
@@ -20908,15 +20913,27 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20908
20913
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
20909
20914
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
20910
20915
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
20916
|
+
If the order has \`pairBuilds\`, every build task needs \`verifyKind: "reviewer_agent"\` and a
|
|
20917
|
+
\`verifyReviewerId\` on a different \`workerType\` from the writers (see \`mcp__relai__list_agents\`);
|
|
20918
|
+
the API refuses a paired build task without one and routes it away from the reviewer's vendor.
|
|
20911
20919
|
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
20912
20920
|
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
20913
20921
|
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
20914
20922
|
the planning task's thread and complete it.
|
|
20915
20923
|
|
|
20924
|
+
**orderRole "plan_review"** (assigned to the planner, reviewed by another agent):
|
|
20925
|
+
- After the task graph is written, complete each plan review task. That hands the plan to its
|
|
20926
|
+
reviewer. Every build task waits until all plan reviews are approved.
|
|
20927
|
+
- On a reject the task comes back to you with the reviewer's note: revise the plan under the Epic
|
|
20928
|
+
(edit, add or cancel tasks), reply on the task's thread with what changed, and complete it again.
|
|
20929
|
+
- As the reviewer of a plan review: read the goal and every task under its Epic. Approve when the
|
|
20930
|
+
tasks cover the goal, are small and independent, and their \`blockedBy\` order is right; otherwise
|
|
20931
|
+
reject with exactly what to change.
|
|
20932
|
+
|
|
20916
20933
|
**orderRole "build"**:
|
|
20917
|
-
- \`git fetch origin && git checkout
|
|
20918
|
-
|
|
20919
|
-
- Push with \`git
|
|
20934
|
+
- \`git fetch origin && git checkout --detach origin/order/<orderId>\`, then implement, test and commit
|
|
20935
|
+
on the detached head. Do not create a local branch, another branch on the remote, or a PR.
|
|
20936
|
+
- Push with \`git fetch origin && git rebase origin/order/<orderId> && git push origin HEAD:order/<orderId>\`,
|
|
20920
20937
|
repeating up to 5 times if the push is rejected. Never force-push.
|
|
20921
20938
|
- If you drop or change any part of what the goal asked for, even for a good reason, record each one
|
|
20922
20939
|
with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
|
|
@@ -20933,12 +20950,15 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20933
20950
|
on deviations in the approval gate.
|
|
20934
20951
|
|
|
20935
20952
|
**orderRole "merge"**:
|
|
20936
|
-
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
|
|
20953
|
+
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`. If the PR has no checks
|
|
20954
|
+
(the repo has no CI), check out the PR head and run the repo's own lint, typecheck and test scripts
|
|
20955
|
+
from its package manifest instead, and treat a failure there as a failed check.
|
|
20937
20956
|
Before merging, replace the PR body with a run report built from
|
|
20938
20957
|
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
20939
20958
|
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
20940
20959
|
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.
|
|
20960
|
+
from the planning task's creation, and the CI checks that ran. Keep any \`Closes #<n>\` line. Use
|
|
20961
|
+
\`gh pr edit <prUrl> --body-file <file>\`.
|
|
20942
20962
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
20943
20963
|
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
20944
20964
|
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
@@ -21332,6 +21352,7 @@ async function runCursorSession(config) {
|
|
|
21332
21352
|
}
|
|
21333
21353
|
var cursorAdapter = {
|
|
21334
21354
|
runtime: "cursor",
|
|
21355
|
+
workerType: "cursor",
|
|
21335
21356
|
prepare(ctx) {
|
|
21336
21357
|
ctx.writeMcpServer(join13(ctx.worktree, ".cursor", "mcp.json"), "relai", {
|
|
21337
21358
|
command: ctx.mcp.command,
|
|
@@ -21347,6 +21368,7 @@ var cursorAdapter = {
|
|
|
21347
21368
|
// ../claude-worker/src/adapters/claude.ts
|
|
21348
21369
|
var claudeAdapter = {
|
|
21349
21370
|
runtime: "claude",
|
|
21371
|
+
workerType: "claude",
|
|
21350
21372
|
prepare() {
|
|
21351
21373
|
},
|
|
21352
21374
|
runSession: runClaudeSession,
|
|
@@ -21445,6 +21467,7 @@ async function runCopilotSession(config) {
|
|
|
21445
21467
|
}
|
|
21446
21468
|
var copilotAdapter = {
|
|
21447
21469
|
runtime: "copilot",
|
|
21470
|
+
workerType: "copilot",
|
|
21448
21471
|
prepare() {
|
|
21449
21472
|
},
|
|
21450
21473
|
runSession: runCopilotSession,
|
|
@@ -21455,9 +21478,135 @@ var copilotAdapter = {
|
|
|
21455
21478
|
}
|
|
21456
21479
|
};
|
|
21457
21480
|
|
|
21481
|
+
// ../claude-worker/src/adapters/codex.ts
|
|
21482
|
+
import { spawn as spawn5 } from "child_process";
|
|
21483
|
+
import { chmodSync, mkdirSync as mkdirSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
21484
|
+
import { homedir as homedir5 } from "os";
|
|
21485
|
+
import { join as join14 } from "path";
|
|
21486
|
+
var bin3 = () => process.env.CODEX_BIN ?? "codex";
|
|
21487
|
+
var codexHome = () => process.env.CODEX_HOME ?? join14(homedir5(), ".codex");
|
|
21488
|
+
var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
|
|
21489
|
+
var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
|
|
21490
|
+
function codexArgs(repoPath, prompt, profile) {
|
|
21491
|
+
return [
|
|
21492
|
+
"exec",
|
|
21493
|
+
"--json",
|
|
21494
|
+
"--ephemeral",
|
|
21495
|
+
"--skip-git-repo-check",
|
|
21496
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
21497
|
+
"-C",
|
|
21498
|
+
repoPath,
|
|
21499
|
+
"-p",
|
|
21500
|
+
profile,
|
|
21501
|
+
prompt
|
|
21502
|
+
];
|
|
21503
|
+
}
|
|
21504
|
+
var tomlString = (v) => JSON.stringify(v);
|
|
21505
|
+
function codexProfileToml(server, env2) {
|
|
21506
|
+
return [
|
|
21507
|
+
"[mcp_servers.relai]",
|
|
21508
|
+
`command = ${tomlString(server.command)}`,
|
|
21509
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
21510
|
+
"",
|
|
21511
|
+
"[mcp_servers.relai.env]",
|
|
21512
|
+
...Object.entries(env2).map(([k, v]) => `${k} = ${tomlString(v)}`),
|
|
21513
|
+
""
|
|
21514
|
+
].join("\n");
|
|
21515
|
+
}
|
|
21516
|
+
function readCodexEvent(line) {
|
|
21517
|
+
let event;
|
|
21518
|
+
try {
|
|
21519
|
+
event = JSON.parse(line);
|
|
21520
|
+
} catch {
|
|
21521
|
+
return {};
|
|
21522
|
+
}
|
|
21523
|
+
if (event.type === "item.started" && event.item) {
|
|
21524
|
+
if (event.item.type === "command_execution") return { tool: "shell" };
|
|
21525
|
+
if (event.item.type === "mcp_tool_call") return { tool: `${String(event.item.server ?? "mcp")}-${String(event.item.tool ?? "tool")}` };
|
|
21526
|
+
return {};
|
|
21527
|
+
}
|
|
21528
|
+
if (event.type === "turn.failed") return { error: String(event.error?.message ?? "codex turn failed") };
|
|
21529
|
+
if (event.type === "error") return { error: String(event.message ?? "codex error") };
|
|
21530
|
+
return {};
|
|
21531
|
+
}
|
|
21532
|
+
async function runCodexSession(config) {
|
|
21533
|
+
const profile = `pitboss-${config.agentId}`;
|
|
21534
|
+
mkdirSync6(codexHome(), { recursive: true, mode: 448 });
|
|
21535
|
+
const profilePath = join14(codexHome(), `${profile}.config.toml`);
|
|
21536
|
+
writeFileSync7(profilePath, codexProfileToml(mcpServerLaunch(), {
|
|
21537
|
+
API_URL: config.apiUrl,
|
|
21538
|
+
API_SECRET: config.apiSecret,
|
|
21539
|
+
AGENT_ID: config.agentId,
|
|
21540
|
+
REPO_ID: config.repoId
|
|
21541
|
+
}), { mode: 384 });
|
|
21542
|
+
chmodSync(profilePath, 384);
|
|
21543
|
+
const cleanup = () => {
|
|
21544
|
+
try {
|
|
21545
|
+
unlinkSync3(profilePath);
|
|
21546
|
+
} catch {
|
|
21547
|
+
}
|
|
21548
|
+
};
|
|
21549
|
+
const args = codexArgs(config.repoPath, buildPrompt(config), profile);
|
|
21550
|
+
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
21551
|
+
return new Promise((resolve8, reject) => {
|
|
21552
|
+
const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
21553
|
+
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
21554
|
+
const toolsUsed = [];
|
|
21555
|
+
let buffer = "";
|
|
21556
|
+
let sessionError = "";
|
|
21557
|
+
let stderrOutput = "";
|
|
21558
|
+
proc.stdout.on("data", (chunk) => {
|
|
21559
|
+
buffer += chunk.toString();
|
|
21560
|
+
const lines = buffer.split("\n");
|
|
21561
|
+
buffer = lines.pop() ?? "";
|
|
21562
|
+
for (const line of lines) {
|
|
21563
|
+
const { tool, error } = readCodexEvent(line);
|
|
21564
|
+
if (tool) {
|
|
21565
|
+
toolsUsed.push(tool);
|
|
21566
|
+
process.stdout.write(` \u2699 ${tool}
|
|
21567
|
+
`);
|
|
21568
|
+
}
|
|
21569
|
+
if (error) {
|
|
21570
|
+
sessionError = error;
|
|
21571
|
+
console.error(`[codex-worker] Session failed: ${error}`);
|
|
21572
|
+
}
|
|
21573
|
+
}
|
|
21574
|
+
});
|
|
21575
|
+
proc.stderr.on("data", (chunk) => {
|
|
21576
|
+
stderrOutput += chunk.toString();
|
|
21577
|
+
});
|
|
21578
|
+
proc.on("close", (code, signal) => {
|
|
21579
|
+
cleanup();
|
|
21580
|
+
console.log(`[codex-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
21581
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
21582
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
21583
|
+
reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
21584
|
+
} else {
|
|
21585
|
+
resolve8([...new Set(toolsUsed)]);
|
|
21586
|
+
}
|
|
21587
|
+
});
|
|
21588
|
+
proc.on("error", (err) => {
|
|
21589
|
+
cleanup();
|
|
21590
|
+
reject(err);
|
|
21591
|
+
});
|
|
21592
|
+
});
|
|
21593
|
+
}
|
|
21594
|
+
var codexAdapter = {
|
|
21595
|
+
runtime: "codex",
|
|
21596
|
+
workerType: "gpt",
|
|
21597
|
+
prepare() {
|
|
21598
|
+
},
|
|
21599
|
+
runSession: runCodexSession,
|
|
21600
|
+
classifyError: (text) => {
|
|
21601
|
+
if (QUOTA2.some((re) => re.test(text))) return "quota";
|
|
21602
|
+
if (CREDENTIALS.some((re) => re.test(text))) return "credentials";
|
|
21603
|
+
return classifySessionError(text);
|
|
21604
|
+
}
|
|
21605
|
+
};
|
|
21606
|
+
|
|
21458
21607
|
// ../claude-worker/src/adapters/index.ts
|
|
21459
|
-
var RUNTIMES3 = ["claude", "cursor", "copilot"];
|
|
21460
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
|
|
21608
|
+
var RUNTIMES3 = ["claude", "cursor", "copilot", "codex"];
|
|
21609
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter, codex: codexAdapter };
|
|
21461
21610
|
function getAdapter(runtime) {
|
|
21462
21611
|
const adapter = ADAPTERS[runtime];
|
|
21463
21612
|
if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
|
|
@@ -21482,7 +21631,7 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
|
|
|
21482
21631
|
function poolMembers(pool) {
|
|
21483
21632
|
return Array.from({ length: pool.max }, (_, i) => {
|
|
21484
21633
|
const name = `${pool.pool}-${i + 1}`;
|
|
21485
|
-
return { name, runtime: pool.runtime, worktree:
|
|
21634
|
+
return { name, runtime: pool.runtime, worktree: join15(pool.worktreeRoot, name), specialization: pool.specialization };
|
|
21486
21635
|
});
|
|
21487
21636
|
}
|
|
21488
21637
|
var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
|
|
@@ -21570,17 +21719,17 @@ function parseSpec(text, specDir) {
|
|
|
21570
21719
|
}
|
|
21571
21720
|
|
|
21572
21721
|
// src/node/store.ts
|
|
21573
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
21574
|
-
import { dirname as dirname8, join as
|
|
21722
|
+
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";
|
|
21723
|
+
import { dirname as dirname8, join as join16 } from "node:path";
|
|
21575
21724
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
21576
|
-
var nodeDir = () =>
|
|
21577
|
-
var credsPath = (name) =>
|
|
21578
|
-
var statePath2 = () =>
|
|
21579
|
-
var logPath = (name) =>
|
|
21725
|
+
var nodeDir = () => join16(configDir(), "node");
|
|
21726
|
+
var credsPath = (name) => join16(nodeDir(), "agents", `${name}.json`);
|
|
21727
|
+
var statePath2 = () => join16(nodeDir(), "state.json");
|
|
21728
|
+
var logPath = (name) => join16(nodeDir(), "logs", `${name}.log`);
|
|
21580
21729
|
function writePrivate(path3, value) {
|
|
21581
|
-
|
|
21730
|
+
mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
|
|
21582
21731
|
const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
|
|
21583
|
-
|
|
21732
|
+
writeFileSync8(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
21584
21733
|
renameSync4(tmp, path3);
|
|
21585
21734
|
}
|
|
21586
21735
|
function readJson(path3) {
|
|
@@ -21594,7 +21743,7 @@ var removeCreds = (name) => {
|
|
|
21594
21743
|
if (existsSync8(path3)) rmSync4(path3);
|
|
21595
21744
|
};
|
|
21596
21745
|
var listStoredAgentNames = () => {
|
|
21597
|
-
const dir =
|
|
21746
|
+
const dir = join16(nodeDir(), "agents");
|
|
21598
21747
|
if (!existsSync8(dir)) return [];
|
|
21599
21748
|
return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
21600
21749
|
};
|
|
@@ -21676,7 +21825,7 @@ async function redeemInvite(spec, agent, code, have, specPath) {
|
|
|
21676
21825
|
name: agent.name,
|
|
21677
21826
|
role: "worker",
|
|
21678
21827
|
specialization: agent.specialization,
|
|
21679
|
-
workerType: agent.runtime,
|
|
21828
|
+
workerType: getAdapter(agent.runtime).workerType,
|
|
21680
21829
|
domains: [],
|
|
21681
21830
|
repoPath
|
|
21682
21831
|
});
|
|
@@ -21769,7 +21918,7 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
21769
21918
|
name: agent.name,
|
|
21770
21919
|
role: invite.role,
|
|
21771
21920
|
specialization: agent.specialization,
|
|
21772
|
-
workerType: agent.runtime,
|
|
21921
|
+
workerType: getAdapter(agent.runtime).workerType,
|
|
21773
21922
|
domains: invite.domains ?? [],
|
|
21774
21923
|
repoPath
|
|
21775
21924
|
});
|
|
@@ -21920,27 +22069,27 @@ var Supervisor = class {
|
|
|
21920
22069
|
|
|
21921
22070
|
// src/node/entries.ts
|
|
21922
22071
|
import { existsSync as existsSync9 } from "node:fs";
|
|
21923
|
-
import { dirname as dirname9, join as
|
|
22072
|
+
import { dirname as dirname9, join as join17, resolve as resolve5 } from "node:path";
|
|
21924
22073
|
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.";
|
|
21925
22074
|
function checkoutWorker(dir) {
|
|
21926
22075
|
return {
|
|
21927
|
-
command:
|
|
21928
|
-
args: [
|
|
22076
|
+
command: join17(dir, "node_modules", ".bin", "tsx"),
|
|
22077
|
+
args: [join17(dir, "src", "index.ts")]
|
|
21929
22078
|
};
|
|
21930
22079
|
}
|
|
21931
22080
|
function checkoutMcp(dir, execPath) {
|
|
21932
22081
|
return {
|
|
21933
22082
|
command: execPath,
|
|
21934
22083
|
args: [
|
|
21935
|
-
|
|
21936
|
-
|
|
22084
|
+
join17(dir, "node_modules", "tsx", "dist", "cli.mjs"),
|
|
22085
|
+
join17(dir, "src", "index.ts")
|
|
21937
22086
|
]
|
|
21938
22087
|
};
|
|
21939
22088
|
}
|
|
21940
22089
|
function walkFor(here, packageName, exists) {
|
|
21941
22090
|
for (let dir = dirname9(here); dirname9(dir) !== dir; dir = dirname9(dir)) {
|
|
21942
|
-
const candidate =
|
|
21943
|
-
if (exists(
|
|
22091
|
+
const candidate = join17(dir, packageName);
|
|
22092
|
+
if (exists(join17(candidate, "src", "index.ts"))) return candidate;
|
|
21944
22093
|
}
|
|
21945
22094
|
return null;
|
|
21946
22095
|
}
|
|
@@ -21952,10 +22101,10 @@ function resolveEventWorker(opts) {
|
|
|
21952
22101
|
if (override) {
|
|
21953
22102
|
const resolved = resolve5(override);
|
|
21954
22103
|
if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
|
|
21955
|
-
if (exists(
|
|
22104
|
+
if (exists(join17(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
|
|
21956
22105
|
throw new Error(MISSING_WORKER);
|
|
21957
22106
|
}
|
|
21958
|
-
const bundled =
|
|
22107
|
+
const bundled = join17(dirname9(opts.here), "event-worker.js");
|
|
21959
22108
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21960
22109
|
const dir = walkFor(opts.here, "event-worker", exists);
|
|
21961
22110
|
if (dir) return checkoutWorker(dir);
|
|
@@ -21964,7 +22113,7 @@ function resolveEventWorker(opts) {
|
|
|
21964
22113
|
function resolveMcpServer(opts) {
|
|
21965
22114
|
const execPath = opts.execPath ?? process.execPath;
|
|
21966
22115
|
const exists = opts.exists ?? existsSync9;
|
|
21967
|
-
const bundled =
|
|
22116
|
+
const bundled = join17(dirname9(opts.here), "mcp.js");
|
|
21968
22117
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21969
22118
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
21970
22119
|
if (dir) return checkoutMcp(dir, execPath);
|
|
@@ -22074,8 +22223,8 @@ function loadPruneTargets(names) {
|
|
|
22074
22223
|
|
|
22075
22224
|
// src/node/pool.ts
|
|
22076
22225
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
22077
|
-
import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as
|
|
22078
|
-
import { basename as basename2, dirname as dirname10, join as
|
|
22226
|
+
import { existsSync as existsSync10, lstatSync as lstatSync3, mkdirSync as mkdirSync8, realpathSync as realpathSync4 } from "node:fs";
|
|
22227
|
+
import { basename as basename2, dirname as dirname10, join as join18, resolve as resolve6 } from "node:path";
|
|
22079
22228
|
var IDLE_STOP_MS = 10 * 6e4;
|
|
22080
22229
|
var RECONCILE_MS = 15e3;
|
|
22081
22230
|
var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
|
|
@@ -22168,7 +22317,7 @@ function memberPathProblem(s) {
|
|
|
22168
22317
|
function realpathAllowMissing(path3) {
|
|
22169
22318
|
const abs = resolve6(path3);
|
|
22170
22319
|
if (existsSync10(abs)) return realpathSync4.native(abs);
|
|
22171
|
-
return
|
|
22320
|
+
return join18(realpathAllowMissing(dirname10(abs)), basename2(abs));
|
|
22172
22321
|
}
|
|
22173
22322
|
function run2(cwd, args) {
|
|
22174
22323
|
try {
|
|
@@ -22221,7 +22370,7 @@ function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
|
22221
22370
|
}
|
|
22222
22371
|
const missing = members.filter((m) => !existsSync10(m.worktree));
|
|
22223
22372
|
if (missing.length > 0) {
|
|
22224
|
-
|
|
22373
|
+
mkdirSync8(root, { recursive: true });
|
|
22225
22374
|
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22226
22375
|
const branch = defaultBranch(pool.base);
|
|
22227
22376
|
run2(pool.base, ["worktree", "prune"]);
|
|
@@ -22280,13 +22429,13 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
22280
22429
|
writeMergedJson(target, (existing) => mergeMcpServer(existing, name, entry), root);
|
|
22281
22430
|
excludeIfUntracked(root, target);
|
|
22282
22431
|
},
|
|
22283
|
-
run(
|
|
22284
|
-
execFileSync7(
|
|
22432
|
+
run(bin4, args, cwd) {
|
|
22433
|
+
execFileSync7(bin4, args, { cwd, stdio: "ignore" });
|
|
22285
22434
|
}
|
|
22286
22435
|
});
|
|
22287
22436
|
}
|
|
22288
22437
|
function spawnWorker(spec) {
|
|
22289
|
-
|
|
22438
|
+
mkdirSync9(dirname11(spec.logPath), { recursive: true, mode: 448 });
|
|
22290
22439
|
const log = openSync(spec.logPath, "a", 384);
|
|
22291
22440
|
try {
|
|
22292
22441
|
const child = spawnChild(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, stdio: ["ignore", log, log], detached: true });
|
package/dist/event-worker.js
CHANGED
|
@@ -277,10 +277,15 @@ Find the default branch with \`git remote show origin\`.
|
|
|
277
277
|
**orderRole "planning"** (you are the order's planner):
|
|
278
278
|
1. Read the order's \`feature\` and \`acceptance\`, and read the repo enough to plan it. If the order
|
|
279
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.
|
|
280
283
|
2. Create the branch and PR:
|
|
281
|
-
\`git fetch origin && git checkout
|
|
282
|
-
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push
|
|
284
|
+
\`git fetch origin && git checkout --detach origin/<default>\`,
|
|
285
|
+
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push origin HEAD:refs/heads/order/<orderId>\`,
|
|
283
286
|
then \`gh pr create --draft --base <default> --head order/<orderId> --title "<feature>" --body "Order <orderId>"\`.
|
|
287
|
+
Work detached and never check out \`order/<orderId>\` as a local branch: every agent's worktree
|
|
288
|
+
shares one repository, and git lets only one worktree hold a branch.
|
|
284
289
|
Record both with \`mcp__relai__update_order\` (\`branch\`, \`prUrl\`).
|
|
285
290
|
3. Build tasks: \`mcp__relai__create_task\` for each piece of work, with \`epicId\` = the order's
|
|
286
291
|
\`epicId\`, \`assignedTo: "@auto"\`, and
|
|
@@ -300,15 +305,27 @@ Find the default branch with \`git remote show origin\`.
|
|
|
300
305
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
301
306
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
302
307
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
308
|
+
If the order has \`pairBuilds\`, every build task needs \`verifyKind: "reviewer_agent"\` and a
|
|
309
|
+
\`verifyReviewerId\` on a different \`workerType\` from the writers (see \`mcp__relai__list_agents\`);
|
|
310
|
+
the API refuses a paired build task without one and routes it away from the reviewer's vendor.
|
|
303
311
|
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
304
312
|
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
305
313
|
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
306
314
|
the planning task's thread and complete it.
|
|
307
315
|
|
|
316
|
+
**orderRole "plan_review"** (assigned to the planner, reviewed by another agent):
|
|
317
|
+
- After the task graph is written, complete each plan review task. That hands the plan to its
|
|
318
|
+
reviewer. Every build task waits until all plan reviews are approved.
|
|
319
|
+
- On a reject the task comes back to you with the reviewer's note: revise the plan under the Epic
|
|
320
|
+
(edit, add or cancel tasks), reply on the task's thread with what changed, and complete it again.
|
|
321
|
+
- As the reviewer of a plan review: read the goal and every task under its Epic. Approve when the
|
|
322
|
+
tasks cover the goal, are small and independent, and their \`blockedBy\` order is right; otherwise
|
|
323
|
+
reject with exactly what to change.
|
|
324
|
+
|
|
308
325
|
**orderRole "build"**:
|
|
309
|
-
- \`git fetch origin && git checkout
|
|
310
|
-
|
|
311
|
-
- Push with \`git
|
|
326
|
+
- \`git fetch origin && git checkout --detach origin/order/<orderId>\`, then implement, test and commit
|
|
327
|
+
on the detached head. Do not create a local branch, another branch on the remote, or a PR.
|
|
328
|
+
- Push with \`git fetch origin && git rebase origin/order/<orderId> && git push origin HEAD:order/<orderId>\`,
|
|
312
329
|
repeating up to 5 times if the push is rejected. Never force-push.
|
|
313
330
|
- If you drop or change any part of what the goal asked for, even for a good reason, record each one
|
|
314
331
|
with \`mcp__relai__update_task_status\` metadata \`deviations: [{ "item": "<what the goal asked>",
|
|
@@ -325,12 +342,15 @@ Find the default branch with \`git remote show origin\`.
|
|
|
325
342
|
on deviations in the approval gate.
|
|
326
343
|
|
|
327
344
|
**orderRole "merge"**:
|
|
328
|
-
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`.
|
|
345
|
+
1. \`gh pr ready <prUrl>\`, then \`gh pr checks <prUrl> --watch --fail-fast\`. If the PR has no checks
|
|
346
|
+
(the repo has no CI), check out the PR head and run the repo's own lint, typecheck and test scripts
|
|
347
|
+
from its package manifest instead, and treat a failure there as a failed check.
|
|
329
348
|
Before merging, replace the PR body with a run report built from
|
|
330
349
|
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
331
350
|
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
332
351
|
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.
|
|
352
|
+
from the planning task's creation, and the CI checks that ran. Keep any \`Closes #<n>\` line. Use
|
|
353
|
+
\`gh pr edit <prUrl> --body-file <file>\`.
|
|
334
354
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
335
355
|
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
336
356
|
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
@@ -724,6 +744,7 @@ async function runCursorSession(config2) {
|
|
|
724
744
|
}
|
|
725
745
|
var cursorAdapter = {
|
|
726
746
|
runtime: "cursor",
|
|
747
|
+
workerType: "cursor",
|
|
727
748
|
prepare(ctx) {
|
|
728
749
|
ctx.writeMcpServer(join2(ctx.worktree, ".cursor", "mcp.json"), "relai", {
|
|
729
750
|
command: ctx.mcp.command,
|
|
@@ -739,6 +760,7 @@ var cursorAdapter = {
|
|
|
739
760
|
// ../claude-worker/src/adapters/claude.ts
|
|
740
761
|
var claudeAdapter = {
|
|
741
762
|
runtime: "claude",
|
|
763
|
+
workerType: "claude",
|
|
742
764
|
prepare() {
|
|
743
765
|
},
|
|
744
766
|
runSession: runClaudeSession,
|
|
@@ -837,6 +859,7 @@ async function runCopilotSession(config2) {
|
|
|
837
859
|
}
|
|
838
860
|
var copilotAdapter = {
|
|
839
861
|
runtime: "copilot",
|
|
862
|
+
workerType: "copilot",
|
|
840
863
|
prepare() {
|
|
841
864
|
},
|
|
842
865
|
runSession: runCopilotSession,
|
|
@@ -847,9 +870,135 @@ var copilotAdapter = {
|
|
|
847
870
|
}
|
|
848
871
|
};
|
|
849
872
|
|
|
873
|
+
// ../claude-worker/src/adapters/codex.ts
|
|
874
|
+
import { spawn as spawn4 } from "child_process";
|
|
875
|
+
import { chmodSync, mkdirSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
876
|
+
import { homedir } from "os";
|
|
877
|
+
import { join as join3 } from "path";
|
|
878
|
+
var bin3 = () => process.env.CODEX_BIN ?? "codex";
|
|
879
|
+
var codexHome = () => process.env.CODEX_HOME ?? join3(homedir(), ".codex");
|
|
880
|
+
var QUOTA2 = [/usage limit/i, /rate limit reached for/i, /upgrade to (plus|pro)/i];
|
|
881
|
+
var CREDENTIALS = [/not logged in/i, /codex login/i, /401 unauthorized/i];
|
|
882
|
+
function codexArgs(repoPath, prompt, profile) {
|
|
883
|
+
return [
|
|
884
|
+
"exec",
|
|
885
|
+
"--json",
|
|
886
|
+
"--ephemeral",
|
|
887
|
+
"--skip-git-repo-check",
|
|
888
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
889
|
+
"-C",
|
|
890
|
+
repoPath,
|
|
891
|
+
"-p",
|
|
892
|
+
profile,
|
|
893
|
+
prompt
|
|
894
|
+
];
|
|
895
|
+
}
|
|
896
|
+
var tomlString = (v) => JSON.stringify(v);
|
|
897
|
+
function codexProfileToml(server, env) {
|
|
898
|
+
return [
|
|
899
|
+
"[mcp_servers.relai]",
|
|
900
|
+
`command = ${tomlString(server.command)}`,
|
|
901
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
902
|
+
"",
|
|
903
|
+
"[mcp_servers.relai.env]",
|
|
904
|
+
...Object.entries(env).map(([k, v]) => `${k} = ${tomlString(v)}`),
|
|
905
|
+
""
|
|
906
|
+
].join("\n");
|
|
907
|
+
}
|
|
908
|
+
function readCodexEvent(line) {
|
|
909
|
+
let event;
|
|
910
|
+
try {
|
|
911
|
+
event = JSON.parse(line);
|
|
912
|
+
} catch {
|
|
913
|
+
return {};
|
|
914
|
+
}
|
|
915
|
+
if (event.type === "item.started" && event.item) {
|
|
916
|
+
if (event.item.type === "command_execution") return { tool: "shell" };
|
|
917
|
+
if (event.item.type === "mcp_tool_call") return { tool: `${String(event.item.server ?? "mcp")}-${String(event.item.tool ?? "tool")}` };
|
|
918
|
+
return {};
|
|
919
|
+
}
|
|
920
|
+
if (event.type === "turn.failed") return { error: String(event.error?.message ?? "codex turn failed") };
|
|
921
|
+
if (event.type === "error") return { error: String(event.message ?? "codex error") };
|
|
922
|
+
return {};
|
|
923
|
+
}
|
|
924
|
+
async function runCodexSession(config2) {
|
|
925
|
+
const profile = `pitboss-${config2.agentId}`;
|
|
926
|
+
mkdirSync(codexHome(), { recursive: true, mode: 448 });
|
|
927
|
+
const profilePath = join3(codexHome(), `${profile}.config.toml`);
|
|
928
|
+
writeFileSync2(profilePath, codexProfileToml(mcpServerLaunch(), {
|
|
929
|
+
API_URL: config2.apiUrl,
|
|
930
|
+
API_SECRET: config2.apiSecret,
|
|
931
|
+
AGENT_ID: config2.agentId,
|
|
932
|
+
REPO_ID: config2.repoId
|
|
933
|
+
}), { mode: 384 });
|
|
934
|
+
chmodSync(profilePath, 384);
|
|
935
|
+
const cleanup = () => {
|
|
936
|
+
try {
|
|
937
|
+
unlinkSync3(profilePath);
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const args = codexArgs(config2.repoPath, buildPrompt(config2), profile);
|
|
942
|
+
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
943
|
+
return new Promise((resolve, reject) => {
|
|
944
|
+
const proc = spawn4(bin3(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
945
|
+
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
946
|
+
const toolsUsed = [];
|
|
947
|
+
let buffer = "";
|
|
948
|
+
let sessionError = "";
|
|
949
|
+
let stderrOutput = "";
|
|
950
|
+
proc.stdout.on("data", (chunk) => {
|
|
951
|
+
buffer += chunk.toString();
|
|
952
|
+
const lines = buffer.split("\n");
|
|
953
|
+
buffer = lines.pop() ?? "";
|
|
954
|
+
for (const line of lines) {
|
|
955
|
+
const { tool, error } = readCodexEvent(line);
|
|
956
|
+
if (tool) {
|
|
957
|
+
toolsUsed.push(tool);
|
|
958
|
+
process.stdout.write(` \u2699 ${tool}
|
|
959
|
+
`);
|
|
960
|
+
}
|
|
961
|
+
if (error) {
|
|
962
|
+
sessionError = error;
|
|
963
|
+
console.error(`[codex-worker] Session failed: ${error}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
proc.stderr.on("data", (chunk) => {
|
|
968
|
+
stderrOutput += chunk.toString();
|
|
969
|
+
});
|
|
970
|
+
proc.on("close", (code, signal) => {
|
|
971
|
+
cleanup();
|
|
972
|
+
console.log(`[codex-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
|
|
973
|
+
if (sessionError || signal || code !== 0 && code !== null) {
|
|
974
|
+
const detail = (sessionError || stderrOutput).trim();
|
|
975
|
+
reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
|
|
976
|
+
} else {
|
|
977
|
+
resolve([...new Set(toolsUsed)]);
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
proc.on("error", (err) => {
|
|
981
|
+
cleanup();
|
|
982
|
+
reject(err);
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
var codexAdapter = {
|
|
987
|
+
runtime: "codex",
|
|
988
|
+
workerType: "gpt",
|
|
989
|
+
prepare() {
|
|
990
|
+
},
|
|
991
|
+
runSession: runCodexSession,
|
|
992
|
+
classifyError: (text) => {
|
|
993
|
+
if (QUOTA2.some((re) => re.test(text))) return "quota";
|
|
994
|
+
if (CREDENTIALS.some((re) => re.test(text))) return "credentials";
|
|
995
|
+
return classifySessionError(text);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
|
|
850
999
|
// ../claude-worker/src/adapters/index.ts
|
|
851
|
-
var RUNTIMES = ["claude", "cursor", "copilot"];
|
|
852
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
|
|
1000
|
+
var RUNTIMES = ["claude", "cursor", "copilot", "codex"];
|
|
1001
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter, codex: codexAdapter };
|
|
853
1002
|
function getAdapter(runtime) {
|
|
854
1003
|
const adapter = ADAPTERS[runtime];
|
|
855
1004
|
if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
|
package/dist/mcp.js
CHANGED
|
@@ -12536,16 +12536,23 @@ function buildCreateOrderTool(client, repoId, opts) {
|
|
|
12536
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." : ""),
|
|
12537
12537
|
inputSchema: external_exports.object({
|
|
12538
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."),
|
|
12539
|
-
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."),
|
|
12540
12543
|
title: external_exports.string().min(1).max(120).optional().describe("A short name for the goal. The planner writes one when omitted."),
|
|
12541
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."),
|
|
12542
12545
|
budget: external_exports.record(external_exports.unknown()).optional().describe("Optional ceilings. Stored only; not enforced.")
|
|
12543
12546
|
}),
|
|
12544
12547
|
handler: async (input) => {
|
|
12548
|
+
if (!input.feature && !input.issueUrl) return mcpFail("create_order needs feature or issueUrl");
|
|
12545
12549
|
try {
|
|
12546
12550
|
const created = await client.createOrder({
|
|
12547
12551
|
repoId: targetRepo(input),
|
|
12548
|
-
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 } : {},
|
|
12549
12556
|
...input.title ? { title: input.title } : {},
|
|
12550
12557
|
acceptance: input.acceptance,
|
|
12551
12558
|
budget: input.budget
|
|
@@ -13231,21 +13238,42 @@ ${version.body}`
|
|
|
13231
13238
|
] : []
|
|
13232
13239
|
];
|
|
13233
13240
|
}
|
|
13241
|
+
var ATTENTION_STATUSES = "blocked,pending_verification,proposed";
|
|
13242
|
+
function attentionMaxAgeDays() {
|
|
13243
|
+
const n = Number(process.env.ATTENTION_MAX_AGE_DAYS);
|
|
13244
|
+
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
13245
|
+
}
|
|
13246
|
+
var attentionWindowSchema = {
|
|
13247
|
+
includeStale: external_exports.boolean().optional().describe("Include items untouched for longer than maxAgeDays. Off by default so new asks lead."),
|
|
13248
|
+
maxAgeDays: external_exports.number().positive().optional().describe("Hide items untouched for longer than this many days (default 7).")
|
|
13249
|
+
};
|
|
13250
|
+
function attentionCutoff(input) {
|
|
13251
|
+
if (input.includeStale) return void 0;
|
|
13252
|
+
return new Date(Date.now() - (input.maxAgeDays ?? attentionMaxAgeDays()) * 864e5).toISOString();
|
|
13253
|
+
}
|
|
13254
|
+
async function staleAttentionCount(client, cutoff) {
|
|
13255
|
+
if (!cutoff) return 0;
|
|
13256
|
+
const { meta } = await client.getTasksPage({ status: ATTENTION_STATUSES, updatedBefore: cutoff, limit: 1, clip: true });
|
|
13257
|
+
return meta?.total ?? 0;
|
|
13258
|
+
}
|
|
13234
13259
|
function buildGodCoordinationTools(client) {
|
|
13235
13260
|
return [
|
|
13236
13261
|
{
|
|
13237
13262
|
name: "list_attention",
|
|
13238
|
-
description: "List what needs attention across ALL your owner's repos: tasks that are 'blocked' (a worker is waiting), 'pending_verification' (awaiting a review decision), or 'proposed' (a worker's task awaiting commit), plus idle workers that cannot take the ready work that is sitting there (idle_unroutable \u2014 derived now, not from the event log). Each task carries its repoId. A blocked task with metadata.blockedThreadId resumes when you answer with send_message on that thread, as if the owner had. One without it (stalled or overflowed work, or your own task) needs reassigning with assign_task or splitting instead. An idle_unroutable item names the agent and why each task is ineligible (domain, specialization, review_gate, for-human, product-decision, pinned, blockedBy). Capped and clipped, newest first; read one in full with get_task.",
|
|
13239
|
-
inputSchema: external_exports.object(
|
|
13240
|
-
handler: async () => {
|
|
13263
|
+
description: "List what needs attention across ALL your owner's repos: tasks that are 'blocked' (a worker is waiting), 'pending_verification' (awaiting a review decision), or 'proposed' (a worker's task awaiting commit), plus idle workers that cannot take the ready work that is sitting there (idle_unroutable \u2014 derived now, not from the event log). Each task carries its repoId. A blocked task with metadata.blockedThreadId resumes when you answer with send_message (type decision) on that thread, as if the owner had, when the owner has given you a decide delegation for that repo; the task records clearedBy. Answer only reversible, branch-level confirmations yourself (editing, pushing or deleting files on an order branch, choosing between plan options). Merges into a default branch, deploys, deletes outside an order branch, and anything touching credentials or money go to the owner. One without it (stalled or overflowed work, or your own task) needs reassigning with assign_task or splitting instead. An idle_unroutable item names the agent and why each task is ineligible (domain, specialization, review_gate, for-human, product-decision, pinned, blockedBy). Capped and clipped, newest first; read one in full with get_task.",
|
|
13264
|
+
inputSchema: external_exports.object(attentionWindowSchema),
|
|
13265
|
+
handler: async (input) => {
|
|
13266
|
+
const cutoff = attentionCutoff(input);
|
|
13241
13267
|
const [{ data, meta }, idleUnroutable] = await Promise.all([
|
|
13242
13268
|
client.getTasksPage({
|
|
13243
|
-
status:
|
|
13269
|
+
status: ATTENTION_STATUSES,
|
|
13244
13270
|
limit: listTasksLimit(),
|
|
13245
|
-
clip: true
|
|
13271
|
+
clip: true,
|
|
13272
|
+
...cutoff ? { updatedAfter: cutoff } : {}
|
|
13246
13273
|
}),
|
|
13247
13274
|
client.getIdleUnroutable()
|
|
13248
13275
|
]);
|
|
13276
|
+
const staleCount = await staleAttentionCount(client, cutoff);
|
|
13249
13277
|
const tasks = data ?? [];
|
|
13250
13278
|
const idle = idleUnroutable ?? [];
|
|
13251
13279
|
const total = meta?.total ?? tasks.length;
|
|
@@ -13253,11 +13281,12 @@ function buildGodCoordinationTools(client) {
|
|
|
13253
13281
|
return {
|
|
13254
13282
|
content: [{
|
|
13255
13283
|
type: "text",
|
|
13256
|
-
text: tasks.length === 0 && idle.length === 0 ? "Nothing needs attention across your repos right now." : JSON.stringify({
|
|
13284
|
+
text: tasks.length === 0 && idle.length === 0 && staleCount === 0 ? "Nothing needs attention across your repos right now." : JSON.stringify({
|
|
13257
13285
|
tasks,
|
|
13258
13286
|
taskCount: total,
|
|
13259
13287
|
...idle.length > 0 ? { idleUnroutable: idle } : {},
|
|
13260
13288
|
...notShown ? { notShown } : {},
|
|
13289
|
+
...staleCount > 0 ? { staleCount, stale: `${staleCount} older items hidden; pass includeStale: true to see them, and cancel or archive what is dead.` } : {},
|
|
13261
13290
|
peerBoundary: PEER_BOUNDARY
|
|
13262
13291
|
}, null, 2)
|
|
13263
13292
|
}]
|
|
@@ -13529,19 +13558,22 @@ function buildOperatorTools(client, ownerId) {
|
|
|
13529
13558
|
{
|
|
13530
13559
|
name: "list_attention",
|
|
13531
13560
|
description: "List everything across ALL your projects that needs you right now: tasks that are 'blocked' (a worker is waiting on your input), 'pending_verification' (awaiting a review decision), or 'proposed' (a worker's task awaiting your commit), plus idle workers that cannot take the ready work sitting there (idle_unroutable). Call this first to see what to act on. Each task carries its repoId and, for blocked tasks, metadata.blockedThreadId \u2014 the thread to post a reply on (via reply_human) to resume the worker. An idle_unroutable item names the agent and why each task is ineligible.",
|
|
13532
|
-
inputSchema: external_exports.object(
|
|
13533
|
-
handler: async () => {
|
|
13534
|
-
const
|
|
13535
|
-
|
|
13536
|
-
client.
|
|
13561
|
+
inputSchema: external_exports.object(attentionWindowSchema),
|
|
13562
|
+
handler: async (input) => {
|
|
13563
|
+
const cutoff = attentionCutoff(input);
|
|
13564
|
+
const [tasks, idleUnroutable, staleCount] = await Promise.all([
|
|
13565
|
+
client.getTasks({ status: ATTENTION_STATUSES, ...cutoff ? { updatedAfter: cutoff } : {} }),
|
|
13566
|
+
client.getIdleUnroutable(),
|
|
13567
|
+
staleAttentionCount(client, cutoff)
|
|
13537
13568
|
]);
|
|
13538
13569
|
const idle = idleUnroutable ?? [];
|
|
13539
13570
|
return {
|
|
13540
13571
|
content: [{
|
|
13541
13572
|
type: "text",
|
|
13542
|
-
text: tasks.length === 0 && idle.length === 0 ? "Nothing needs your attention across your projects right now." : JSON.stringify({
|
|
13573
|
+
text: tasks.length === 0 && idle.length === 0 && staleCount === 0 ? "Nothing needs your attention across your projects right now." : JSON.stringify({
|
|
13543
13574
|
tasks,
|
|
13544
|
-
...idle.length > 0 ? { idleUnroutable: idle } : {}
|
|
13575
|
+
...idle.length > 0 ? { idleUnroutable: idle } : {},
|
|
13576
|
+
...staleCount > 0 ? { staleCount, stale: `${staleCount} older items hidden; pass includeStale: true to see them, and cancel or archive what is dead.` } : {}
|
|
13545
13577
|
}, null, 2)
|
|
13546
13578
|
}]
|
|
13547
13579
|
};
|