@ricsam/r5dctl 0.0.132 → 0.0.133

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/cjs/cli.cjs CHANGED
@@ -30,10 +30,12 @@ var cli_exports = {};
30
30
  __export(cli_exports, {
31
31
  BRANCH_DELETION_POLL_INTERVAL_MS: () => BRANCH_DELETION_POLL_INTERVAL_MS,
32
32
  BRANCH_DELETION_WAIT_TIMEOUT_MS: () => BRANCH_DELETION_WAIT_TIMEOUT_MS,
33
+ R5dctlClaimScopeMismatchError: () => R5dctlClaimScopeMismatchError,
33
34
  advanceTransientDevicePollBackoff: () => advanceTransientDevicePollBackoff,
34
35
  branchDeletionConfirmationMessage: () => branchDeletionConfirmationMessage,
35
36
  branchDeletionsConfirmationMessage: () => branchDeletionsConfirmationMessage,
36
37
  branchDeletionsFailureMessage: () => branchDeletionsFailureMessage,
38
+ claimScopeMatches: () => claimScopeMatches,
37
39
  createBranchThroughPlatform: () => createBranchThroughPlatform,
38
40
  deleteBranchThroughPlatform: () => deleteBranchThroughPlatform,
39
41
  deleteBranchesThroughPlatform: () => deleteBranchesThroughPlatform,
@@ -53,6 +55,7 @@ __export(cli_exports, {
53
55
  parseAnswerQuestionsCommandArgs: () => parseAnswerQuestionsCommandArgs,
54
56
  parseBranchCreateArgs: () => parseBranchCreateArgs,
55
57
  parseBranchDeleteArgs: () => parseBranchDeleteArgs,
58
+ parseCheckoutPathArgs: () => parseCheckoutPathArgs,
56
59
  parseClaimDuration: () => parseClaimDuration,
57
60
  parseConversationForkArgs: () => parseConversationForkArgs,
58
61
  parseConversationInspectWorkArgs: () => parseConversationInspectWorkArgs,
@@ -78,6 +81,7 @@ __export(cli_exports, {
78
81
  renderBranchDeletionStatus: () => renderBranchDeletionStatus,
79
82
  renderBranchList: () => renderBranchList,
80
83
  renderBranchOperation: () => renderBranchOperation,
84
+ renderCheckoutPath: () => renderCheckoutPath,
81
85
  renderConversationForkResponse: () => renderConversationForkResponse,
82
86
  renderConversationHeadResponse: () => renderConversationHeadResponse,
83
87
  renderConversationNodeResponse: () => renderConversationNodeResponse,
@@ -178,9 +182,10 @@ const SESSION_RUN_HELP_TEXT = [
178
182
  " r5dctl session stop <session-id>",
179
183
  " r5dctl session set-worker <session-id> --worker <label>",
180
184
  " r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
181
- " r5dctl [-s <session-id>] session claim <topic> [--ttl <duration>] [--note <text>]",
182
- " r5dctl [-s <session-id>] session claim <topic> --status | --renew [--ttl <duration>] | --release",
183
- " r5dctl [-s <session-id>] session claims",
185
+ " r5dctl [-s <session-id>] [-p <project> --branch <branch>] session claim <topic> [--ttl <duration>] [--note <text>]",
186
+ " r5dctl [-s <session-id>] [-p <project> --branch <branch>] session claim <topic> --status | --renew [--ttl <duration>] | --release",
187
+ " r5dctl [-s <session-id>] [-p <project> --branch <branch>] session claims",
188
+ " r5dctl [-s <session-id>] -p <project> --branch <branch> checkout path",
184
189
  "",
185
190
  "Create, inspect, resume, and stop ordinary agent sessions.",
186
191
  "prompt reaches a busy session by --delivery: steer (default) lands after the agent's next tool result,",
@@ -193,6 +198,11 @@ const SESSION_RUN_HELP_TEXT = [
193
198
  "on a pause, wait for the recipient's explicit reply before proceeding.",
194
199
  "claim takes atomic ownership of a topic within the session's project branch (one winner; a losing call prints the holder and expiry).",
195
200
  "Claims expire at --ttl (default 30m, max 24h) unless renewed; --renew and --release are fenced to the holder and its claim generation.",
201
+ "-p <project> --branch <branch> (both or neither) claims on a sibling checkout the same user owns instead of the session's own branch.",
202
+ "The reserved topic `checkout` declares tracked-file writes in that checkout: claim it before running the command, hold it until the",
203
+ "write is done, and release it. A running session native to that branch holds `checkout` implicitly, so the call names who to message.",
204
+ "Commands started while a cross-scope `checkout` claim is held also fence the claimed checkout on the bound worker (the fence line says",
205
+ "whether the worker supports it). checkout path prints where a project branch is checked out on the session's bound worker.",
196
206
  "Agent shells act as their own session; user credentials select the acting session with -s or R5D_SESSION_ID.",
197
207
  "A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
198
208
  "--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
@@ -215,6 +225,11 @@ const SHARED_HELP_ENTRIES = [
215
225
  { section: "auth", usage: "auth api-key list", description: "List API keys for the current account." },
216
226
  { section: "auth", usage: "auth api-key revoke <key-id>", description: "Revoke an API key." },
217
227
  { section: "workspace", usage: "workspace status", description: "Inspect per-user workspace Git state and active remediation." },
228
+ {
229
+ section: "workspace",
230
+ usage: "workspace recover-hydration --worker <label> --transaction <id> --confirm",
231
+ description: "Preserve current checkout files and archive interrupted hydration evidence, then reconcile from the previous recorded basis. Stop active worker processes first."
232
+ },
218
233
  {
219
234
  section: "workspace",
220
235
  usage: "workspace sync [--worker <label>]",
@@ -346,13 +361,18 @@ const SHARED_HELP_ENTRIES = [
346
361
  },
347
362
  {
348
363
  section: "sessions",
349
- usage: "[-s <session-id>] session claim <topic> [--ttl <duration>] [--note <text>] [--status|--renew|--release]",
350
- description: "Atomically own a coordination topic within the session's project branch; one winner, and a losing call prints the current holder and expiry. --status reads it, --renew extends your lease, --release gives it up. Agent shells act as their own session."
364
+ usage: "[-s <session-id>] [-p <project> --branch <branch>] session claim <topic> [--ttl <duration>] [--note <text>] [--status|--renew|--release]",
365
+ description: "Atomically own a coordination topic within the session's project branch, or with -p and --branch on a sibling checkout; one winner, and a losing call prints the current holder and expiry. --status reads it, --renew extends your lease, --release gives it up. Claim the reserved topic `checkout` before writing tracked files in another checkout. Agent shells act as their own session."
351
366
  },
352
367
  {
353
368
  section: "sessions",
354
- usage: "[-s <session-id>] session claims",
355
- description: "List the claims on the session's project branch with holder, generation, and expiry."
369
+ usage: "[-s <session-id>] [-p <project> --branch <branch>] session claims",
370
+ description: "List the claims on the session's project branch, or on a sibling checkout, with holder, origin branch, generation, and expiry."
371
+ },
372
+ {
373
+ section: "branches",
374
+ usage: "[-s <session-id>] -p <project> --branch <branch> checkout path",
375
+ description: "Print the absolute path of a project branch checkout on the acting session's bound worker."
356
376
  },
357
377
  {
358
378
  section: "sessions",
@@ -955,13 +975,25 @@ function parseSessionProcessLogArgs(args) {
955
975
  const grep = parsed.values.get("--grep");
956
976
  return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
957
977
  }
978
+ function resolveClaimScope(explicitProject, explicitBranch, options) {
979
+ const project = explicitProject?.trim() || options.project?.trim() || void 0;
980
+ const branch = explicitBranch?.trim() || options.branch?.trim() || void 0;
981
+ if (project === void 0 && branch === void 0) return void 0;
982
+ if (project === void 0 || branch === void 0) {
983
+ throw new Error(
984
+ "A scoped claim needs both -p/--project and --branch/-b; pass both to act on a sibling checkout, or neither for the session's own branch"
985
+ );
986
+ }
987
+ return { projectId: project, branchName: branch };
988
+ }
958
989
  const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
959
990
  function parseClaimDuration(value) {
960
991
  const match = CLAIM_DURATION_PATTERN.exec(value.trim());
961
992
  if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
962
993
  const amount = Number(match[1]);
963
994
  const seconds = match[2] === "s" ? amount : match[2] === "m" ? amount * 60 : amount * 3600;
964
- if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400) throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
995
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
996
+ throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
965
997
  return seconds;
966
998
  }
967
999
  function resolveClaimSessionId(explicit, options, env) {
@@ -970,7 +1002,7 @@ function resolveClaimSessionId(explicit, options, env) {
970
1002
  function parseSessionClaimArgs(args, options = {}, env = process.env) {
971
1003
  const parsed = parseCommandOperands(
972
1004
  args,
973
- /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
1005
+ /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
974
1006
  /* @__PURE__ */ new Map([
975
1007
  ["--status", "--status"],
976
1008
  ["--renew", "--renew"],
@@ -983,7 +1015,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
983
1015
  if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
984
1016
  const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
985
1017
  const ttl = parsed.values.get("--ttl");
986
- if (ttl !== void 0 && action !== "acquire" && action !== "renew") throw new Error("--ttl is only supported when acquiring or renewing a claim");
1018
+ if (ttl !== void 0 && action !== "acquire" && action !== "renew")
1019
+ throw new Error("--ttl is only supported when acquiring or renewing a claim");
987
1020
  const note = parsed.values.get("--note");
988
1021
  if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
989
1022
  const generationValue = parsed.values.get("--generation");
@@ -991,22 +1024,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
991
1024
  throw new Error("--generation is only supported with --renew or --release");
992
1025
  }
993
1026
  const generation = generationValue === void 0 ? void 0 : Number(generationValue);
994
- if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1)) throw new Error("--generation must be a positive integer");
1027
+ if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
1028
+ throw new Error("--generation must be a positive integer");
995
1029
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
1030
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
996
1031
  return {
997
1032
  action,
998
1033
  topic,
999
1034
  ...sessionId ? { sessionId } : {},
1035
+ ...scope ? { scope } : {},
1000
1036
  ...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
1001
1037
  ...note !== void 0 ? { note } : {},
1002
1038
  ...generation !== void 0 ? { generation } : {}
1003
1039
  };
1004
1040
  }
1005
1041
  function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
1006
- const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session"]), /* @__PURE__ */ new Map(), "session claims");
1042
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "session claims");
1007
1043
  if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
1008
1044
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
1009
- return sessionId ? { sessionId } : {};
1045
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
1046
+ return { ...sessionId ? { sessionId } : {}, ...scope ? { scope } : {} };
1047
+ }
1048
+ function parseCheckoutPathArgs(args, options = {}, env = process.env) {
1049
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "checkout path");
1050
+ if (parsed.positionals.length > 0) throw new Error(`Unexpected checkout path argument: ${parsed.positionals[0]}`);
1051
+ const projectId = parsed.values.get("--project")?.trim() || options.project?.trim();
1052
+ const branchName = parsed.values.get("--branch")?.trim() || options.branch?.trim();
1053
+ if (!projectId) throw new Error("--project/-p is required for `checkout path`");
1054
+ if (!branchName) throw new Error("--branch/-b is required for `checkout path`");
1055
+ const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
1056
+ return { projectId, branchName, ...sessionId ? { sessionId } : {} };
1057
+ }
1058
+ class R5dctlClaimScopeMismatchError extends Error {
1059
+ constructor(requested, answered) {
1060
+ super(
1061
+ `The server did not apply the requested claim scope ${requested.projectId}#${requested.branchName}; it answered for ${answered.projectPath ?? answered.projectId}#${answered.branchName}. Update the r5d server before using scoped claims`
1062
+ );
1063
+ this.name = "R5dctlClaimScopeMismatchError";
1064
+ }
1065
+ }
1066
+ function claimScopeMatches(requested, answered) {
1067
+ if (!requested || !answered) return true;
1068
+ const projectMatches = answered.projectId === requested.projectId || answered.projectPath === requested.projectId;
1069
+ return projectMatches && answered.branchName === requested.branchName;
1070
+ }
1071
+ function assertClaimScopeEcho(requested, answered) {
1072
+ if (!claimScopeMatches(requested, answered)) throw new R5dctlClaimScopeMismatchError(requested, answered);
1010
1073
  }
1011
1074
  const CLAIM_OUTCOME_TEXT = {
1012
1075
  acquired: "acquired",
@@ -1019,35 +1082,55 @@ const CLAIM_OUTCOME_TEXT = {
1019
1082
  released: "released"
1020
1083
  };
1021
1084
  function formatClaimExpiry(expiresAt, nowMs) {
1085
+ if (expiresAt === null) return "while the holder runs (implicit)";
1022
1086
  const expiresAtMs = Date.parse(expiresAt);
1023
1087
  if (!Number.isFinite(expiresAtMs)) return expiresAt;
1024
1088
  return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
1025
1089
  }
1090
+ function describeClaimScope(scope) {
1091
+ return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
1092
+ }
1026
1093
  function describeClaimHolder(claim) {
1027
1094
  const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
1028
- return `${claim.holderSessionId}${name}${claim.heldByActor ? " (this session)" : ""}`;
1095
+ const self = claim.heldByActor ? " (this session)" : "";
1096
+ let origin = "";
1097
+ if (claim.holderBranchName !== void 0 && claim.holderBranchName !== null) {
1098
+ const native = claim.holderProjectId === claim.projectId && claim.holderBranchName === claim.branchName;
1099
+ if (native) origin = claim.implicit ? " (native, running)" : " (native)";
1100
+ else if (claim.holderProjectId === claim.projectId) origin = ` (foreign: native to branch ${claim.holderBranchName})`;
1101
+ else origin = ` (foreign: native to ${claim.holderProjectId ?? "unknown"}#${claim.holderBranchName})`;
1102
+ }
1103
+ return `${claim.holderSessionId}${name}${self}${origin}`;
1104
+ }
1105
+ function describeClaimFence(response) {
1106
+ if (response.fenceExtended === void 0) return void 0;
1107
+ if (response.fenceExtended)
1108
+ return "Fence: commands this session starts while holding the claim also fence the claimed checkout on the bound worker";
1109
+ return `Fence: claim acquired, but ${response.fenceDetail ?? "the bound worker does not fence sibling checkouts (update the worker)"}; the claim still coordinates between sessions`;
1029
1110
  }
1030
1111
  function renderSessionClaimResponse(response, nowMs = Date.now()) {
1031
1112
  const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
1032
1113
  if (response.claim) {
1033
1114
  lines.push(
1034
1115
  `Claim: ${response.claim.topic}`,
1035
- `Branch: ${response.claim.branchName}`,
1116
+ `Scope: ${describeClaimScope(response.claim)}`,
1036
1117
  `Holder: ${describeClaimHolder(response.claim)}`,
1037
1118
  `Generation: ${response.claim.generation}`,
1038
1119
  `Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
1039
1120
  );
1040
1121
  if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
1041
1122
  }
1123
+ const fence = describeClaimFence(response);
1124
+ if (fence) lines.push(fence);
1042
1125
  return `${lines.join("\n")}
1043
1126
  `;
1044
1127
  }
1045
1128
  function renderSessionClaimList(response, nowMs = Date.now()) {
1046
- const header = `Claims on ${response.branchName} (acting session ${response.actorSessionId})`;
1129
+ const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
1047
1130
  if (response.claims.length === 0) return `${header}: none
1048
1131
  `;
1049
1132
  const rows = response.claims.map((claim) => {
1050
- const expiry = claim.expired ? "expired" : "expires";
1133
+ const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
1051
1134
  const note = claim.note ? ` note: ${claim.note}` : "";
1052
1135
  return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
1053
1136
  });
@@ -1055,6 +1138,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
1055
1138
  ${rows.join("\n")}
1056
1139
  `;
1057
1140
  }
1141
+ function renderCheckoutPath(response) {
1142
+ return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
1143
+ "\n"
1144
+ );
1145
+ }
1058
1146
  function assertAuthLoginArgs(args) {
1059
1147
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
1060
1148
  const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
@@ -1896,6 +1984,12 @@ function renderWorkspaceStatus(status) {
1896
1984
  `Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
1897
1985
  ...status.workers.flatMap((worker) => [
1898
1986
  `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
1987
+ ...worker.hydrationRecovery ? [
1988
+ ` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
1989
+ ` Evidence: ${worker.hydrationRecovery.transactionPath}`,
1990
+ ...(worker.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`),
1991
+ ` After stopping active processes: r5dctl workspace recover-hydration --worker ${JSON.stringify(worker.workerLabel)} --transaction ${worker.hydrationRecovery.transactionId} --confirm`
1992
+ ] : [],
1899
1993
  ...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
1900
1994
  ...renderWorkerCapacity(worker.workerLabel, worker.capacity)
1901
1995
  ])
@@ -1903,6 +1997,15 @@ function renderWorkspaceStatus(status) {
1903
1997
  if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
1904
1998
  lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
1905
1999
  }
2000
+ if (status.latestSync?.hydrationRecovery && !status.workers.some((worker) => worker.workerLabel === status.latestSync.workerLabel)) {
2001
+ lines.push(
2002
+ `Disconnected worker ${status.latestSync.workerLabel} reported interrupted hydration ${status.latestSync.hydrationRecovery.transactionId}; preserved evidence: ${status.latestSync.hydrationRecovery.transactionPath}. Reconnect it, inspect workspace status, and preserve the transaction before resuming work.`,
2003
+ ...(status.latestSync.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`)
2004
+ );
2005
+ }
2006
+ for (const [mountId, skip] of Object.entries(status.latestSync?.mountSkipReasons ?? {})) {
2007
+ for (const retainedPath of skip.retainedPaths ?? []) lines.push(`Retained writer entry for ${mountId}: ${retainedPath}`);
2008
+ }
1906
2009
  return `${lines.join("\n")}
1907
2010
  `;
1908
2011
  }
@@ -2189,11 +2292,17 @@ function renderSessionRunStart(session) {
2189
2292
  lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
2190
2293
  const sessionUrl = responseString(session, "sessionUrl");
2191
2294
  if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
2295
+ lines.push(...formatSessionWarningLines(session));
2192
2296
  const sessionId = responseString(session, "sessionId");
2193
2297
  if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
2194
2298
  return `${lines.join("\n")}
2195
2299
  `;
2196
2300
  }
2301
+ function formatSessionWarningLines(session) {
2302
+ const warnings = session.warnings;
2303
+ if (!Array.isArray(warnings)) return [];
2304
+ return warnings.filter((warning) => typeof warning === "string" && warning.length > 0).map((warning) => `Warning: ${warning}`);
2305
+ }
2197
2306
  function formatBranchStateHint(branch) {
2198
2307
  const target = branch ?? "<branch>";
2199
2308
  const commands = [
@@ -2244,6 +2353,7 @@ function renderSessionRunStatus(session) {
2244
2353
  }
2245
2354
  const childSessions = formatChildSessionsLine(session);
2246
2355
  if (childSessions) lines.push(childSessions);
2356
+ lines.push(...formatSessionWarningLines(session));
2247
2357
  lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
2248
2358
  const error = responseString(session, "error");
2249
2359
  if (error) lines.push(`Error: ${error}`);
@@ -2870,6 +2980,23 @@ async function executeR5dctlCommand(client, json, args) {
2870
2980
  write(result, renderWorkspaceSync(result));
2871
2981
  return;
2872
2982
  }
2983
+ if (first === "workspace" && second === "recover-hydration") {
2984
+ let worker;
2985
+ let transactionId;
2986
+ let confirmed = false;
2987
+ for (let index = 2; index < args.length; index += 1) {
2988
+ const arg = args[index];
2989
+ if (arg === "--worker") worker = requireValue(args[++index], "Missing worker label");
2990
+ else if (arg === "--transaction") transactionId = requireValue(args[++index], "Missing transaction id");
2991
+ else if (arg === "--confirm") confirmed = true;
2992
+ else throw new Error(`Unexpected recovery argument: ${arg}`);
2993
+ }
2994
+ if (!worker || !transactionId || !confirmed)
2995
+ throw new Error("workspace recover-hydration requires --worker, --transaction and --confirm; inspect workspace status first");
2996
+ const result = await client.workspace.recoverHydration({ worker, transactionId, confirmed: true });
2997
+ write(result, renderWorkspaceSync(result));
2998
+ return;
2999
+ }
2873
3000
  if (first === "workspace" && second === "reset") {
2874
3001
  const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
2875
3002
  const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
@@ -2900,7 +3027,7 @@ async function executeR5dctlCommand(client, json, args) {
2900
3027
  }
2901
3028
  if (first === "session" && second === "claim") {
2902
3029
  const claim = parseSessionClaimArgs(args.slice(2), {}, {});
2903
- const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
3030
+ const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
2904
3031
  const nowMs = Date.now();
2905
3032
  if (claim.action === "acquire") {
2906
3033
  const result2 = await client.claims.acquire({
@@ -2909,23 +3036,34 @@ async function executeR5dctlCommand(client, json, args) {
2909
3036
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
2910
3037
  ...claim.note !== void 0 ? { note: claim.note } : {}
2911
3038
  });
3039
+ if (!claimScopeMatches(claim.scope, result2.claim)) {
3040
+ if (result2.outcome === "acquired" && result2.claim.heldByActor) {
3041
+ await client.claims.release({
3042
+ topic: claim.topic,
3043
+ ...claim.sessionId ? { sessionId: claim.sessionId } : {},
3044
+ generation: result2.claim.generation
3045
+ }).catch(() => void 0);
3046
+ }
3047
+ throw new R5dctlClaimScopeMismatchError(claim.scope, result2.claim);
3048
+ }
2912
3049
  write(result2, renderSessionClaimResponse(result2, nowMs));
2913
3050
  if (result2.outcome === "held_by_other") {
2914
- throw new Error(
2915
- `Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} until ${result2.claim.expiresAt}; not acquired`
2916
- );
3051
+ const until = result2.claim.expiresAt === null ? "while it runs (native session)" : `until ${result2.claim.expiresAt}`;
3052
+ throw new Error(`Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} ${until}; not acquired`);
2917
3053
  }
2918
3054
  return;
2919
3055
  }
2920
3056
  if (claim.action === "status") {
2921
3057
  const list = await client.claims.list({ ...scope, topic: claim.topic });
3058
+ assertClaimScopeEcho(claim.scope, list);
2922
3059
  write(list, renderSessionClaimList(list, nowMs));
2923
3060
  return;
2924
3061
  }
2925
3062
  let generation = claim.generation;
2926
- if (generation === void 0) {
3063
+ if (generation === void 0 || claim.scope) {
2927
3064
  const list = await client.claims.list({ ...scope, topic: claim.topic });
2928
- const current = list.claims[0];
3065
+ assertClaimScopeEcho(claim.scope, list);
3066
+ const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
2929
3067
  if (!current) {
2930
3068
  write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
2931
3069
  `);
@@ -2939,7 +3077,7 @@ async function executeR5dctlCommand(client, json, args) {
2939
3077
  );
2940
3078
  throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
2941
3079
  }
2942
- generation = current.generation;
3080
+ generation ??= current.generation;
2943
3081
  }
2944
3082
  const result = claim.action === "renew" ? await client.claims.renew({
2945
3083
  topic: claim.topic,
@@ -2947,16 +3085,25 @@ async function executeR5dctlCommand(client, json, args) {
2947
3085
  generation,
2948
3086
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
2949
3087
  }) : await client.claims.release({ topic: claim.topic, ...scope, generation });
3088
+ assertClaimScopeEcho(claim.scope, result.claim);
2950
3089
  write(result, renderSessionClaimResponse(result, nowMs));
2951
3090
  const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
2952
3091
  if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
2953
3092
  return;
2954
3093
  }
2955
3094
  if (first === "session" && second === "claims") {
2956
- const list = await client.claims.list(parseSessionClaimsListArgs(args.slice(2), {}, {}));
3095
+ const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
3096
+ const list = await client.claims.list(selection);
3097
+ assertClaimScopeEcho(selection.scope, list);
2957
3098
  write(list, renderSessionClaimList(list));
2958
3099
  return;
2959
3100
  }
3101
+ if (first === "checkout" && second === "path") {
3102
+ const selection = parseCheckoutPathArgs(args.slice(2), {}, {});
3103
+ const result = await client.checkouts.path(selection);
3104
+ write(result, renderCheckoutPath(result));
3105
+ return;
3106
+ }
2960
3107
  if (first === "session" && second === "process-log") {
2961
3108
  const selection = parseSessionProcessLogArgs(args.slice(2));
2962
3109
  const log = await client.processes.log(selection.runId, {
@@ -3056,7 +3203,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
3056
3203
  throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
3057
3204
  }
3058
3205
  if (json) write({ results: deletion.results }, "");
3059
- if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3206
+ if (deletion.results.some((result) => !result.deleted && !result.accepted))
3207
+ throw new Error(branchDeletionsFailureMessage(deletion.results));
3060
3208
  return;
3061
3209
  }
3062
3210
  if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
@@ -3653,24 +3801,43 @@ function resolveCommandExecution(options, rest) {
3653
3801
  ...parseShellArgs(commandArgs)
3654
3802
  };
3655
3803
  }
3804
+ if (command === "checkout") {
3805
+ if (commandArgs[0] !== "path") throw new Error("Unknown checkout command");
3806
+ const parsed = parseCheckoutPathArgs(commandArgs.slice(1), options);
3807
+ return {
3808
+ kind: "plugin",
3809
+ pluginArgs: [
3810
+ "checkout",
3811
+ "path",
3812
+ `--project=${parsed.projectId}`,
3813
+ `--branch=${parsed.branchName}`,
3814
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3815
+ ]
3816
+ };
3817
+ }
3656
3818
  if (command === "session") {
3657
3819
  if (commandArgs[0] === "claim" || commandArgs[0] === "claims") {
3658
- if (options.project) throw new Error("--project/-p is not supported for `session claim`; the acting session selects the project branch");
3659
- if (options.branch) throw new Error("--branch/-b is not supported for `session claim`; the acting session selects the branch");
3660
3820
  const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
3661
- const withoutSessionValue = [];
3821
+ const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
3822
+ const withoutNormalizedValues = [];
3662
3823
  for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
3663
3824
  const arg = commandArgs.slice(1)[index];
3664
- if (arg === "--session") {
3825
+ if (normalizedFlags.has(arg)) {
3665
3826
  index += 1;
3666
3827
  continue;
3667
3828
  }
3668
- if (arg.startsWith("--session=")) continue;
3669
- withoutSessionValue.push(arg);
3829
+ if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
3830
+ withoutNormalizedValues.push(arg);
3670
3831
  }
3671
3832
  return {
3672
3833
  kind: "plugin",
3673
- pluginArgs: ["session", commandArgs[0], ...withoutSessionValue, ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []]
3834
+ pluginArgs: [
3835
+ "session",
3836
+ commandArgs[0],
3837
+ ...withoutNormalizedValues,
3838
+ ...parsed.scope ? [`--project=${parsed.scope.projectId}`, `--branch=${parsed.scope.branchName}`] : [],
3839
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3840
+ ]
3674
3841
  };
3675
3842
  }
3676
3843
  if (commandArgs[0] === "process-log") {
@@ -3844,10 +4011,12 @@ async function main(argv = process.argv.slice(2)) {
3844
4011
  0 && (module.exports = {
3845
4012
  BRANCH_DELETION_POLL_INTERVAL_MS,
3846
4013
  BRANCH_DELETION_WAIT_TIMEOUT_MS,
4014
+ R5dctlClaimScopeMismatchError,
3847
4015
  advanceTransientDevicePollBackoff,
3848
4016
  branchDeletionConfirmationMessage,
3849
4017
  branchDeletionsConfirmationMessage,
3850
4018
  branchDeletionsFailureMessage,
4019
+ claimScopeMatches,
3851
4020
  createBranchThroughPlatform,
3852
4021
  deleteBranchThroughPlatform,
3853
4022
  deleteBranchesThroughPlatform,
@@ -3867,6 +4036,7 @@ async function main(argv = process.argv.slice(2)) {
3867
4036
  parseAnswerQuestionsCommandArgs,
3868
4037
  parseBranchCreateArgs,
3869
4038
  parseBranchDeleteArgs,
4039
+ parseCheckoutPathArgs,
3870
4040
  parseClaimDuration,
3871
4041
  parseConversationForkArgs,
3872
4042
  parseConversationInspectWorkArgs,
@@ -3892,6 +4062,7 @@ async function main(argv = process.argv.slice(2)) {
3892
4062
  renderBranchDeletionStatus,
3893
4063
  renderBranchList,
3894
4064
  renderBranchOperation,
4065
+ renderCheckoutPath,
3895
4066
  renderConversationForkResponse,
3896
4067
  renderConversationHeadResponse,
3897
4068
  renderConversationNodeResponse,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.132",
3
+ "version": "0.0.133",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -72,9 +72,10 @@ const SESSION_RUN_HELP_TEXT = [
72
72
  " r5dctl session stop <session-id>",
73
73
  " r5dctl session set-worker <session-id> --worker <label>",
74
74
  " r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
75
- " r5dctl [-s <session-id>] session claim <topic> [--ttl <duration>] [--note <text>]",
76
- " r5dctl [-s <session-id>] session claim <topic> --status | --renew [--ttl <duration>] | --release",
77
- " r5dctl [-s <session-id>] session claims",
75
+ " r5dctl [-s <session-id>] [-p <project> --branch <branch>] session claim <topic> [--ttl <duration>] [--note <text>]",
76
+ " r5dctl [-s <session-id>] [-p <project> --branch <branch>] session claim <topic> --status | --renew [--ttl <duration>] | --release",
77
+ " r5dctl [-s <session-id>] [-p <project> --branch <branch>] session claims",
78
+ " r5dctl [-s <session-id>] -p <project> --branch <branch> checkout path",
78
79
  "",
79
80
  "Create, inspect, resume, and stop ordinary agent sessions.",
80
81
  "prompt reaches a busy session by --delivery: steer (default) lands after the agent's next tool result,",
@@ -87,6 +88,11 @@ const SESSION_RUN_HELP_TEXT = [
87
88
  "on a pause, wait for the recipient's explicit reply before proceeding.",
88
89
  "claim takes atomic ownership of a topic within the session's project branch (one winner; a losing call prints the holder and expiry).",
89
90
  "Claims expire at --ttl (default 30m, max 24h) unless renewed; --renew and --release are fenced to the holder and its claim generation.",
91
+ "-p <project> --branch <branch> (both or neither) claims on a sibling checkout the same user owns instead of the session's own branch.",
92
+ "The reserved topic `checkout` declares tracked-file writes in that checkout: claim it before running the command, hold it until the",
93
+ "write is done, and release it. A running session native to that branch holds `checkout` implicitly, so the call names who to message.",
94
+ "Commands started while a cross-scope `checkout` claim is held also fence the claimed checkout on the bound worker (the fence line says",
95
+ "whether the worker supports it). checkout path prints where a project branch is checked out on the session's bound worker.",
90
96
  "Agent shells act as their own session; user credentials select the acting session with -s or R5D_SESSION_ID.",
91
97
  "A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
92
98
  "--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
@@ -109,6 +115,11 @@ const SHARED_HELP_ENTRIES = [
109
115
  { section: "auth", usage: "auth api-key list", description: "List API keys for the current account." },
110
116
  { section: "auth", usage: "auth api-key revoke <key-id>", description: "Revoke an API key." },
111
117
  { section: "workspace", usage: "workspace status", description: "Inspect per-user workspace Git state and active remediation." },
118
+ {
119
+ section: "workspace",
120
+ usage: "workspace recover-hydration --worker <label> --transaction <id> --confirm",
121
+ description: "Preserve current checkout files and archive interrupted hydration evidence, then reconcile from the previous recorded basis. Stop active worker processes first."
122
+ },
112
123
  {
113
124
  section: "workspace",
114
125
  usage: "workspace sync [--worker <label>]",
@@ -240,13 +251,18 @@ const SHARED_HELP_ENTRIES = [
240
251
  },
241
252
  {
242
253
  section: "sessions",
243
- usage: "[-s <session-id>] session claim <topic> [--ttl <duration>] [--note <text>] [--status|--renew|--release]",
244
- description: "Atomically own a coordination topic within the session's project branch; one winner, and a losing call prints the current holder and expiry. --status reads it, --renew extends your lease, --release gives it up. Agent shells act as their own session."
254
+ usage: "[-s <session-id>] [-p <project> --branch <branch>] session claim <topic> [--ttl <duration>] [--note <text>] [--status|--renew|--release]",
255
+ description: "Atomically own a coordination topic within the session's project branch, or with -p and --branch on a sibling checkout; one winner, and a losing call prints the current holder and expiry. --status reads it, --renew extends your lease, --release gives it up. Claim the reserved topic `checkout` before writing tracked files in another checkout. Agent shells act as their own session."
245
256
  },
246
257
  {
247
258
  section: "sessions",
248
- usage: "[-s <session-id>] session claims",
249
- description: "List the claims on the session's project branch with holder, generation, and expiry."
259
+ usage: "[-s <session-id>] [-p <project> --branch <branch>] session claims",
260
+ description: "List the claims on the session's project branch, or on a sibling checkout, with holder, origin branch, generation, and expiry."
261
+ },
262
+ {
263
+ section: "branches",
264
+ usage: "[-s <session-id>] -p <project> --branch <branch> checkout path",
265
+ description: "Print the absolute path of a project branch checkout on the acting session's bound worker."
250
266
  },
251
267
  {
252
268
  section: "sessions",
@@ -849,13 +865,25 @@ function parseSessionProcessLogArgs(args) {
849
865
  const grep = parsed.values.get("--grep");
850
866
  return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
851
867
  }
868
+ function resolveClaimScope(explicitProject, explicitBranch, options) {
869
+ const project = explicitProject?.trim() || options.project?.trim() || void 0;
870
+ const branch = explicitBranch?.trim() || options.branch?.trim() || void 0;
871
+ if (project === void 0 && branch === void 0) return void 0;
872
+ if (project === void 0 || branch === void 0) {
873
+ throw new Error(
874
+ "A scoped claim needs both -p/--project and --branch/-b; pass both to act on a sibling checkout, or neither for the session's own branch"
875
+ );
876
+ }
877
+ return { projectId: project, branchName: branch };
878
+ }
852
879
  const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
853
880
  function parseClaimDuration(value) {
854
881
  const match = CLAIM_DURATION_PATTERN.exec(value.trim());
855
882
  if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
856
883
  const amount = Number(match[1]);
857
884
  const seconds = match[2] === "s" ? amount : match[2] === "m" ? amount * 60 : amount * 3600;
858
- if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400) throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
885
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
886
+ throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
859
887
  return seconds;
860
888
  }
861
889
  function resolveClaimSessionId(explicit, options, env) {
@@ -864,7 +892,7 @@ function resolveClaimSessionId(explicit, options, env) {
864
892
  function parseSessionClaimArgs(args, options = {}, env = process.env) {
865
893
  const parsed = parseCommandOperands(
866
894
  args,
867
- /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
895
+ /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
868
896
  /* @__PURE__ */ new Map([
869
897
  ["--status", "--status"],
870
898
  ["--renew", "--renew"],
@@ -877,7 +905,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
877
905
  if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
878
906
  const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
879
907
  const ttl = parsed.values.get("--ttl");
880
- if (ttl !== void 0 && action !== "acquire" && action !== "renew") throw new Error("--ttl is only supported when acquiring or renewing a claim");
908
+ if (ttl !== void 0 && action !== "acquire" && action !== "renew")
909
+ throw new Error("--ttl is only supported when acquiring or renewing a claim");
881
910
  const note = parsed.values.get("--note");
882
911
  if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
883
912
  const generationValue = parsed.values.get("--generation");
@@ -885,22 +914,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
885
914
  throw new Error("--generation is only supported with --renew or --release");
886
915
  }
887
916
  const generation = generationValue === void 0 ? void 0 : Number(generationValue);
888
- if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1)) throw new Error("--generation must be a positive integer");
917
+ if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
918
+ throw new Error("--generation must be a positive integer");
889
919
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
920
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
890
921
  return {
891
922
  action,
892
923
  topic,
893
924
  ...sessionId ? { sessionId } : {},
925
+ ...scope ? { scope } : {},
894
926
  ...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
895
927
  ...note !== void 0 ? { note } : {},
896
928
  ...generation !== void 0 ? { generation } : {}
897
929
  };
898
930
  }
899
931
  function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
900
- const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session"]), /* @__PURE__ */ new Map(), "session claims");
932
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "session claims");
901
933
  if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
902
934
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
903
- return sessionId ? { sessionId } : {};
935
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
936
+ return { ...sessionId ? { sessionId } : {}, ...scope ? { scope } : {} };
937
+ }
938
+ function parseCheckoutPathArgs(args, options = {}, env = process.env) {
939
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "checkout path");
940
+ if (parsed.positionals.length > 0) throw new Error(`Unexpected checkout path argument: ${parsed.positionals[0]}`);
941
+ const projectId = parsed.values.get("--project")?.trim() || options.project?.trim();
942
+ const branchName = parsed.values.get("--branch")?.trim() || options.branch?.trim();
943
+ if (!projectId) throw new Error("--project/-p is required for `checkout path`");
944
+ if (!branchName) throw new Error("--branch/-b is required for `checkout path`");
945
+ const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
946
+ return { projectId, branchName, ...sessionId ? { sessionId } : {} };
947
+ }
948
+ class R5dctlClaimScopeMismatchError extends Error {
949
+ constructor(requested, answered) {
950
+ super(
951
+ `The server did not apply the requested claim scope ${requested.projectId}#${requested.branchName}; it answered for ${answered.projectPath ?? answered.projectId}#${answered.branchName}. Update the r5d server before using scoped claims`
952
+ );
953
+ this.name = "R5dctlClaimScopeMismatchError";
954
+ }
955
+ }
956
+ function claimScopeMatches(requested, answered) {
957
+ if (!requested || !answered) return true;
958
+ const projectMatches = answered.projectId === requested.projectId || answered.projectPath === requested.projectId;
959
+ return projectMatches && answered.branchName === requested.branchName;
960
+ }
961
+ function assertClaimScopeEcho(requested, answered) {
962
+ if (!claimScopeMatches(requested, answered)) throw new R5dctlClaimScopeMismatchError(requested, answered);
904
963
  }
905
964
  const CLAIM_OUTCOME_TEXT = {
906
965
  acquired: "acquired",
@@ -913,35 +972,55 @@ const CLAIM_OUTCOME_TEXT = {
913
972
  released: "released"
914
973
  };
915
974
  function formatClaimExpiry(expiresAt, nowMs) {
975
+ if (expiresAt === null) return "while the holder runs (implicit)";
916
976
  const expiresAtMs = Date.parse(expiresAt);
917
977
  if (!Number.isFinite(expiresAtMs)) return expiresAt;
918
978
  return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
919
979
  }
980
+ function describeClaimScope(scope) {
981
+ return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
982
+ }
920
983
  function describeClaimHolder(claim) {
921
984
  const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
922
- return `${claim.holderSessionId}${name}${claim.heldByActor ? " (this session)" : ""}`;
985
+ const self = claim.heldByActor ? " (this session)" : "";
986
+ let origin = "";
987
+ if (claim.holderBranchName !== void 0 && claim.holderBranchName !== null) {
988
+ const native = claim.holderProjectId === claim.projectId && claim.holderBranchName === claim.branchName;
989
+ if (native) origin = claim.implicit ? " (native, running)" : " (native)";
990
+ else if (claim.holderProjectId === claim.projectId) origin = ` (foreign: native to branch ${claim.holderBranchName})`;
991
+ else origin = ` (foreign: native to ${claim.holderProjectId ?? "unknown"}#${claim.holderBranchName})`;
992
+ }
993
+ return `${claim.holderSessionId}${name}${self}${origin}`;
994
+ }
995
+ function describeClaimFence(response) {
996
+ if (response.fenceExtended === void 0) return void 0;
997
+ if (response.fenceExtended)
998
+ return "Fence: commands this session starts while holding the claim also fence the claimed checkout on the bound worker";
999
+ return `Fence: claim acquired, but ${response.fenceDetail ?? "the bound worker does not fence sibling checkouts (update the worker)"}; the claim still coordinates between sessions`;
923
1000
  }
924
1001
  function renderSessionClaimResponse(response, nowMs = Date.now()) {
925
1002
  const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
926
1003
  if (response.claim) {
927
1004
  lines.push(
928
1005
  `Claim: ${response.claim.topic}`,
929
- `Branch: ${response.claim.branchName}`,
1006
+ `Scope: ${describeClaimScope(response.claim)}`,
930
1007
  `Holder: ${describeClaimHolder(response.claim)}`,
931
1008
  `Generation: ${response.claim.generation}`,
932
1009
  `Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
933
1010
  );
934
1011
  if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
935
1012
  }
1013
+ const fence = describeClaimFence(response);
1014
+ if (fence) lines.push(fence);
936
1015
  return `${lines.join("\n")}
937
1016
  `;
938
1017
  }
939
1018
  function renderSessionClaimList(response, nowMs = Date.now()) {
940
- const header = `Claims on ${response.branchName} (acting session ${response.actorSessionId})`;
1019
+ const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
941
1020
  if (response.claims.length === 0) return `${header}: none
942
1021
  `;
943
1022
  const rows = response.claims.map((claim) => {
944
- const expiry = claim.expired ? "expired" : "expires";
1023
+ const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
945
1024
  const note = claim.note ? ` note: ${claim.note}` : "";
946
1025
  return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
947
1026
  });
@@ -949,6 +1028,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
949
1028
  ${rows.join("\n")}
950
1029
  `;
951
1030
  }
1031
+ function renderCheckoutPath(response) {
1032
+ return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
1033
+ "\n"
1034
+ );
1035
+ }
952
1036
  function assertAuthLoginArgs(args) {
953
1037
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
954
1038
  const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
@@ -1790,6 +1874,12 @@ function renderWorkspaceStatus(status) {
1790
1874
  `Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
1791
1875
  ...status.workers.flatMap((worker) => [
1792
1876
  `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
1877
+ ...worker.hydrationRecovery ? [
1878
+ ` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
1879
+ ` Evidence: ${worker.hydrationRecovery.transactionPath}`,
1880
+ ...(worker.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`),
1881
+ ` After stopping active processes: r5dctl workspace recover-hydration --worker ${JSON.stringify(worker.workerLabel)} --transaction ${worker.hydrationRecovery.transactionId} --confirm`
1882
+ ] : [],
1793
1883
  ...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
1794
1884
  ...renderWorkerCapacity(worker.workerLabel, worker.capacity)
1795
1885
  ])
@@ -1797,6 +1887,15 @@ function renderWorkspaceStatus(status) {
1797
1887
  if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
1798
1888
  lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
1799
1889
  }
1890
+ if (status.latestSync?.hydrationRecovery && !status.workers.some((worker) => worker.workerLabel === status.latestSync.workerLabel)) {
1891
+ lines.push(
1892
+ `Disconnected worker ${status.latestSync.workerLabel} reported interrupted hydration ${status.latestSync.hydrationRecovery.transactionId}; preserved evidence: ${status.latestSync.hydrationRecovery.transactionPath}. Reconnect it, inspect workspace status, and preserve the transaction before resuming work.`,
1893
+ ...(status.latestSync.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`)
1894
+ );
1895
+ }
1896
+ for (const [mountId, skip] of Object.entries(status.latestSync?.mountSkipReasons ?? {})) {
1897
+ for (const retainedPath of skip.retainedPaths ?? []) lines.push(`Retained writer entry for ${mountId}: ${retainedPath}`);
1898
+ }
1800
1899
  return `${lines.join("\n")}
1801
1900
  `;
1802
1901
  }
@@ -2083,11 +2182,17 @@ function renderSessionRunStart(session) {
2083
2182
  lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
2084
2183
  const sessionUrl = responseString(session, "sessionUrl");
2085
2184
  if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
2185
+ lines.push(...formatSessionWarningLines(session));
2086
2186
  const sessionId = responseString(session, "sessionId");
2087
2187
  if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
2088
2188
  return `${lines.join("\n")}
2089
2189
  `;
2090
2190
  }
2191
+ function formatSessionWarningLines(session) {
2192
+ const warnings = session.warnings;
2193
+ if (!Array.isArray(warnings)) return [];
2194
+ return warnings.filter((warning) => typeof warning === "string" && warning.length > 0).map((warning) => `Warning: ${warning}`);
2195
+ }
2091
2196
  function formatBranchStateHint(branch) {
2092
2197
  const target = branch ?? "<branch>";
2093
2198
  const commands = [
@@ -2138,6 +2243,7 @@ function renderSessionRunStatus(session) {
2138
2243
  }
2139
2244
  const childSessions = formatChildSessionsLine(session);
2140
2245
  if (childSessions) lines.push(childSessions);
2246
+ lines.push(...formatSessionWarningLines(session));
2141
2247
  lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
2142
2248
  const error = responseString(session, "error");
2143
2249
  if (error) lines.push(`Error: ${error}`);
@@ -2764,6 +2870,23 @@ async function executeR5dctlCommand(client, json, args) {
2764
2870
  write(result, renderWorkspaceSync(result));
2765
2871
  return;
2766
2872
  }
2873
+ if (first === "workspace" && second === "recover-hydration") {
2874
+ let worker;
2875
+ let transactionId;
2876
+ let confirmed = false;
2877
+ for (let index = 2; index < args.length; index += 1) {
2878
+ const arg = args[index];
2879
+ if (arg === "--worker") worker = requireValue(args[++index], "Missing worker label");
2880
+ else if (arg === "--transaction") transactionId = requireValue(args[++index], "Missing transaction id");
2881
+ else if (arg === "--confirm") confirmed = true;
2882
+ else throw new Error(`Unexpected recovery argument: ${arg}`);
2883
+ }
2884
+ if (!worker || !transactionId || !confirmed)
2885
+ throw new Error("workspace recover-hydration requires --worker, --transaction and --confirm; inspect workspace status first");
2886
+ const result = await client.workspace.recoverHydration({ worker, transactionId, confirmed: true });
2887
+ write(result, renderWorkspaceSync(result));
2888
+ return;
2889
+ }
2767
2890
  if (first === "workspace" && second === "reset") {
2768
2891
  const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
2769
2892
  const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
@@ -2794,7 +2917,7 @@ async function executeR5dctlCommand(client, json, args) {
2794
2917
  }
2795
2918
  if (first === "session" && second === "claim") {
2796
2919
  const claim = parseSessionClaimArgs(args.slice(2), {}, {});
2797
- const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
2920
+ const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
2798
2921
  const nowMs = Date.now();
2799
2922
  if (claim.action === "acquire") {
2800
2923
  const result2 = await client.claims.acquire({
@@ -2803,23 +2926,34 @@ async function executeR5dctlCommand(client, json, args) {
2803
2926
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
2804
2927
  ...claim.note !== void 0 ? { note: claim.note } : {}
2805
2928
  });
2929
+ if (!claimScopeMatches(claim.scope, result2.claim)) {
2930
+ if (result2.outcome === "acquired" && result2.claim.heldByActor) {
2931
+ await client.claims.release({
2932
+ topic: claim.topic,
2933
+ ...claim.sessionId ? { sessionId: claim.sessionId } : {},
2934
+ generation: result2.claim.generation
2935
+ }).catch(() => void 0);
2936
+ }
2937
+ throw new R5dctlClaimScopeMismatchError(claim.scope, result2.claim);
2938
+ }
2806
2939
  write(result2, renderSessionClaimResponse(result2, nowMs));
2807
2940
  if (result2.outcome === "held_by_other") {
2808
- throw new Error(
2809
- `Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} until ${result2.claim.expiresAt}; not acquired`
2810
- );
2941
+ const until = result2.claim.expiresAt === null ? "while it runs (native session)" : `until ${result2.claim.expiresAt}`;
2942
+ throw new Error(`Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} ${until}; not acquired`);
2811
2943
  }
2812
2944
  return;
2813
2945
  }
2814
2946
  if (claim.action === "status") {
2815
2947
  const list = await client.claims.list({ ...scope, topic: claim.topic });
2948
+ assertClaimScopeEcho(claim.scope, list);
2816
2949
  write(list, renderSessionClaimList(list, nowMs));
2817
2950
  return;
2818
2951
  }
2819
2952
  let generation = claim.generation;
2820
- if (generation === void 0) {
2953
+ if (generation === void 0 || claim.scope) {
2821
2954
  const list = await client.claims.list({ ...scope, topic: claim.topic });
2822
- const current = list.claims[0];
2955
+ assertClaimScopeEcho(claim.scope, list);
2956
+ const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
2823
2957
  if (!current) {
2824
2958
  write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
2825
2959
  `);
@@ -2833,7 +2967,7 @@ async function executeR5dctlCommand(client, json, args) {
2833
2967
  );
2834
2968
  throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
2835
2969
  }
2836
- generation = current.generation;
2970
+ generation ??= current.generation;
2837
2971
  }
2838
2972
  const result = claim.action === "renew" ? await client.claims.renew({
2839
2973
  topic: claim.topic,
@@ -2841,16 +2975,25 @@ async function executeR5dctlCommand(client, json, args) {
2841
2975
  generation,
2842
2976
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
2843
2977
  }) : await client.claims.release({ topic: claim.topic, ...scope, generation });
2978
+ assertClaimScopeEcho(claim.scope, result.claim);
2844
2979
  write(result, renderSessionClaimResponse(result, nowMs));
2845
2980
  const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
2846
2981
  if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
2847
2982
  return;
2848
2983
  }
2849
2984
  if (first === "session" && second === "claims") {
2850
- const list = await client.claims.list(parseSessionClaimsListArgs(args.slice(2), {}, {}));
2985
+ const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
2986
+ const list = await client.claims.list(selection);
2987
+ assertClaimScopeEcho(selection.scope, list);
2851
2988
  write(list, renderSessionClaimList(list));
2852
2989
  return;
2853
2990
  }
2991
+ if (first === "checkout" && second === "path") {
2992
+ const selection = parseCheckoutPathArgs(args.slice(2), {}, {});
2993
+ const result = await client.checkouts.path(selection);
2994
+ write(result, renderCheckoutPath(result));
2995
+ return;
2996
+ }
2854
2997
  if (first === "session" && second === "process-log") {
2855
2998
  const selection = parseSessionProcessLogArgs(args.slice(2));
2856
2999
  const log = await client.processes.log(selection.runId, {
@@ -2950,7 +3093,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
2950
3093
  throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
2951
3094
  }
2952
3095
  if (json) write({ results: deletion.results }, "");
2953
- if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3096
+ if (deletion.results.some((result) => !result.deleted && !result.accepted))
3097
+ throw new Error(branchDeletionsFailureMessage(deletion.results));
2954
3098
  return;
2955
3099
  }
2956
3100
  if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
@@ -3547,24 +3691,43 @@ function resolveCommandExecution(options, rest) {
3547
3691
  ...parseShellArgs(commandArgs)
3548
3692
  };
3549
3693
  }
3694
+ if (command === "checkout") {
3695
+ if (commandArgs[0] !== "path") throw new Error("Unknown checkout command");
3696
+ const parsed = parseCheckoutPathArgs(commandArgs.slice(1), options);
3697
+ return {
3698
+ kind: "plugin",
3699
+ pluginArgs: [
3700
+ "checkout",
3701
+ "path",
3702
+ `--project=${parsed.projectId}`,
3703
+ `--branch=${parsed.branchName}`,
3704
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3705
+ ]
3706
+ };
3707
+ }
3550
3708
  if (command === "session") {
3551
3709
  if (commandArgs[0] === "claim" || commandArgs[0] === "claims") {
3552
- if (options.project) throw new Error("--project/-p is not supported for `session claim`; the acting session selects the project branch");
3553
- if (options.branch) throw new Error("--branch/-b is not supported for `session claim`; the acting session selects the branch");
3554
3710
  const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
3555
- const withoutSessionValue = [];
3711
+ const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
3712
+ const withoutNormalizedValues = [];
3556
3713
  for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
3557
3714
  const arg = commandArgs.slice(1)[index];
3558
- if (arg === "--session") {
3715
+ if (normalizedFlags.has(arg)) {
3559
3716
  index += 1;
3560
3717
  continue;
3561
3718
  }
3562
- if (arg.startsWith("--session=")) continue;
3563
- withoutSessionValue.push(arg);
3719
+ if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
3720
+ withoutNormalizedValues.push(arg);
3564
3721
  }
3565
3722
  return {
3566
3723
  kind: "plugin",
3567
- pluginArgs: ["session", commandArgs[0], ...withoutSessionValue, ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []]
3724
+ pluginArgs: [
3725
+ "session",
3726
+ commandArgs[0],
3727
+ ...withoutNormalizedValues,
3728
+ ...parsed.scope ? [`--project=${parsed.scope.projectId}`, `--branch=${parsed.scope.branchName}`] : [],
3729
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3730
+ ]
3568
3731
  };
3569
3732
  }
3570
3733
  if (commandArgs[0] === "process-log") {
@@ -3737,10 +3900,12 @@ async function main(argv = process.argv.slice(2)) {
3737
3900
  export {
3738
3901
  BRANCH_DELETION_POLL_INTERVAL_MS,
3739
3902
  BRANCH_DELETION_WAIT_TIMEOUT_MS,
3903
+ R5dctlClaimScopeMismatchError,
3740
3904
  advanceTransientDevicePollBackoff,
3741
3905
  branchDeletionConfirmationMessage,
3742
3906
  branchDeletionsConfirmationMessage,
3743
3907
  branchDeletionsFailureMessage,
3908
+ claimScopeMatches,
3744
3909
  createBranchThroughPlatform,
3745
3910
  deleteBranchThroughPlatform,
3746
3911
  deleteBranchesThroughPlatform,
@@ -3760,6 +3925,7 @@ export {
3760
3925
  parseAnswerQuestionsCommandArgs,
3761
3926
  parseBranchCreateArgs,
3762
3927
  parseBranchDeleteArgs,
3928
+ parseCheckoutPathArgs,
3763
3929
  parseClaimDuration,
3764
3930
  parseConversationForkArgs,
3765
3931
  parseConversationInspectWorkArgs,
@@ -3785,6 +3951,7 @@ export {
3785
3951
  renderBranchDeletionStatus,
3786
3952
  renderBranchList,
3787
3953
  renderBranchOperation,
3954
+ renderCheckoutPath,
3788
3955
  renderConversationForkResponse,
3789
3956
  renderConversationHeadResponse,
3790
3957
  renderConversationNodeResponse,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.132",
3
+ "version": "0.0.133",
4
4
  "type": "module"
5
5
  }
@@ -1,4 +1,4 @@
1
- import { R5dctlApiError, R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlWorkerCapacityReport, type R5dctlBranch, type R5dctlBranchDeletionStatus, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionSummary, type R5dctlSessionClaim, type R5dctlSessionClaimListResponse, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlPromptDelivery, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
1
+ import { R5dctlApiError, R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlWorkerCapacityReport, type R5dctlBranch, type R5dctlBranchDeletionStatus, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionSummary, type R5dctlCheckoutPathResponse, type R5dctlSessionClaim, type R5dctlSessionClaimListResponse, type R5dctlSessionClaimScope, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlPromptDelivery, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
2
2
  import { type AccountCommand } from "./account";
3
3
  export type GlobalOptions = {
4
4
  baseUrl?: string;
@@ -209,23 +209,54 @@ export type R5dctlSessionClaimArgs = {
209
209
  topic: string;
210
210
  /** The acting session; omitted lets a worker credential act as its own verified session. */
211
211
  sessionId?: string;
212
+ /** A sibling checkout to claim on instead of the acting session's own branch. */
213
+ scope?: R5dctlSessionClaimScope;
212
214
  ttlSeconds?: number;
213
215
  note?: string;
214
216
  /** Claim generation observed by the caller; renew and release are fenced to it. */
215
217
  generation?: number;
216
218
  };
219
+ type ClaimScopeOptions = Pick<GlobalOptions, "session" | "project" | "branch">;
217
220
  /** `90s`, `30m`, `2h`; the server clamps to between one minute and one day. */
218
221
  export declare function parseClaimDuration(value: string): number;
219
- export declare function parseSessionClaimArgs(args: string[], options?: Pick<GlobalOptions, "session">, env?: Record<string, string | undefined>): R5dctlSessionClaimArgs;
220
- export declare function parseSessionClaimsListArgs(args: string[], options?: Pick<GlobalOptions, "session">, env?: Record<string, string | undefined>): {
222
+ export declare function parseSessionClaimArgs(args: string[], options?: ClaimScopeOptions, env?: Record<string, string | undefined>): R5dctlSessionClaimArgs;
223
+ export declare function parseSessionClaimsListArgs(args: string[], options?: ClaimScopeOptions, env?: Record<string, string | undefined>): {
221
224
  sessionId?: string;
225
+ scope?: R5dctlSessionClaimScope;
222
226
  };
227
+ export type R5dctlCheckoutPathArgs = {
228
+ projectId: string;
229
+ branchName: string;
230
+ sessionId?: string;
231
+ };
232
+ /** `checkout path` names the checkout with -p/--project and --branch/-b (both required) and acts as the selected session. */
233
+ export declare function parseCheckoutPathArgs(args: string[], options?: ClaimScopeOptions, env?: Record<string, string | undefined>): R5dctlCheckoutPathArgs;
234
+ /**
235
+ * A server that predates claim scopes strips the unknown `scope` field and
236
+ * answers for the acting session's own branch. Every scoped command must see
237
+ * its scope echoed back, so a claim can never silently land elsewhere.
238
+ */
239
+ export declare class R5dctlClaimScopeMismatchError extends Error {
240
+ constructor(requested: R5dctlSessionClaimScope, answered: {
241
+ projectId: string;
242
+ projectPath?: string;
243
+ branchName: string;
244
+ });
245
+ }
246
+ export declare function claimScopeMatches(requested: R5dctlSessionClaimScope | undefined, answered: {
247
+ projectId: string;
248
+ projectPath?: string;
249
+ branchName: string;
250
+ } | null | undefined): boolean;
223
251
  export declare function renderSessionClaimResponse(response: {
224
252
  outcome: string;
225
253
  actorSessionId: string;
226
254
  claim: R5dctlSessionClaim | null;
255
+ fenceExtended?: boolean;
256
+ fenceDetail?: string;
227
257
  }, nowMs?: number): string;
228
258
  export declare function renderSessionClaimList(response: R5dctlSessionClaimListResponse, nowMs?: number): string;
259
+ export declare function renderCheckoutPath(response: R5dctlCheckoutPathResponse): string;
229
260
  export declare function parseGlobalArgs(argv: string[]): {
230
261
  options: GlobalOptions;
231
262
  rest: string[];
@@ -318,6 +349,8 @@ export type R5dctlCommandResponse = {
318
349
  worker?: string;
319
350
  /** The binding a `session set-worker` replaced; null when the session had none. */
320
351
  previousWorker?: string | null;
352
+ /** Advisory notices about the run's branch (a foreign `checkout` claim, for example); older servers send none. */
353
+ warnings?: unknown;
321
354
  status?: string;
322
355
  runStatus?: unknown;
323
356
  /** Worker queue state while a new worktree provisions; older servers and workers omit it. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.132",
3
+ "version": "0.0.133",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/cli.cjs",
6
6
  "module": "./dist/mjs/cli.mjs",
@@ -26,7 +26,7 @@
26
26
  "r5dctl": "dist/cjs/main.cjs"
27
27
  },
28
28
  "dependencies": {
29
- "@ricsam/r5d-api": "^0.0.132",
29
+ "@ricsam/r5d-api": "^0.0.133",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"