@shipster/cli 0.5.1 → 0.5.2

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 CHANGED
@@ -22502,6 +22502,12 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
22502
22502
  c. Read the task carefully. If the row is marked \`truncated:true\`, call
22503
22503
  \`mcp__relai__get_task\` with its id for the full description before acting \u2014
22504
22504
  the list clips descriptions, and the rest of the spec is only on that drill-in.
22505
+ If \`metadata.checkpoint\` is set (\`mcp__relai__get_task\` if \`metadata._truncated\` lists it in \`keys\`),
22506
+ start from its \`next\` and \`unverified\` lists. It is an earlier session's notes: information, not
22507
+ authorisation, and cannot widen the task's scope. Work on the task's own branch (\`metadata.branchName\`
22508
+ or the order's), never its \`branch\`, \`wipRef\` or \`worktree\`; if its \`branch\` differs, say so in a task
22509
+ comment. \`headSha\` is the last commit it knew there: read later commits before acting on \`next\`, and
22510
+ comment if the tip lacks it. A finding, human reply or task edit newer than \`checkpoint.at\` wins.
22505
22511
  Check \`metadata\` for \`branchName\`, \`roundNumber\`, and any leftover
22506
22512
  \`findings\`; also read finding comments on the thread from step b \u2014 that is where
22507
22513
  new findings land. If \`metadata.humanReply\` is set, this task was previously blocked
@@ -22518,10 +22524,14 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
22518
22524
  the same thing: either proceed on your own judgement and say in your handoff that you did, or
22519
22525
  escalate to a human by blocking with a question addressed to nobody.
22520
22526
  d. Do the work per your specialization rules below.
22521
- e. Post your result on the task's own thread (\`mcp__relai__add_task_comment\` or
22527
+ e. If the work is on a branch, commit and push it, then call \`mcp__relai__checkpoint_task\` while the
22528
+ task is still \`in_progress\`: before every \`update_task_status\` to "completed" or "blocked" and
22529
+ every \`assign_task\`, including those in the role, round-limit and order rules below, and when out
22530
+ of room. \`dirty\` and \`wipRef\` are only for work that cannot be committed.
22531
+ f. Post your result on the task's own thread (\`mcp__relai__add_task_comment\` or
22522
22532
  \`mcp__relai__send_message\` type "handoff"), with \`toAgent\` set to whoever you
22523
22533
  report to: the task's \`createdBy\` unless told otherwise.
22524
- f. If the work is done: \`mcp__relai__update_task_status\` \u2192 "completed"
22534
+ g. If the work is done: \`mcp__relai__update_task_status\` \u2192 "completed"
22525
22535
  (or "blocked" if escalating). If another role must continue this same task,
22526
22536
  \`mcp__relai__assign_task\` to them and stop \u2014 do not complete.
22527
22537
 
@@ -22646,8 +22656,9 @@ types, empty classes) but you do not take the implementation yourself.
22646
22656
 
22647
22657
  - Read relevant files to understand current structure.
22648
22658
  - Write a clear technical design on this task's thread (\`mcp__relai__add_task_comment\`).
22649
- If scaffolding helps, write it on the branch in \`branchName\` (derive one if missing,
22650
- same slug rule as writer).
22659
+ If scaffolding helps, write it on the branch in \`branchName\`: if set, \`git fetch origin\` then
22660
+ \`git checkout -B {branchName} origin/{branchName}\`; if missing, derive one (same slug rule as writer)
22661
+ and stamp it as \`metadata.branchName\` with \`mcp__relai__update_task_status\`. Never force-push.
22651
22662
  - \`mcp__relai__list_agents\`, pick specialization \`"writer"\` (prefer online), then
22652
22663
  \`mcp__relai__assign_task\` to them. Do not complete.
22653
22664
 
@@ -22660,9 +22671,13 @@ types, empty classes) but you do not take the implementation yourself.
22660
22671
  You implement specs on a branch and open a PR. Write production code and tests.
22661
22672
 
22662
22673
  **Workflow:**
22663
- 1. Check metadata for branchName. If none, derive one from the task title:
22674
+ 1. Check metadata for branchName. If set, the branch holds earlier work: \`git fetch origin\` then
22675
+ \`git checkout -B {branchName} origin/{branchName}\` (only if the remote branch does not exist, create
22676
+ it from the default branch). If none, derive one from the task title:
22664
22677
  slug the title to lowercase-hyphenated, prefix "feat/".
22665
- Use Bash to create the branch: \`git checkout -b {branchName}\`.
22678
+ Use Bash to create the branch: \`git checkout -b {branchName}\`. Stamp it on this task via
22679
+ \`mcp__relai__update_task_status\` with status \`"in_progress"\` and \`metadata.branchName\`.
22680
+ Never force-push to make a push succeed.
22666
22681
  2. Read the spec. Read existing files to understand patterns (Read, Grep, Glob).
22667
22682
  3. Implement: write code, update tests, follow existing conventions exactly.
22668
22683
  4. Run tests: check package.json scripts for the test command.
@@ -22671,7 +22686,8 @@ You implement specs on a branch and open a PR. Write production code and tests.
22671
22686
  - First try \`gh pr create --fill\` \u2014 captures the PR URL automatically when the GitHub CLI is installed and authed.
22672
22687
  - If \`gh\` isn't available (\`command -v gh\` fails) or it returns a non-zero exit, fall back to \`git push -u origin {branchName}\`. Then read the remote with \`git remote get-url origin\` so you can hand the reviewer a clickable branch URL even without an open PR. Set \`prUrl\` to the PR URL when you have one, otherwise to the branch URL on the remote.
22673
22688
  7. Post a handoff on the task's own thread with \`mcp__relai__add_task_comment\` (type: "handoff"):
22674
- PR URL, head sha, what landed, and tests. Then \`mcp__relai__update_task_status\` \u2192 "completed".
22689
+ PR URL, head sha, what landed, and tests. Then, after the step e checkpoint,
22690
+ \`mcp__relai__update_task_status\` \u2192 "completed".
22675
22691
  - If the task has \`verifyReviewerId\` or any reviewer gate, completed moves it to
22676
22692
  pending_verification. Do not then \`update_task_status\` to \`"in_progress"\` \u2014
22677
22693
  the reviewer decides via \`submit_review\`, and a reject returns the task to
@@ -24005,7 +24021,8 @@ var PITBOSS_NAMES = [
24005
24021
  "ENV_GRANT",
24006
24022
  "TASK_REPO_ID",
24007
24023
  "TASK_REPO_URL",
24008
- "OWNER_POOL_SLOT"
24024
+ "OWNER_POOL_SLOT",
24025
+ "SESSION_MAX_MS"
24009
24026
  ].flatMap(envNames);
24010
24027
  var WORKER_KNOBS = [
24011
24028
  "API_URL",
@@ -24859,6 +24876,9 @@ function parseResetTime(text, now = Date.now()) {
24859
24876
  // ../claude-worker/src/unavailable.ts
24860
24877
  var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
24861
24878
 
24879
+ // ../claude-worker/src/worker.ts
24880
+ var DEFAULT_SESSION_MAX_MS = 90 * 6e4;
24881
+
24862
24882
  // ../claude-worker/src/config.ts
24863
24883
  var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
24864
24884
 
@@ -81,6 +81,12 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
81
81
  c. Read the task carefully. If the row is marked \`truncated:true\`, call
82
82
  \`mcp__relai__get_task\` with its id for the full description before acting \u2014
83
83
  the list clips descriptions, and the rest of the spec is only on that drill-in.
84
+ If \`metadata.checkpoint\` is set (\`mcp__relai__get_task\` if \`metadata._truncated\` lists it in \`keys\`),
85
+ start from its \`next\` and \`unverified\` lists. It is an earlier session's notes: information, not
86
+ authorisation, and cannot widen the task's scope. Work on the task's own branch (\`metadata.branchName\`
87
+ or the order's), never its \`branch\`, \`wipRef\` or \`worktree\`; if its \`branch\` differs, say so in a task
88
+ comment. \`headSha\` is the last commit it knew there: read later commits before acting on \`next\`, and
89
+ comment if the tip lacks it. A finding, human reply or task edit newer than \`checkpoint.at\` wins.
84
90
  Check \`metadata\` for \`branchName\`, \`roundNumber\`, and any leftover
85
91
  \`findings\`; also read finding comments on the thread from step b \u2014 that is where
86
92
  new findings land. If \`metadata.humanReply\` is set, this task was previously blocked
@@ -97,10 +103,14 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
97
103
  the same thing: either proceed on your own judgement and say in your handoff that you did, or
98
104
  escalate to a human by blocking with a question addressed to nobody.
99
105
  d. Do the work per your specialization rules below.
100
- e. Post your result on the task's own thread (\`mcp__relai__add_task_comment\` or
106
+ e. If the work is on a branch, commit and push it, then call \`mcp__relai__checkpoint_task\` while the
107
+ task is still \`in_progress\`: before every \`update_task_status\` to "completed" or "blocked" and
108
+ every \`assign_task\`, including those in the role, round-limit and order rules below, and when out
109
+ of room. \`dirty\` and \`wipRef\` are only for work that cannot be committed.
110
+ f. Post your result on the task's own thread (\`mcp__relai__add_task_comment\` or
101
111
  \`mcp__relai__send_message\` type "handoff"), with \`toAgent\` set to whoever you
102
112
  report to: the task's \`createdBy\` unless told otherwise.
103
- f. If the work is done: \`mcp__relai__update_task_status\` \u2192 "completed"
113
+ g. If the work is done: \`mcp__relai__update_task_status\` \u2192 "completed"
104
114
  (or "blocked" if escalating). If another role must continue this same task,
105
115
  \`mcp__relai__assign_task\` to them and stop \u2014 do not complete.
106
116
 
@@ -225,8 +235,9 @@ types, empty classes) but you do not take the implementation yourself.
225
235
 
226
236
  - Read relevant files to understand current structure.
227
237
  - Write a clear technical design on this task's thread (\`mcp__relai__add_task_comment\`).
228
- If scaffolding helps, write it on the branch in \`branchName\` (derive one if missing,
229
- same slug rule as writer).
238
+ If scaffolding helps, write it on the branch in \`branchName\`: if set, \`git fetch origin\` then
239
+ \`git checkout -B {branchName} origin/{branchName}\`; if missing, derive one (same slug rule as writer)
240
+ and stamp it as \`metadata.branchName\` with \`mcp__relai__update_task_status\`. Never force-push.
230
241
  - \`mcp__relai__list_agents\`, pick specialization \`"writer"\` (prefer online), then
231
242
  \`mcp__relai__assign_task\` to them. Do not complete.
232
243
 
@@ -239,9 +250,13 @@ types, empty classes) but you do not take the implementation yourself.
239
250
  You implement specs on a branch and open a PR. Write production code and tests.
240
251
 
241
252
  **Workflow:**
242
- 1. Check metadata for branchName. If none, derive one from the task title:
253
+ 1. Check metadata for branchName. If set, the branch holds earlier work: \`git fetch origin\` then
254
+ \`git checkout -B {branchName} origin/{branchName}\` (only if the remote branch does not exist, create
255
+ it from the default branch). If none, derive one from the task title:
243
256
  slug the title to lowercase-hyphenated, prefix "feat/".
244
- Use Bash to create the branch: \`git checkout -b {branchName}\`.
257
+ Use Bash to create the branch: \`git checkout -b {branchName}\`. Stamp it on this task via
258
+ \`mcp__relai__update_task_status\` with status \`"in_progress"\` and \`metadata.branchName\`.
259
+ Never force-push to make a push succeed.
245
260
  2. Read the spec. Read existing files to understand patterns (Read, Grep, Glob).
246
261
  3. Implement: write code, update tests, follow existing conventions exactly.
247
262
  4. Run tests: check package.json scripts for the test command.
@@ -250,7 +265,8 @@ You implement specs on a branch and open a PR. Write production code and tests.
250
265
  - First try \`gh pr create --fill\` \u2014 captures the PR URL automatically when the GitHub CLI is installed and authed.
251
266
  - If \`gh\` isn't available (\`command -v gh\` fails) or it returns a non-zero exit, fall back to \`git push -u origin {branchName}\`. Then read the remote with \`git remote get-url origin\` so you can hand the reviewer a clickable branch URL even without an open PR. Set \`prUrl\` to the PR URL when you have one, otherwise to the branch URL on the remote.
252
267
  7. Post a handoff on the task's own thread with \`mcp__relai__add_task_comment\` (type: "handoff"):
253
- PR URL, head sha, what landed, and tests. Then \`mcp__relai__update_task_status\` \u2192 "completed".
268
+ PR URL, head sha, what landed, and tests. Then, after the step e checkpoint,
269
+ \`mcp__relai__update_task_status\` \u2192 "completed".
254
270
  - If the task has \`verifyReviewerId\` or any reviewer gate, completed moves it to
255
271
  pending_verification. Do not then \`update_task_status\` to \`"in_progress"\` \u2014
256
272
  the reviewer decides via \`submit_review\`, and a reject returns the task to
@@ -1889,7 +1905,8 @@ var PITBOSS_NAMES = [
1889
1905
  "ENV_GRANT",
1890
1906
  "TASK_REPO_ID",
1891
1907
  "TASK_REPO_URL",
1892
- "OWNER_POOL_SLOT"
1908
+ "OWNER_POOL_SLOT",
1909
+ "SESSION_MAX_MS"
1893
1910
  ].flatMap(envNames);
1894
1911
  var WORKER_KNOBS = [
1895
1912
  "API_URL",
@@ -2750,6 +2767,13 @@ async function clearUnavailable(config2, logPrefix = "[claude-worker]") {
2750
2767
  }
2751
2768
 
2752
2769
  // ../claude-worker/src/worker.ts
2770
+ var SESSION_HEARTBEAT_MS = 6e4;
2771
+ var DEFAULT_SESSION_MAX_MS = 90 * 6e4;
2772
+ var MAX_TIMER_MS = 2147483647;
2773
+ function sessionMaxMs(source = process.env) {
2774
+ const ms = Number(readEnv("SESSION_MAX_MS", source) ?? NaN);
2775
+ return Number.isFinite(ms) && ms >= 0 ? Math.min(ms, MAX_TIMER_MS) : DEFAULT_SESSION_MAX_MS;
2776
+ }
2753
2777
  async function heartbeat(config2, logPrefix = "[claude-worker]") {
2754
2778
  await fetch(`${config2.apiUrl}/agents/${config2.agentId}/heartbeat`, {
2755
2779
  method: "PUT",
@@ -2757,6 +2781,17 @@ async function heartbeat(config2, logPrefix = "[claude-worker]") {
2757
2781
  body: "{}"
2758
2782
  }).catch((err) => console.warn(`${logPrefix} Heartbeat failed:`, err.message));
2759
2783
  }
2784
+ async function withHeartbeat(config2, logPrefix, session) {
2785
+ const maxMs = sessionMaxMs();
2786
+ const quiet = maxMs > 0 ? setTimeout(() => clearInterval(beat), maxMs) : void 0;
2787
+ const beat = setInterval(() => void heartbeat(config2, logPrefix), SESSION_HEARTBEAT_MS);
2788
+ try {
2789
+ return await session();
2790
+ } finally {
2791
+ clearTimeout(quiet);
2792
+ clearInterval(beat);
2793
+ }
2794
+ }
2760
2795
  async function assertRepoOrExit(config2, logPrefix = "[claude-worker]") {
2761
2796
  const repoUrl = await fetchRepoUrl(config2.apiUrl, config2.repoId, config2.apiSecret);
2762
2797
  const check = checkRepoMatch(config2.repoPath, repoUrl);
@@ -3658,6 +3693,32 @@ async function checkWork(config2) {
3658
3693
  repoIds: [...new Set(myTasks.map((t) => t.repoId).filter((id) => typeof id === "string"))].sort()
3659
3694
  };
3660
3695
  }
3696
+ async function recordSessionTimeout(config2, taskId, minutes, timeouts) {
3697
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${config2.apiSecret}` };
3698
+ const count = (timeouts.get(taskId) ?? 0) + 1;
3699
+ timeouts.set(taskId, count);
3700
+ try {
3701
+ const res = await fetch(`${config2.apiUrl}/tasks/${taskId}`, { headers });
3702
+ if (!res.ok) throw new Error(`task lookup failed (${res.status} ${res.statusText})`);
3703
+ const { data: task } = await res.json();
3704
+ const mine = task?.assignedTo === config2.agentId;
3705
+ if (!mine) console.error(`[event-worker] ${taskId} is not assigned to this agent (a review, or reassigned), so the timeout is a comment only`);
3706
+ const block = mine && count >= 2;
3707
+ const out = block ? await fetch(`${config2.apiUrl}/tasks/${taskId}`, {
3708
+ method: "PUT",
3709
+ headers,
3710
+ body: JSON.stringify({ status: "blocked", metadata: { blockedReason: `The session cap (${minutes} minutes) was hit twice on this task. Each session was killed. Split the task or find out why it hangs before reassigning it.` } })
3711
+ }) : await fetch(`${config2.apiUrl}/tasks/${taskId}/comments`, {
3712
+ method: "POST",
3713
+ headers,
3714
+ body: JSON.stringify({ body: `The session working this task hit the ${minutes}-minute cap and was killed. The task stays as it is, and the next wake resumes it in a fresh session.` })
3715
+ });
3716
+ if (!out.ok) console.error(`[event-worker] Could not record the session timeout on ${taskId} (${out.status} ${out.statusText})`);
3717
+ else if (block) timeouts.delete(taskId);
3718
+ } catch (err) {
3719
+ console.error("[event-worker] Could not record the session timeout:", err instanceof Error ? err.message : String(err));
3720
+ }
3721
+ }
3661
3722
  var EVENT_KINDS = [
3662
3723
  "message.posted",
3663
3724
  "task.created",
@@ -3671,6 +3732,9 @@ var EVENT_KINDS = [
3671
3732
  "task.blocked",
3672
3733
  "task.pending_verification",
3673
3734
  "task.stalled",
3735
+ "task.stall_released",
3736
+ "task.down_released",
3737
+ "task.rerouted",
3674
3738
  "task.verified",
3675
3739
  "task.verification_failed",
3676
3740
  "task.review_requested",
@@ -3704,6 +3768,16 @@ function endsTask(event, taskId) {
3704
3768
  if (!task || task.id !== taskId) return false;
3705
3769
  return task.status === "cancelled" || task.archivedAt !== void 0 && task.archivedAt !== null;
3706
3770
  }
3771
+ var RELEASE_KINDS = ["task.stall_released", "task.down_released"];
3772
+ function releasedTask(event, taskId) {
3773
+ const e = event;
3774
+ return e?.targetId === taskId && typeof e.kind === "string" && RELEASE_KINDS.includes(e.kind);
3775
+ }
3776
+ function reroutedAway(event, taskId, agentId) {
3777
+ const e = event;
3778
+ if (e?.kind !== "task.rerouted" || e.targetId !== taskId) return false;
3779
+ return e.payload?.previousAssignee === agentId && typeof e.payload.assignedTo === "string" && e.payload.assignedTo !== agentId;
3780
+ }
3707
3781
  async function runEventWorker(config2) {
3708
3782
  console.log(`[event-worker] Starting \u2014 agent ${config2.agentId}, watching ${config2.apiUrl}/events`);
3709
3783
  console.log(`[event-worker] Repo: ${config2.repoPath} | Runtime: ${config2.runtime} | Model: ${config2.model}`);
@@ -3711,6 +3785,7 @@ async function runEventWorker(config2) {
3711
3785
  await assertNotOwnerScopedOrExit(config2, "[event-worker]");
3712
3786
  await selfSubscribe(config2);
3713
3787
  let live = null;
3788
+ const timeouts = /* @__PURE__ */ new Map();
3714
3789
  const queue = createRunQueue(async () => {
3715
3790
  await heartbeat(config2, "[event-worker]");
3716
3791
  const slotMode = readEnv("OWNER_POOL_SLOT") === "1";
@@ -3765,6 +3840,8 @@ async function runEventWorker(config2) {
3765
3840
  }
3766
3841
  }
3767
3842
  const controller = new AbortController();
3843
+ const maxMs = sessionMaxMs();
3844
+ let timedOut = false;
3768
3845
  try {
3769
3846
  console.log("[event-worker] Event received \u2014 running session...");
3770
3847
  const activityFile = readEnv("ACTIVITY_FILE");
@@ -3784,9 +3861,16 @@ async function runEventWorker(config2) {
3784
3861
  setEnv(TASK_REPO_URL_ENV, checkout.repoUrl);
3785
3862
  }
3786
3863
  if (checkout) live = { taskId: checkout.taskId, controller };
3864
+ const cap = maxMs > 0 ? setTimeout(() => {
3865
+ if (controller.signal.aborted) return;
3866
+ timedOut = true;
3867
+ controller.abort();
3868
+ }, maxMs) : void 0;
3787
3869
  try {
3788
- await getAdapter(config2.runtime).runSession(config2, { signal: controller.signal });
3870
+ await withHeartbeat(config2, "[event-worker]", () => getAdapter(config2.runtime).runSession(config2, { signal: controller.signal }));
3871
+ if (checkout) timeouts.delete(checkout.taskId);
3789
3872
  } finally {
3873
+ clearTimeout(cap);
3790
3874
  live = null;
3791
3875
  if (controller.signal.aborted && readEnv(RESET_SEAT_DIR_ENV)) {
3792
3876
  try {
@@ -3809,8 +3893,14 @@ async function runEventWorker(config2) {
3809
3893
  await clearUnavailable(config2, "[event-worker]");
3810
3894
  } catch (err) {
3811
3895
  const text = err instanceof Error ? err.message : String(err);
3896
+ if (timedOut) {
3897
+ const minutes = Math.max(1, Math.ceil(maxMs / 6e4));
3898
+ console.error(`[event-worker] Session hit the ${minutes}-minute cap (SESSION_MAX_MS) and was killed${checkout ? "" : "; it had no tracked task, so no task was written"}`);
3899
+ if (checkout) await recordSessionTimeout(config2, checkout.taskId, minutes, timeouts);
3900
+ return;
3901
+ }
3812
3902
  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)})`);
3903
+ console.log(`[event-worker] Session stopped because its task was ${controller.signal.reason} (${text.split("\n")[0].slice(0, 120)})`);
3814
3904
  return;
3815
3905
  }
3816
3906
  const exitActivityFile = readEnv("ACTIVITY_FILE");
@@ -3857,9 +3947,10 @@ async function runEventWorker(config2) {
3857
3947
  try {
3858
3948
  const event = JSON.parse(raw.data);
3859
3949
  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();
3950
+ const why = !live || live.controller.signal.aborted ? null : endsTask(event, live.taskId) ? "cancelled or archived" : releasedTask(event, live.taskId) ? "released to the queue" : reroutedAway(event, live.taskId, config2.agentId) ? "rerouted to another agent" : null;
3951
+ if (live && why) {
3952
+ console.warn(`[event-worker] task ${live.taskId} was ${why} \u2014 stopping its session`);
3953
+ live.controller.abort(why);
3863
3954
  }
3864
3955
  } catch {
3865
3956
  }
package/dist/mcp.js CHANGED
@@ -12343,7 +12343,8 @@ var ApiClient = class {
12343
12343
  this.headers = {
12344
12344
  "Content-Type": "application/json",
12345
12345
  Authorization: `Bearer ${config.secret}`,
12346
- ...config.ownerId ? { "X-Owner-Id": config.ownerId } : {}
12346
+ ...config.ownerId ? { "X-Owner-Id": config.ownerId } : {},
12347
+ ...config.sessionId ? { "X-Shipster-Session": config.sessionId } : {}
12347
12348
  };
12348
12349
  }
12349
12350
  // For routes with sibling fields next to `data` (e.g. the unread feed's
@@ -12653,6 +12654,9 @@ var ApiClient = class {
12653
12654
  releaseTask(taskId, force) {
12654
12655
  return this.request("POST", `/tasks/${taskId}/release`, force === void 0 ? {} : { force });
12655
12656
  }
12657
+ checkpointTask(taskId, checkpoint) {
12658
+ return this.request("POST", `/tasks/${taskId}/checkpoint`, checkpoint);
12659
+ }
12656
12660
  archiveThread(threadId) {
12657
12661
  return this.request("PUT", `/threads/${threadId}/archive`, {});
12658
12662
  }
@@ -12709,6 +12713,26 @@ var ApiClient = class {
12709
12713
  }
12710
12714
  };
12711
12715
 
12716
+ // ../mcp-server/src/session-id.ts
12717
+ import { randomBytes } from "node:crypto";
12718
+ var SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
12719
+ var URL_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
12720
+ var generated;
12721
+ function generate() {
12722
+ const bytes = randomBytes(12);
12723
+ let out = "ses_";
12724
+ for (const b of bytes) out += URL_SAFE[b % URL_SAFE.length];
12725
+ return out;
12726
+ }
12727
+ function resolveSessionId(env2 = process.env) {
12728
+ for (const name of ["CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID"]) {
12729
+ const value = env2[name];
12730
+ if (value && SESSION_ID_PATTERN.test(value)) return value;
12731
+ }
12732
+ generated ??= generate();
12733
+ return generated;
12734
+ }
12735
+
12712
12736
  // ../mcp-server/src/register-tools.ts
12713
12737
  init_zod();
12714
12738
 
@@ -12856,7 +12880,7 @@ function pausedExplanation(held) {
12856
12880
  return {
12857
12881
  omitted: held.length,
12858
12882
  pauses: [...seen.values()],
12859
- instruction: "The owner paused new work. Start nothing new: finish the current step of any in_progress task, commit or push what you have, post a checkpoint comment, and stop."
12883
+ instruction: "The owner paused new work. Start nothing new: finish the current step of any in_progress task, commit or push what you have, save a checkpoint with checkpoint_task, and stop."
12860
12884
  };
12861
12885
  }
12862
12886
  function listTasksLimit() {
@@ -13694,7 +13718,7 @@ ${version.body}`
13694
13718
  },
13695
13719
  {
13696
13720
  name: "get_my_tasks",
13697
- description: "Retrieve tasks assigned to this agent. Use this at the start of a session or when you want to know what work is queued for you. Returns tasks with status 'assigned' or 'in_progress'. Always call this before starting new work so you don't duplicate effort. humanLabel 'Not picked up' means this task was assigned to you hours ago and nothing has touched it since \u2014 the assignment notice never reached you. Start it now, or say on its thread why you are not going to. Capped and newest-first, with descriptions clipped \u2014 an uncapped queue once blew past the tool-result limit. A task still 'assigned' inside a paused scope (repo, order or account) is left out and explained in a top-level `paused` block: the owner paused new work, so start nothing new; in_progress tasks stay listed so you can finish the step and post a checkpoint. taskCount is the true total; raise `limit` to see more. A row marked truncated:true is a pointer \u2014 call get_task for the full description.",
13721
+ description: "Retrieve tasks assigned to this agent. Use this at the start of a session or when you want to know what work is queued for you. Returns tasks with status 'assigned' or 'in_progress'. Always call this before starting new work so you don't duplicate effort. humanLabel 'Not picked up' means this task was assigned to you hours ago and nothing has touched it since \u2014 the assignment notice never reached you. Start it now, or say on its thread why you are not going to. Capped and newest-first, with descriptions clipped \u2014 an uncapped queue once blew past the tool-result limit. A task still 'assigned' inside a paused scope (repo, order or account) is left out and explained in a top-level `paused` block: the owner paused new work, so start nothing new; in_progress tasks stay listed so you can finish the step and save a checkpoint with checkpoint_task. taskCount is the true total; raise `limit` to see more. A row marked truncated:true is a pointer \u2014 call get_task for the full description.",
13698
13722
  inputSchema: external_exports.object({
13699
13723
  status: external_exports.enum(["assigned", "in_progress", "pending", "all"]).optional().describe("Filter by task status. Omit for both 'assigned' and 'in_progress'."),
13700
13724
  limit: external_exports.number().int().min(1).optional().describe("Max tasks to return, newest-updated first. Defaults to 50.")
@@ -13772,6 +13796,33 @@ ${version.body}`
13772
13796
  });
13773
13797
  }
13774
13798
  },
13799
+ {
13800
+ name: "checkpoint_task",
13801
+ description: "Save your work-in-progress state on an in_progress task you hold. The next session on it remembers nothing of this one and starts from this checkpoint. Call it before you stop working the task for any reason, and after any step you would not want redone. Each call replaces the previous checkpoint. You keep the task; with a pause pending the checkpoint releases it and the result says to stop.",
13802
+ inputSchema: external_exports.object({
13803
+ taskId: external_exports.string().describe("The task you hold."),
13804
+ branch: external_exports.string().describe("Branch the work is on."),
13805
+ headSha: external_exports.string().describe("Full sha of your last commit."),
13806
+ worktree: external_exports.string().optional().describe("Checkout path, if not the repo path."),
13807
+ done: external_exports.array(external_exports.string()).describe("Finished and proved, with the evidence."),
13808
+ next: external_exports.array(external_exports.string()).describe("What to do next, in order."),
13809
+ unverified: external_exports.array(external_exports.string()).describe("Claims not yet proved, and what would prove each."),
13810
+ dirty: external_exports.boolean().optional().describe("Uncommitted changes exist. Needs wipRef."),
13811
+ wipRef: external_exports.string().optional().describe("Pushed ref holding them: refs/wip/<taskId>.")
13812
+ }),
13813
+ handler: async (input) => {
13814
+ if (input.dirty && !input.wipRef) {
13815
+ return mcpFail(new Error("A dirty checkpoint needs wipRef: push the uncommitted work to a ref first."));
13816
+ }
13817
+ const { taskId, ...checkpoint } = input;
13818
+ try {
13819
+ const { task } = await client.checkpointTask(taskId, checkpoint);
13820
+ return { content: [{ type: "text", text: !task.assignedTo ? `Checkpoint saved and task ${taskId} released: a pause was pending. Stop working on it now.` : `Checkpoint saved on task ${taskId}. You still hold it.` }] };
13821
+ } catch (err) {
13822
+ return mcpFail(err);
13823
+ }
13824
+ }
13825
+ },
13775
13826
  buildUpdateTaskTool(client),
13776
13827
  {
13777
13828
  name: "send_message",
@@ -15190,7 +15241,10 @@ if (OWNER_MODE) {
15190
15241
  var apiClient = new ApiClient({
15191
15242
  baseUrl: API_URL,
15192
15243
  secret: OWNER_MODE ? API_OWNER_TOKEN : API_SECRET,
15193
- ownerId: OWNER_MODE ? OWNER_ID : void 0
15244
+ ownerId: OWNER_MODE ? OWNER_ID : void 0,
15245
+ // stdio is one process per agent session, so one id per process is right.
15246
+ // HTTP serves several callers from this single client, so it sends none.
15247
+ sessionId: TRANSPORT === "stdio" ? resolveSessionId() : void 0
15194
15248
  });
15195
15249
  var server = createMcpServer(OWNER_MODE ? "relai-operator" : "relai", pkg.version);
15196
15250
  var toolConfig = OWNER_MODE ? { ownerMode: true, ownerId: OWNER_ID } : { ownerMode: false, agentId: AGENT_ID, repoId: REPO_ID };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipster/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [