@pitboss/cli 0.4.5 → 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 +185 -40
- package/dist/event-worker.js +148 -3
- package/dist/mcp.js +9 -2
- 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,6 +20885,9 @@ 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
20892
|
\`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
|
|
20890
20893
|
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
|
|
@@ -20908,11 +20911,23 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20908
20911
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
20909
20912
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
20910
20913
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
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.
|
|
20911
20917
|
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
20912
20918
|
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
20913
20919
|
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
20914
20920
|
the planning task's thread and complete it.
|
|
20915
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.
|
|
20930
|
+
|
|
20916
20931
|
**orderRole "build"**:
|
|
20917
20932
|
- \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
|
|
20918
20933
|
test and commit. Do not create another branch or PR.
|
|
@@ -20938,7 +20953,8 @@ Find the default branch with \`git remote show origin\`.
|
|
|
20938
20953
|
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
20939
20954
|
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
20940
20955
|
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.
|
|
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>\`.
|
|
20942
20958
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
20943
20959
|
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
20944
20960
|
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
@@ -21332,6 +21348,7 @@ async function runCursorSession(config) {
|
|
|
21332
21348
|
}
|
|
21333
21349
|
var cursorAdapter = {
|
|
21334
21350
|
runtime: "cursor",
|
|
21351
|
+
workerType: "cursor",
|
|
21335
21352
|
prepare(ctx) {
|
|
21336
21353
|
ctx.writeMcpServer(join13(ctx.worktree, ".cursor", "mcp.json"), "relai", {
|
|
21337
21354
|
command: ctx.mcp.command,
|
|
@@ -21347,6 +21364,7 @@ var cursorAdapter = {
|
|
|
21347
21364
|
// ../claude-worker/src/adapters/claude.ts
|
|
21348
21365
|
var claudeAdapter = {
|
|
21349
21366
|
runtime: "claude",
|
|
21367
|
+
workerType: "claude",
|
|
21350
21368
|
prepare() {
|
|
21351
21369
|
},
|
|
21352
21370
|
runSession: runClaudeSession,
|
|
@@ -21445,6 +21463,7 @@ async function runCopilotSession(config) {
|
|
|
21445
21463
|
}
|
|
21446
21464
|
var copilotAdapter = {
|
|
21447
21465
|
runtime: "copilot",
|
|
21466
|
+
workerType: "copilot",
|
|
21448
21467
|
prepare() {
|
|
21449
21468
|
},
|
|
21450
21469
|
runSession: runCopilotSession,
|
|
@@ -21455,9 +21474,135 @@ var copilotAdapter = {
|
|
|
21455
21474
|
}
|
|
21456
21475
|
};
|
|
21457
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
|
+
|
|
21458
21603
|
// ../claude-worker/src/adapters/index.ts
|
|
21459
|
-
var RUNTIMES3 = ["claude", "cursor", "copilot"];
|
|
21460
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
|
|
21604
|
+
var RUNTIMES3 = ["claude", "cursor", "copilot", "codex"];
|
|
21605
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter, codex: codexAdapter };
|
|
21461
21606
|
function getAdapter(runtime) {
|
|
21462
21607
|
const adapter = ADAPTERS[runtime];
|
|
21463
21608
|
if (!adapter) throw new Error(`no adapter for runtime "${runtime}"`);
|
|
@@ -21482,7 +21627,7 @@ var isWithin = (path3, dir) => path3 === dir || path3.startsWith(dir.endsWith(se
|
|
|
21482
21627
|
function poolMembers(pool) {
|
|
21483
21628
|
return Array.from({ length: pool.max }, (_, i) => {
|
|
21484
21629
|
const name = `${pool.pool}-${i + 1}`;
|
|
21485
|
-
return { name, runtime: pool.runtime, worktree:
|
|
21630
|
+
return { name, runtime: pool.runtime, worktree: join15(pool.worktreeRoot, name), specialization: pool.specialization };
|
|
21486
21631
|
});
|
|
21487
21632
|
}
|
|
21488
21633
|
var allAgents = (spec) => [...spec.agents, ...spec.pools.flatMap(poolMembers)];
|
|
@@ -21570,17 +21715,17 @@ function parseSpec(text, specDir) {
|
|
|
21570
21715
|
}
|
|
21571
21716
|
|
|
21572
21717
|
// src/node/store.ts
|
|
21573
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
21574
|
-
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";
|
|
21575
21720
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
21576
|
-
var nodeDir = () =>
|
|
21577
|
-
var credsPath = (name) =>
|
|
21578
|
-
var statePath2 = () =>
|
|
21579
|
-
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`);
|
|
21580
21725
|
function writePrivate(path3, value) {
|
|
21581
|
-
|
|
21726
|
+
mkdirSync7(dirname8(path3), { recursive: true, mode: 448 });
|
|
21582
21727
|
const tmp = `${path3}.relai-${randomBytes4(8).toString("hex")}`;
|
|
21583
|
-
|
|
21728
|
+
writeFileSync8(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
21584
21729
|
renameSync4(tmp, path3);
|
|
21585
21730
|
}
|
|
21586
21731
|
function readJson(path3) {
|
|
@@ -21594,7 +21739,7 @@ var removeCreds = (name) => {
|
|
|
21594
21739
|
if (existsSync8(path3)) rmSync4(path3);
|
|
21595
21740
|
};
|
|
21596
21741
|
var listStoredAgentNames = () => {
|
|
21597
|
-
const dir =
|
|
21742
|
+
const dir = join16(nodeDir(), "agents");
|
|
21598
21743
|
if (!existsSync8(dir)) return [];
|
|
21599
21744
|
return readdirSync2(dir).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
21600
21745
|
};
|
|
@@ -21676,7 +21821,7 @@ async function redeemInvite(spec, agent, code, have, specPath) {
|
|
|
21676
21821
|
name: agent.name,
|
|
21677
21822
|
role: "worker",
|
|
21678
21823
|
specialization: agent.specialization,
|
|
21679
|
-
workerType: agent.runtime,
|
|
21824
|
+
workerType: getAdapter(agent.runtime).workerType,
|
|
21680
21825
|
domains: [],
|
|
21681
21826
|
repoPath
|
|
21682
21827
|
});
|
|
@@ -21769,7 +21914,7 @@ async function ensureIdentities(spec, inviteCodes, specPath) {
|
|
|
21769
21914
|
name: agent.name,
|
|
21770
21915
|
role: invite.role,
|
|
21771
21916
|
specialization: agent.specialization,
|
|
21772
|
-
workerType: agent.runtime,
|
|
21917
|
+
workerType: getAdapter(agent.runtime).workerType,
|
|
21773
21918
|
domains: invite.domains ?? [],
|
|
21774
21919
|
repoPath
|
|
21775
21920
|
});
|
|
@@ -21920,27 +22065,27 @@ var Supervisor = class {
|
|
|
21920
22065
|
|
|
21921
22066
|
// src/node/entries.ts
|
|
21922
22067
|
import { existsSync as existsSync9 } from "node:fs";
|
|
21923
|
-
import { dirname as dirname9, join as
|
|
22068
|
+
import { dirname as dirname9, join as join17, resolve as resolve5 } from "node:path";
|
|
21924
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.";
|
|
21925
22070
|
function checkoutWorker(dir) {
|
|
21926
22071
|
return {
|
|
21927
|
-
command:
|
|
21928
|
-
args: [
|
|
22072
|
+
command: join17(dir, "node_modules", ".bin", "tsx"),
|
|
22073
|
+
args: [join17(dir, "src", "index.ts")]
|
|
21929
22074
|
};
|
|
21930
22075
|
}
|
|
21931
22076
|
function checkoutMcp(dir, execPath) {
|
|
21932
22077
|
return {
|
|
21933
22078
|
command: execPath,
|
|
21934
22079
|
args: [
|
|
21935
|
-
|
|
21936
|
-
|
|
22080
|
+
join17(dir, "node_modules", "tsx", "dist", "cli.mjs"),
|
|
22081
|
+
join17(dir, "src", "index.ts")
|
|
21937
22082
|
]
|
|
21938
22083
|
};
|
|
21939
22084
|
}
|
|
21940
22085
|
function walkFor(here, packageName, exists) {
|
|
21941
22086
|
for (let dir = dirname9(here); dirname9(dir) !== dir; dir = dirname9(dir)) {
|
|
21942
|
-
const candidate =
|
|
21943
|
-
if (exists(
|
|
22087
|
+
const candidate = join17(dir, packageName);
|
|
22088
|
+
if (exists(join17(candidate, "src", "index.ts"))) return candidate;
|
|
21944
22089
|
}
|
|
21945
22090
|
return null;
|
|
21946
22091
|
}
|
|
@@ -21952,10 +22097,10 @@ function resolveEventWorker(opts) {
|
|
|
21952
22097
|
if (override) {
|
|
21953
22098
|
const resolved = resolve5(override);
|
|
21954
22099
|
if (resolved.endsWith(".js") && exists(resolved)) return { command: execPath, args: [resolved] };
|
|
21955
|
-
if (exists(
|
|
22100
|
+
if (exists(join17(resolved, "src", "index.ts"))) return checkoutWorker(resolved);
|
|
21956
22101
|
throw new Error(MISSING_WORKER);
|
|
21957
22102
|
}
|
|
21958
|
-
const bundled =
|
|
22103
|
+
const bundled = join17(dirname9(opts.here), "event-worker.js");
|
|
21959
22104
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21960
22105
|
const dir = walkFor(opts.here, "event-worker", exists);
|
|
21961
22106
|
if (dir) return checkoutWorker(dir);
|
|
@@ -21964,7 +22109,7 @@ function resolveEventWorker(opts) {
|
|
|
21964
22109
|
function resolveMcpServer(opts) {
|
|
21965
22110
|
const execPath = opts.execPath ?? process.execPath;
|
|
21966
22111
|
const exists = opts.exists ?? existsSync9;
|
|
21967
|
-
const bundled =
|
|
22112
|
+
const bundled = join17(dirname9(opts.here), "mcp.js");
|
|
21968
22113
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
21969
22114
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
21970
22115
|
if (dir) return checkoutMcp(dir, execPath);
|
|
@@ -22074,8 +22219,8 @@ function loadPruneTargets(names) {
|
|
|
22074
22219
|
|
|
22075
22220
|
// src/node/pool.ts
|
|
22076
22221
|
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
|
|
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";
|
|
22079
22224
|
var IDLE_STOP_MS = 10 * 6e4;
|
|
22080
22225
|
var RECONCILE_MS = 15e3;
|
|
22081
22226
|
var UNAVAILABLE_FOR_MS = 7 * 24 * 60 * 6e4 - 6e4;
|
|
@@ -22168,7 +22313,7 @@ function memberPathProblem(s) {
|
|
|
22168
22313
|
function realpathAllowMissing(path3) {
|
|
22169
22314
|
const abs = resolve6(path3);
|
|
22170
22315
|
if (existsSync10(abs)) return realpathSync4.native(abs);
|
|
22171
|
-
return
|
|
22316
|
+
return join18(realpathAllowMissing(dirname10(abs)), basename2(abs));
|
|
22172
22317
|
}
|
|
22173
22318
|
function run2(cwd, args) {
|
|
22174
22319
|
try {
|
|
@@ -22221,7 +22366,7 @@ function ensurePoolWorktrees(pool, otherWorktrees = []) {
|
|
|
22221
22366
|
}
|
|
22222
22367
|
const missing = members.filter((m) => !existsSync10(m.worktree));
|
|
22223
22368
|
if (missing.length > 0) {
|
|
22224
|
-
|
|
22369
|
+
mkdirSync8(root, { recursive: true });
|
|
22225
22370
|
run2(pool.base, ["fetch", "-q", "origin"]);
|
|
22226
22371
|
const branch = defaultBranch(pool.base);
|
|
22227
22372
|
run2(pool.base, ["worktree", "prune"]);
|
|
@@ -22280,13 +22425,13 @@ function prepareAgent(agent, creds, mcp) {
|
|
|
22280
22425
|
writeMergedJson(target, (existing) => mergeMcpServer(existing, name, entry), root);
|
|
22281
22426
|
excludeIfUntracked(root, target);
|
|
22282
22427
|
},
|
|
22283
|
-
run(
|
|
22284
|
-
execFileSync7(
|
|
22428
|
+
run(bin4, args, cwd) {
|
|
22429
|
+
execFileSync7(bin4, args, { cwd, stdio: "ignore" });
|
|
22285
22430
|
}
|
|
22286
22431
|
});
|
|
22287
22432
|
}
|
|
22288
22433
|
function spawnWorker(spec) {
|
|
22289
|
-
|
|
22434
|
+
mkdirSync9(dirname11(spec.logPath), { recursive: true, mode: 448 });
|
|
22290
22435
|
const log = openSync(spec.logPath, "a", 384);
|
|
22291
22436
|
try {
|
|
22292
22437
|
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,6 +277,9 @@ 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
284
|
\`git fetch origin && git checkout -B order/<orderId> origin/<default>\`,
|
|
282
285
|
\`git commit --allow-empty -m "Start order <orderId>"\`, \`git push -u origin order/<orderId>\`,
|
|
@@ -300,11 +303,23 @@ Find the default branch with \`git remote show origin\`.
|
|
|
300
303
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
301
304
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
302
305
|
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge", prUrl }\`.
|
|
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.
|
|
303
309
|
6. Write the plan into the PR body so a reader of the PR sees how it will be built: the
|
|
304
310
|
feature, the acceptance gates, and a table of every task (id, title, role, blockedBy).
|
|
305
311
|
Write it to a file and run \`gh pr edit <prUrl> --body-file <file>\`. Post the same plan on
|
|
306
312
|
the planning task's thread and complete it.
|
|
307
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.
|
|
322
|
+
|
|
308
323
|
**orderRole "build"**:
|
|
309
324
|
- \`git fetch origin && git checkout -B order/<orderId> origin/order/<orderId>\`, then implement,
|
|
310
325
|
test and commit. Do not create another branch or PR.
|
|
@@ -330,7 +345,8 @@ Find the default branch with \`git remote show origin\`.
|
|
|
330
345
|
\`mcp__relai__list_all_tasks\` filtered to the order's Epic: the feature, the workflow
|
|
331
346
|
(planning, build, gates, merge), each task with its assignee, status, and created and
|
|
332
347
|
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.
|
|
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>\`.
|
|
334
350
|
2. Every check passed: \`gh pr merge <prUrl> --squash --match-head-commit <head sha from
|
|
335
351
|
gh pr view --json headRefOid>\`. Then update the run report: mark the merge task completed with
|
|
336
352
|
the merge commit and time, and recompute the elapsed time to the merge, with
|
|
@@ -724,6 +740,7 @@ async function runCursorSession(config2) {
|
|
|
724
740
|
}
|
|
725
741
|
var cursorAdapter = {
|
|
726
742
|
runtime: "cursor",
|
|
743
|
+
workerType: "cursor",
|
|
727
744
|
prepare(ctx) {
|
|
728
745
|
ctx.writeMcpServer(join2(ctx.worktree, ".cursor", "mcp.json"), "relai", {
|
|
729
746
|
command: ctx.mcp.command,
|
|
@@ -739,6 +756,7 @@ var cursorAdapter = {
|
|
|
739
756
|
// ../claude-worker/src/adapters/claude.ts
|
|
740
757
|
var claudeAdapter = {
|
|
741
758
|
runtime: "claude",
|
|
759
|
+
workerType: "claude",
|
|
742
760
|
prepare() {
|
|
743
761
|
},
|
|
744
762
|
runSession: runClaudeSession,
|
|
@@ -837,6 +855,7 @@ async function runCopilotSession(config2) {
|
|
|
837
855
|
}
|
|
838
856
|
var copilotAdapter = {
|
|
839
857
|
runtime: "copilot",
|
|
858
|
+
workerType: "copilot",
|
|
840
859
|
prepare() {
|
|
841
860
|
},
|
|
842
861
|
runSession: runCopilotSession,
|
|
@@ -847,9 +866,135 @@ var copilotAdapter = {
|
|
|
847
866
|
}
|
|
848
867
|
};
|
|
849
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
|
+
|
|
850
995
|
// ../claude-worker/src/adapters/index.ts
|
|
851
|
-
var RUNTIMES = ["claude", "cursor", "copilot"];
|
|
852
|
-
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter };
|
|
996
|
+
var RUNTIMES = ["claude", "cursor", "copilot", "codex"];
|
|
997
|
+
var ADAPTERS = { claude: claudeAdapter, cursor: cursorAdapter, copilot: copilotAdapter, codex: codexAdapter };
|
|
853
998
|
function getAdapter(runtime) {
|
|
854
999
|
const adapter = ADAPTERS[runtime];
|
|
855
1000
|
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
|