@shipster/cli 0.5.0 → 0.5.1
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 +457 -212
- package/dist/event-worker.js +48 -16
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
package/dist/event-worker.js
CHANGED
|
@@ -263,7 +263,8 @@ You implement specs on a branch and open a PR. Write production code and tests.
|
|
|
263
263
|
|
|
264
264
|
**Playbook rules:**
|
|
265
265
|
- Take the PR head sha from the PR itself (\`gh pr view <n> --json headRefOid\`), never from a handoff message or a task comment.
|
|
266
|
-
- One branch per order, cut from the default branch: never stack on another order's unmerged branch
|
|
266
|
+
- One branch per order, cut from the default branch: never stack on another order's unmerged branch.
|
|
267
|
+
- A test may delete only paths under a directory it created with mkdtemp in that run. Never remove, empty or clear /tmp, $TMPDIR or any other shared directory as a whole, and never point a cleanup or reset at the real one.`;
|
|
267
268
|
case "tester":
|
|
268
269
|
return `## Your role: Tester
|
|
269
270
|
|
|
@@ -277,7 +278,8 @@ Write, fix, and run tests on the branch in branchName metadata.
|
|
|
277
278
|
- If tests cannot pass due to an implementation bug: \`mcp__relai__add_task_comment\`
|
|
278
279
|
(type: "finding") with type "bug", severity "blocking", then
|
|
279
280
|
\`mcp__relai__assign_task\` back to the writer (previous assignee or list_agents).
|
|
280
|
-
Do not complete
|
|
281
|
+
Do not complete.
|
|
282
|
+
- A test may delete only paths under a directory it created with mkdtemp in that run. Never remove, empty or clear /tmp, $TMPDIR or any other shared directory as a whole, and never point a cleanup or reset at the real one.`;
|
|
281
283
|
case "devops":
|
|
282
284
|
return `## Your role: DevOps
|
|
283
285
|
|
|
@@ -293,6 +295,7 @@ Handle CI/CD, infrastructure, build/deploy configuration.
|
|
|
293
295
|
throw new Error(`Unknown specialization "${spec}" \u2014 valid values: reviewer, architect, writer, tester, devops`);
|
|
294
296
|
}
|
|
295
297
|
}
|
|
298
|
+
var PR_FROM_ORDER = "In this section `<prUrl>` is the `prUrl` that `mcp__relai__get_order` returns for `metadata.orderId`. Never use the task's own `metadata.prUrl`. If the `epicId` get_order returns is not this task's own `epicId`, if get_order has no `prUrl`, or the task's `metadata.prUrl` is set and differs from it, run no `gh` command: block the task with a question naming the orchestrator, and stop.";
|
|
296
299
|
var ORDER_RULES = `## Factory orders
|
|
297
300
|
|
|
298
301
|
An order's tasks carry
|
|
@@ -361,7 +364,7 @@ Find the default branch with \`git remote show origin\`.
|
|
|
361
364
|
is done. With one gate, block it on the build tasks alone.
|
|
362
365
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
363
366
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
364
|
-
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge"
|
|
367
|
+
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge" }\`. The server stamps \`prUrl\` on it from the order, so you may omit it.
|
|
365
368
|
If the order has \`pairBuilds\`, every build task needs \`verifyKind: "reviewer_agent"\` and a
|
|
366
369
|
\`verifyReviewerId\` on a different \`workerType\` from the writers (see \`mcp__relai__list_agents\`);
|
|
367
370
|
the API refuses a paired build task without one and routes it away from the reviewer's vendor.
|
|
@@ -374,26 +377,28 @@ Find the default branch with \`git remote show origin\`.
|
|
|
374
377
|
\`metadata.orderKind: "review"\`): the goal is one review posted on the existing pull request at the
|
|
375
378
|
order's \`prUrl\`. There is no order branch, no draft PR and no merge, so skip steps 2 to 6 above.
|
|
376
379
|
Nobody pushes to the PR's branch, approves it, or requests changes on it.
|
|
380
|
+
${PR_FROM_ORDER}
|
|
377
381
|
1. As the planner, read the PR with \`gh pr view <prUrl>\` and \`gh pr diff <prUrl>\`. Pick the
|
|
378
382
|
angles it needs: correctness, security, simplicity, tests and consistency with the codebase for a
|
|
379
383
|
real change; correctness and consistency alone for a small, low-risk one (docs, tests, under about
|
|
380
384
|
150 changed lines with no auth, input handling, secrets, money or migrations).
|
|
381
385
|
2. One task per angle, \`epicId\` = the order's \`epicId\`,
|
|
382
|
-
\`metadata: { orderId, orderRole: "review", angle: "<angle>"
|
|
386
|
+
\`metadata: { orderId, orderRole: "review", angle: "<angle>" }\`; the server stamps \`prUrl\`. Spread the angles
|
|
383
387
|
across vendors: assign each to a different agent from \`mcp__relai__list_agents\`, preferring a
|
|
384
388
|
different \`workerType\` from the agents already given one, and reuse agents only when there are
|
|
385
389
|
more angles than agents. Each description names the angle, the PR, what that angle hunts for, and
|
|
386
390
|
the reporting format below.
|
|
387
391
|
3. One lead task, \`blockedBy\` every angle task, assigned to yourself, with
|
|
388
|
-
\`metadata: { orderId, orderRole: "review_lead"
|
|
392
|
+
\`metadata: { orderId, orderRole: "review_lead" }\`.
|
|
389
393
|
4. When acceptance includes \`human_approval\`, add a gate task blocked by the lead
|
|
390
394
|
(\`metadata: { orderId, orderRole: "gate" }\`, \`verifyKind: "human_approval"\`) and a post task
|
|
391
|
-
blocked by the gate (\`metadata: { orderId, orderRole: "post_review"
|
|
395
|
+
blocked by the gate (\`metadata: { orderId, orderRole: "post_review" }\`).
|
|
392
396
|
5. If the order has \`relatedRepos\`, copy them into every angle and lead task's metadata and name them in
|
|
393
397
|
each description, so reviewers check the contracts this PR depends on in those repos.
|
|
394
398
|
6. Post the task list on the planning task's thread and complete it.
|
|
395
399
|
|
|
396
400
|
**orderRole "review"** (one angle of a review goal):
|
|
401
|
+
- ${PR_FROM_ORDER} For the PR head, \`<n>\` is that PR's number.
|
|
397
402
|
- Read the diff and as much surrounding code as you need. To run anything, check out the PR head
|
|
398
403
|
detached: \`git fetch origin pull/<n>/head && git checkout --detach FETCH_HEAD\`. Never push, never
|
|
399
404
|
comment on the PR, never edit the repo's history.
|
|
@@ -409,6 +414,7 @@ Nobody pushes to the PR's branch, approves it, or requests changes on it.
|
|
|
409
414
|
- Post the report with \`mcp__relai__add_task_comment\` on this task, then complete it.
|
|
410
415
|
|
|
411
416
|
**orderRole "review_lead"**:
|
|
417
|
+
- ${PR_FROM_ORDER}
|
|
412
418
|
- Read every angle task's report with \`mcp__relai__get_task_comments\`. Merge duplicates, drop
|
|
413
419
|
findings you cannot reproduce or whose premise is wrong, and rank what is left. Keep an UNPROVEN mark
|
|
414
420
|
on any finding about a related repo nobody could read. Severity sets the
|
|
@@ -421,7 +427,7 @@ Nobody pushes to the PR's branch, approves it, or requests changes on it.
|
|
|
421
427
|
and put the review URL on the task's thread. With a gate, post the review as a comment on this task
|
|
422
428
|
for the owner to approve, and do not touch the PR.
|
|
423
429
|
|
|
424
|
-
**orderRole "post_review"**:
|
|
430
|
+
**orderRole "post_review"**: ${PR_FROM_ORDER} Once the gate is approved, post the lead's review comment from the
|
|
425
431
|
lead task's thread with \`gh pr review <prUrl> --comment --body-file <file>\`, put the review URL on
|
|
426
432
|
this task's thread, and complete.
|
|
427
433
|
|
|
@@ -451,6 +457,8 @@ this task's thread, and complete.
|
|
|
451
457
|
the rest; if it is not recorded, reject and name it so the builder records it.
|
|
452
458
|
|
|
453
459
|
**orderRole "merge"**:
|
|
460
|
+
${PR_FROM_ORDER}
|
|
461
|
+
|
|
454
462
|
**Playbook rules:**
|
|
455
463
|
- Report a merge you cannot clear under step 3 the same minute: post which check failed, at which sha, and who must act on the merge task's thread, then set the task blocked.
|
|
456
464
|
- Never merge on a stale head: re-read the PR head sha immediately before merging, pass it as \`--match-head-commit\`, and stop if it differs from the sha the gates approved, or the carried head after an Exit 0 conflict-only remerge.
|
|
@@ -2050,7 +2058,7 @@ function claudeArgs(opts) {
|
|
|
2050
2058
|
...opts.isolateUserConfig ? ["--setting-sources", "project,local"] : []
|
|
2051
2059
|
];
|
|
2052
2060
|
}
|
|
2053
|
-
async function runClaudeSession(config2) {
|
|
2061
|
+
async function runClaudeSession(config2, opts = {}) {
|
|
2054
2062
|
const repoContextOn = readEnv("REPO_CONTEXT") === "1";
|
|
2055
2063
|
const lessonsPromise = repoContextOn ? Promise.resolve("") : lessonsBlock({ ...config2, repoId: taskRepoId(config2) });
|
|
2056
2064
|
const repoContextPromise = repoContextOn ? (readEnv("TASK_REPO_ID")?.trim() ? Promise.resolve(taskRepoId(config2)) : contextRepoId(config2)).then((repoId) => repoContextBlock({ repoId, repoPath: config2.repoPath, role: config2.specialization }, config2)) : Promise.resolve("");
|
|
@@ -2075,7 +2083,8 @@ async function runClaudeSession(config2) {
|
|
|
2075
2083
|
const proc = spawn(config2.claudeBin, args, {
|
|
2076
2084
|
cwd: config2.repoPath,
|
|
2077
2085
|
env: { ...childEnv, PATH: process.env.PATH },
|
|
2078
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2086
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2087
|
+
signal: opts.signal
|
|
2079
2088
|
});
|
|
2080
2089
|
console.log(`[claude-worker] Process PID: ${proc.pid}`);
|
|
2081
2090
|
Promise.all([lessonsPromise, repoContextPromise]).then(([lessons, repoContext]) => {
|
|
@@ -2234,7 +2243,7 @@ function readCursorEvent(line) {
|
|
|
2234
2243
|
}
|
|
2235
2244
|
return {};
|
|
2236
2245
|
}
|
|
2237
|
-
async function runCursorSession(config2) {
|
|
2246
|
+
async function runCursorSession(config2, opts = {}) {
|
|
2238
2247
|
const model = pinnedModel();
|
|
2239
2248
|
recordLaunchedModel(model);
|
|
2240
2249
|
const args = cursorArgs(config2.repoPath, buildPrompt(config2, { lessons: await lessonsBlock({ ...config2, repoId: taskRepoId(config2) }) }), model);
|
|
@@ -2242,7 +2251,7 @@ async function runCursorSession(config2) {
|
|
|
2242
2251
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2243
2252
|
const taskIdsPromise = listSessionTaskIds(config2, "[cursor-worker]");
|
|
2244
2253
|
return new Promise((resolve2, reject) => {
|
|
2245
|
-
const proc = spawn2(bin(), args, { cwd: config2.repoPath, env: workerEnv("cursor", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
|
|
2254
|
+
const proc = spawn2(bin(), args, { cwd: config2.repoPath, env: workerEnv("cursor", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
|
|
2246
2255
|
console.log(`[cursor-worker] Process PID: ${proc.pid}`);
|
|
2247
2256
|
const toolsUsed = [];
|
|
2248
2257
|
let buffer = "";
|
|
@@ -2354,7 +2363,7 @@ function readCopilotEvent(line) {
|
|
|
2354
2363
|
if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
|
|
2355
2364
|
return {};
|
|
2356
2365
|
}
|
|
2357
|
-
async function runCopilotSession(config2) {
|
|
2366
|
+
async function runCopilotSession(config2, opts = {}) {
|
|
2358
2367
|
const mcpConfigPath = writeMcpConfig(config2, "copilot-worker");
|
|
2359
2368
|
const model = pinnedModel();
|
|
2360
2369
|
recordLaunchedModel(model);
|
|
@@ -2363,7 +2372,7 @@ async function runCopilotSession(config2) {
|
|
|
2363
2372
|
const taskIdsPromise = listSessionTaskIds(config2, "[copilot-worker]");
|
|
2364
2373
|
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
2365
2374
|
return new Promise((resolve2, reject) => {
|
|
2366
|
-
const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: workerEnv("copilot", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
|
|
2375
|
+
const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: workerEnv("copilot", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
|
|
2367
2376
|
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
2368
2377
|
const toolsUsed = [];
|
|
2369
2378
|
let buffer = "";
|
|
@@ -2481,7 +2490,7 @@ function readCodexEvent(line) {
|
|
|
2481
2490
|
if (event.type === "error") return { error: String(event.message ?? "codex error") };
|
|
2482
2491
|
return {};
|
|
2483
2492
|
}
|
|
2484
|
-
async function runCodexSession(config2) {
|
|
2493
|
+
async function runCodexSession(config2, opts = {}) {
|
|
2485
2494
|
const profile = `pitboss-${config2.agentId}`;
|
|
2486
2495
|
mkdirSync3(codexHome(), { recursive: true, mode: 448 });
|
|
2487
2496
|
const profilePath = join5(codexHome(), `${profile}.config.toml`);
|
|
@@ -2507,7 +2516,7 @@ async function runCodexSession(config2) {
|
|
|
2507
2516
|
const taskIdsPromise = listSessionTaskIds(config2, "[codex-worker]");
|
|
2508
2517
|
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
2509
2518
|
return new Promise((resolve2, reject) => {
|
|
2510
|
-
const proc = spawn4(bin3(), args, { cwd: config2.repoPath, env: workerEnv("codex", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
|
|
2519
|
+
const proc = spawn4(bin3(), args, { cwd: config2.repoPath, env: workerEnv("codex", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
|
|
2511
2520
|
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
2512
2521
|
const toolsUsed = [];
|
|
2513
2522
|
let buffer = "";
|
|
@@ -3690,12 +3699,18 @@ function endSession(file) {
|
|
|
3690
3699
|
}
|
|
3691
3700
|
}
|
|
3692
3701
|
}
|
|
3702
|
+
function endsTask(event, taskId) {
|
|
3703
|
+
const task = event?.payload?.task;
|
|
3704
|
+
if (!task || task.id !== taskId) return false;
|
|
3705
|
+
return task.status === "cancelled" || task.archivedAt !== void 0 && task.archivedAt !== null;
|
|
3706
|
+
}
|
|
3693
3707
|
async function runEventWorker(config2) {
|
|
3694
3708
|
console.log(`[event-worker] Starting \u2014 agent ${config2.agentId}, watching ${config2.apiUrl}/events`);
|
|
3695
3709
|
console.log(`[event-worker] Repo: ${config2.repoPath} | Runtime: ${config2.runtime} | Model: ${config2.model}`);
|
|
3696
3710
|
await assertRepoOrExit(config2, "[event-worker]");
|
|
3697
3711
|
await assertNotOwnerScopedOrExit(config2, "[event-worker]");
|
|
3698
3712
|
await selfSubscribe(config2);
|
|
3713
|
+
let live = null;
|
|
3699
3714
|
const queue = createRunQueue(async () => {
|
|
3700
3715
|
await heartbeat(config2, "[event-worker]");
|
|
3701
3716
|
const slotMode = readEnv("OWNER_POOL_SLOT") === "1";
|
|
@@ -3749,6 +3764,7 @@ async function runEventWorker(config2) {
|
|
|
3749
3764
|
return;
|
|
3750
3765
|
}
|
|
3751
3766
|
}
|
|
3767
|
+
const controller = new AbortController();
|
|
3752
3768
|
try {
|
|
3753
3769
|
console.log("[event-worker] Event received \u2014 running session...");
|
|
3754
3770
|
const activityFile = readEnv("ACTIVITY_FILE");
|
|
@@ -3767,9 +3783,17 @@ async function runEventWorker(config2) {
|
|
|
3767
3783
|
setEnv(TASK_REPO_ID_ENV, checkout.repoId);
|
|
3768
3784
|
setEnv(TASK_REPO_URL_ENV, checkout.repoUrl);
|
|
3769
3785
|
}
|
|
3786
|
+
if (checkout) live = { taskId: checkout.taskId, controller };
|
|
3770
3787
|
try {
|
|
3771
|
-
await getAdapter(config2.runtime).runSession(config2);
|
|
3788
|
+
await getAdapter(config2.runtime).runSession(config2, { signal: controller.signal });
|
|
3772
3789
|
} finally {
|
|
3790
|
+
live = null;
|
|
3791
|
+
if (controller.signal.aborted && readEnv(RESET_SEAT_DIR_ENV)) {
|
|
3792
|
+
try {
|
|
3793
|
+
await killSeatProcesses();
|
|
3794
|
+
} catch {
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3773
3797
|
if (slotMode) envRestore(priorPin);
|
|
3774
3798
|
if (checkout) {
|
|
3775
3799
|
envRestore(priorTaskRepo.id);
|
|
@@ -3785,6 +3809,10 @@ async function runEventWorker(config2) {
|
|
|
3785
3809
|
await clearUnavailable(config2, "[event-worker]");
|
|
3786
3810
|
} catch (err) {
|
|
3787
3811
|
const text = err instanceof Error ? err.message : String(err);
|
|
3812
|
+
if (controller.signal.aborted) {
|
|
3813
|
+
console.log(`[event-worker] Session stopped because its task was cancelled or archived (${text.split("\n")[0].slice(0, 120)})`);
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3788
3816
|
const exitActivityFile = readEnv("ACTIVITY_FILE");
|
|
3789
3817
|
if (exitActivityFile) {
|
|
3790
3818
|
try {
|
|
@@ -3829,6 +3857,10 @@ async function runEventWorker(config2) {
|
|
|
3829
3857
|
try {
|
|
3830
3858
|
const event = JSON.parse(raw.data);
|
|
3831
3859
|
console.log(`[event-worker] Event: ${event.kind ?? "unknown"}`);
|
|
3860
|
+
if (live && endsTask(event, live.taskId) && !live.controller.signal.aborted) {
|
|
3861
|
+
console.warn(`[event-worker] task ${live.taskId} was cancelled or archived \u2014 stopping its session`);
|
|
3862
|
+
live.controller.abort();
|
|
3863
|
+
}
|
|
3832
3864
|
} catch {
|
|
3833
3865
|
}
|
|
3834
3866
|
queue.notify();
|
package/dist/mcp.js
CHANGED
|
@@ -12925,7 +12925,7 @@ function buildOrderReadTools(client, repoId, opts) {
|
|
|
12925
12925
|
return [
|
|
12926
12926
|
{
|
|
12927
12927
|
name: "get_order",
|
|
12928
|
-
description: "Read one factory order by id. Status is derived from the Epic's child tasks and acceptance gates, not stored transitions. Also returns `tasks` (id, title, status, assignee, orderRole, blockedBy, model, cost), `gates` (kind, status, reviewer, decision), `cost` ({ totalUsd, totalTurns, byModel, tasksReporting, tasksTotal, note }) and `models`. The cost covers only what workers have reported so far: read its `note` before quoting the total. " + ORDER_GATE_NOTE,
|
|
12928
|
+
description: "Read one factory order by id. Status is derived from the Epic's child tasks and acceptance gates, not stored transitions. Also returns `tasks` (id, title, status, assignee, orderRole, blockedBy, model, cost), `gates` (kind, status, reviewer, decision), `cost` ({ totalUsd, totalTurns, byModel, byTier, tasksReporting, tasksTotal, note }) and `models`. The cost covers only what workers have reported so far: read its `note` before quoting the total. " + ORDER_GATE_NOTE,
|
|
12929
12929
|
inputSchema: external_exports.object({
|
|
12930
12930
|
id: external_exports.string().describe("The order id (ord_\u2026).")
|
|
12931
12931
|
}),
|