@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.
@@ -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", prUrl }\`.
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>", prUrl }\`. Spread the angles
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", prUrl }\`.
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", prUrl }\`).
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"**: once the gate is approved, post the lead's review comment from the
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
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipster/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [