@ricsam/r5dctl 0.0.132 → 0.0.134

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",
@@ -503,10 +523,14 @@ function renderCliOnlyCommandHelp(entry) {
503
523
  `;
504
524
  }
505
525
  function renderSharedCommandHelp(pathSegments) {
506
- const entry = SHARED_HELP_ENTRIES.map((candidate) => ({
507
- candidate,
508
- usageSegments: candidate.usage.split(" ").map((segment) => segment.replace(/^\[/, "").replace(/\]$/, "")).filter((segment) => !segment.startsWith("-") && !segment.includes("<") && !segment.startsWith("["))
509
- })).filter(
526
+ const entry = SHARED_HELP_ENTRIES.map((candidate) => {
527
+ const usageSegments = [];
528
+ for (const segment of candidate.usage.split(/\s+/)) {
529
+ if (/^[a-z][a-z-]*$/.test(segment)) usageSegments.push(segment);
530
+ else if (usageSegments.length > 0) break;
531
+ }
532
+ return { candidate, usageSegments };
533
+ }).filter(
510
534
  ({ usageSegments }) => usageSegments.length <= pathSegments.length && usageSegments.every((segment, index) => segment === pathSegments[index])
511
535
  ).sort((left, right) => right.usageSegments.length - left.usageSegments.length)[0]?.candidate;
512
536
  if (!entry) {
@@ -955,13 +979,25 @@ function parseSessionProcessLogArgs(args) {
955
979
  const grep = parsed.values.get("--grep");
956
980
  return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
957
981
  }
982
+ function resolveClaimScope(explicitProject, explicitBranch, options) {
983
+ const project = explicitProject?.trim() || options.project?.trim() || void 0;
984
+ const branch = explicitBranch?.trim() || options.branch?.trim() || void 0;
985
+ if (project === void 0 && branch === void 0) return void 0;
986
+ if (project === void 0 || branch === void 0) {
987
+ throw new Error(
988
+ "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"
989
+ );
990
+ }
991
+ return { projectId: project, branchName: branch };
992
+ }
958
993
  const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
959
994
  function parseClaimDuration(value) {
960
995
  const match = CLAIM_DURATION_PATTERN.exec(value.trim());
961
996
  if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
962
997
  const amount = Number(match[1]);
963
998
  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");
999
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
1000
+ throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
965
1001
  return seconds;
966
1002
  }
967
1003
  function resolveClaimSessionId(explicit, options, env) {
@@ -970,7 +1006,7 @@ function resolveClaimSessionId(explicit, options, env) {
970
1006
  function parseSessionClaimArgs(args, options = {}, env = process.env) {
971
1007
  const parsed = parseCommandOperands(
972
1008
  args,
973
- /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
1009
+ /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
974
1010
  /* @__PURE__ */ new Map([
975
1011
  ["--status", "--status"],
976
1012
  ["--renew", "--renew"],
@@ -983,7 +1019,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
983
1019
  if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
984
1020
  const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
985
1021
  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");
1022
+ if (ttl !== void 0 && action !== "acquire" && action !== "renew")
1023
+ throw new Error("--ttl is only supported when acquiring or renewing a claim");
987
1024
  const note = parsed.values.get("--note");
988
1025
  if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
989
1026
  const generationValue = parsed.values.get("--generation");
@@ -991,22 +1028,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
991
1028
  throw new Error("--generation is only supported with --renew or --release");
992
1029
  }
993
1030
  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");
1031
+ if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
1032
+ throw new Error("--generation must be a positive integer");
995
1033
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
1034
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
996
1035
  return {
997
1036
  action,
998
1037
  topic,
999
1038
  ...sessionId ? { sessionId } : {},
1039
+ ...scope ? { scope } : {},
1000
1040
  ...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
1001
1041
  ...note !== void 0 ? { note } : {},
1002
1042
  ...generation !== void 0 ? { generation } : {}
1003
1043
  };
1004
1044
  }
1005
1045
  function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
1006
- const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session"]), /* @__PURE__ */ new Map(), "session claims");
1046
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "session claims");
1007
1047
  if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
1008
1048
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
1009
- return sessionId ? { sessionId } : {};
1049
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
1050
+ return { ...sessionId ? { sessionId } : {}, ...scope ? { scope } : {} };
1051
+ }
1052
+ function parseCheckoutPathArgs(args, options = {}, env = process.env) {
1053
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "checkout path");
1054
+ if (parsed.positionals.length > 0) throw new Error(`Unexpected checkout path argument: ${parsed.positionals[0]}`);
1055
+ const projectId = parsed.values.get("--project")?.trim() || options.project?.trim();
1056
+ const branchName = parsed.values.get("--branch")?.trim() || options.branch?.trim();
1057
+ if (!projectId) throw new Error("--project/-p is required for `checkout path`");
1058
+ if (!branchName) throw new Error("--branch/-b is required for `checkout path`");
1059
+ const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
1060
+ return { projectId, branchName, ...sessionId ? { sessionId } : {} };
1061
+ }
1062
+ class R5dctlClaimScopeMismatchError extends Error {
1063
+ constructor(requested, answered) {
1064
+ super(
1065
+ `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`
1066
+ );
1067
+ this.name = "R5dctlClaimScopeMismatchError";
1068
+ }
1069
+ }
1070
+ function claimScopeMatches(requested, answered) {
1071
+ if (!requested || !answered) return true;
1072
+ const projectMatches = answered.projectId === requested.projectId || answered.projectPath === requested.projectId;
1073
+ return projectMatches && answered.branchName === requested.branchName;
1074
+ }
1075
+ function assertClaimScopeEcho(requested, answered) {
1076
+ if (!claimScopeMatches(requested, answered)) throw new R5dctlClaimScopeMismatchError(requested, answered);
1010
1077
  }
1011
1078
  const CLAIM_OUTCOME_TEXT = {
1012
1079
  acquired: "acquired",
@@ -1019,35 +1086,55 @@ const CLAIM_OUTCOME_TEXT = {
1019
1086
  released: "released"
1020
1087
  };
1021
1088
  function formatClaimExpiry(expiresAt, nowMs) {
1089
+ if (expiresAt === null) return "while the holder runs (implicit)";
1022
1090
  const expiresAtMs = Date.parse(expiresAt);
1023
1091
  if (!Number.isFinite(expiresAtMs)) return expiresAt;
1024
1092
  return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
1025
1093
  }
1094
+ function describeClaimScope(scope) {
1095
+ return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
1096
+ }
1026
1097
  function describeClaimHolder(claim) {
1027
1098
  const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
1028
- return `${claim.holderSessionId}${name}${claim.heldByActor ? " (this session)" : ""}`;
1099
+ const self = claim.heldByActor ? " (this session)" : "";
1100
+ let origin = "";
1101
+ if (claim.holderBranchName !== void 0 && claim.holderBranchName !== null) {
1102
+ const native = claim.holderProjectId === claim.projectId && claim.holderBranchName === claim.branchName;
1103
+ if (native) origin = claim.implicit ? " (native, running)" : " (native)";
1104
+ else if (claim.holderProjectId === claim.projectId) origin = ` (foreign: native to branch ${claim.holderBranchName})`;
1105
+ else origin = ` (foreign: native to ${claim.holderProjectId ?? "unknown"}#${claim.holderBranchName})`;
1106
+ }
1107
+ return `${claim.holderSessionId}${name}${self}${origin}`;
1108
+ }
1109
+ function describeClaimFence(response) {
1110
+ if (response.fenceExtended === void 0) return void 0;
1111
+ if (response.fenceExtended)
1112
+ return "Fence: commands this session starts while holding the claim also fence the claimed checkout on the bound worker";
1113
+ 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
1114
  }
1030
1115
  function renderSessionClaimResponse(response, nowMs = Date.now()) {
1031
1116
  const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
1032
1117
  if (response.claim) {
1033
1118
  lines.push(
1034
1119
  `Claim: ${response.claim.topic}`,
1035
- `Branch: ${response.claim.branchName}`,
1120
+ `Scope: ${describeClaimScope(response.claim)}`,
1036
1121
  `Holder: ${describeClaimHolder(response.claim)}`,
1037
1122
  `Generation: ${response.claim.generation}`,
1038
1123
  `Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
1039
1124
  );
1040
1125
  if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
1041
1126
  }
1127
+ const fence = describeClaimFence(response);
1128
+ if (fence) lines.push(fence);
1042
1129
  return `${lines.join("\n")}
1043
1130
  `;
1044
1131
  }
1045
1132
  function renderSessionClaimList(response, nowMs = Date.now()) {
1046
- const header = `Claims on ${response.branchName} (acting session ${response.actorSessionId})`;
1133
+ const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
1047
1134
  if (response.claims.length === 0) return `${header}: none
1048
1135
  `;
1049
1136
  const rows = response.claims.map((claim) => {
1050
- const expiry = claim.expired ? "expired" : "expires";
1137
+ const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
1051
1138
  const note = claim.note ? ` note: ${claim.note}` : "";
1052
1139
  return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
1053
1140
  });
@@ -1055,6 +1142,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
1055
1142
  ${rows.join("\n")}
1056
1143
  `;
1057
1144
  }
1145
+ function renderCheckoutPath(response) {
1146
+ return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
1147
+ "\n"
1148
+ );
1149
+ }
1058
1150
  function assertAuthLoginArgs(args) {
1059
1151
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
1060
1152
  const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
@@ -1896,6 +1988,12 @@ function renderWorkspaceStatus(status) {
1896
1988
  `Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
1897
1989
  ...status.workers.flatMap((worker) => [
1898
1990
  `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
1991
+ ...worker.hydrationRecovery ? [
1992
+ ` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
1993
+ ` Evidence: ${worker.hydrationRecovery.transactionPath}`,
1994
+ ...(worker.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`),
1995
+ ` After stopping active processes: r5dctl workspace recover-hydration --worker ${JSON.stringify(worker.workerLabel)} --transaction ${worker.hydrationRecovery.transactionId} --confirm`
1996
+ ] : [],
1899
1997
  ...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
1900
1998
  ...renderWorkerCapacity(worker.workerLabel, worker.capacity)
1901
1999
  ])
@@ -1903,6 +2001,15 @@ function renderWorkspaceStatus(status) {
1903
2001
  if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
1904
2002
  lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
1905
2003
  }
2004
+ if (status.latestSync?.hydrationRecovery && !status.workers.some((worker) => worker.workerLabel === status.latestSync.workerLabel)) {
2005
+ lines.push(
2006
+ `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.`,
2007
+ ...(status.latestSync.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`)
2008
+ );
2009
+ }
2010
+ for (const [mountId, skip] of Object.entries(status.latestSync?.mountSkipReasons ?? {})) {
2011
+ for (const retainedPath of skip.retainedPaths ?? []) lines.push(`Retained writer entry for ${mountId}: ${retainedPath}`);
2012
+ }
1906
2013
  return `${lines.join("\n")}
1907
2014
  `;
1908
2015
  }
@@ -2189,11 +2296,17 @@ function renderSessionRunStart(session) {
2189
2296
  lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
2190
2297
  const sessionUrl = responseString(session, "sessionUrl");
2191
2298
  if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
2299
+ lines.push(...formatSessionWarningLines(session));
2192
2300
  const sessionId = responseString(session, "sessionId");
2193
2301
  if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
2194
2302
  return `${lines.join("\n")}
2195
2303
  `;
2196
2304
  }
2305
+ function formatSessionWarningLines(session) {
2306
+ const warnings = session.warnings;
2307
+ if (!Array.isArray(warnings)) return [];
2308
+ return warnings.filter((warning) => typeof warning === "string" && warning.length > 0).map((warning) => `Warning: ${warning}`);
2309
+ }
2197
2310
  function formatBranchStateHint(branch) {
2198
2311
  const target = branch ?? "<branch>";
2199
2312
  const commands = [
@@ -2244,6 +2357,7 @@ function renderSessionRunStatus(session) {
2244
2357
  }
2245
2358
  const childSessions = formatChildSessionsLine(session);
2246
2359
  if (childSessions) lines.push(childSessions);
2360
+ lines.push(...formatSessionWarningLines(session));
2247
2361
  lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
2248
2362
  const error = responseString(session, "error");
2249
2363
  if (error) lines.push(`Error: ${error}`);
@@ -2870,6 +2984,23 @@ async function executeR5dctlCommand(client, json, args) {
2870
2984
  write(result, renderWorkspaceSync(result));
2871
2985
  return;
2872
2986
  }
2987
+ if (first === "workspace" && second === "recover-hydration") {
2988
+ let worker;
2989
+ let transactionId;
2990
+ let confirmed = false;
2991
+ for (let index = 2; index < args.length; index += 1) {
2992
+ const arg = args[index];
2993
+ if (arg === "--worker") worker = requireValue(args[++index], "Missing worker label");
2994
+ else if (arg === "--transaction") transactionId = requireValue(args[++index], "Missing transaction id");
2995
+ else if (arg === "--confirm") confirmed = true;
2996
+ else throw new Error(`Unexpected recovery argument: ${arg}`);
2997
+ }
2998
+ if (!worker || !transactionId || !confirmed)
2999
+ throw new Error("workspace recover-hydration requires --worker, --transaction and --confirm; inspect workspace status first");
3000
+ const result = await client.workspace.recoverHydration({ worker, transactionId, confirmed: true });
3001
+ write(result, renderWorkspaceSync(result));
3002
+ return;
3003
+ }
2873
3004
  if (first === "workspace" && second === "reset") {
2874
3005
  const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
2875
3006
  const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
@@ -2900,7 +3031,7 @@ async function executeR5dctlCommand(client, json, args) {
2900
3031
  }
2901
3032
  if (first === "session" && second === "claim") {
2902
3033
  const claim = parseSessionClaimArgs(args.slice(2), {}, {});
2903
- const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
3034
+ const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
2904
3035
  const nowMs = Date.now();
2905
3036
  if (claim.action === "acquire") {
2906
3037
  const result2 = await client.claims.acquire({
@@ -2909,23 +3040,34 @@ async function executeR5dctlCommand(client, json, args) {
2909
3040
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
2910
3041
  ...claim.note !== void 0 ? { note: claim.note } : {}
2911
3042
  });
3043
+ if (!claimScopeMatches(claim.scope, result2.claim)) {
3044
+ if (result2.outcome === "acquired" && result2.claim.heldByActor) {
3045
+ await client.claims.release({
3046
+ topic: claim.topic,
3047
+ ...claim.sessionId ? { sessionId: claim.sessionId } : {},
3048
+ generation: result2.claim.generation
3049
+ }).catch(() => void 0);
3050
+ }
3051
+ throw new R5dctlClaimScopeMismatchError(claim.scope, result2.claim);
3052
+ }
2912
3053
  write(result2, renderSessionClaimResponse(result2, nowMs));
2913
3054
  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
- );
3055
+ const until = result2.claim.expiresAt === null ? "while it runs (native session)" : `until ${result2.claim.expiresAt}`;
3056
+ throw new Error(`Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} ${until}; not acquired`);
2917
3057
  }
2918
3058
  return;
2919
3059
  }
2920
3060
  if (claim.action === "status") {
2921
3061
  const list = await client.claims.list({ ...scope, topic: claim.topic });
3062
+ assertClaimScopeEcho(claim.scope, list);
2922
3063
  write(list, renderSessionClaimList(list, nowMs));
2923
3064
  return;
2924
3065
  }
2925
3066
  let generation = claim.generation;
2926
- if (generation === void 0) {
3067
+ if (generation === void 0 || claim.scope) {
2927
3068
  const list = await client.claims.list({ ...scope, topic: claim.topic });
2928
- const current = list.claims[0];
3069
+ assertClaimScopeEcho(claim.scope, list);
3070
+ const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
2929
3071
  if (!current) {
2930
3072
  write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
2931
3073
  `);
@@ -2939,7 +3081,7 @@ async function executeR5dctlCommand(client, json, args) {
2939
3081
  );
2940
3082
  throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
2941
3083
  }
2942
- generation = current.generation;
3084
+ generation ??= current.generation;
2943
3085
  }
2944
3086
  const result = claim.action === "renew" ? await client.claims.renew({
2945
3087
  topic: claim.topic,
@@ -2947,16 +3089,25 @@ async function executeR5dctlCommand(client, json, args) {
2947
3089
  generation,
2948
3090
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
2949
3091
  }) : await client.claims.release({ topic: claim.topic, ...scope, generation });
3092
+ assertClaimScopeEcho(claim.scope, result.claim);
2950
3093
  write(result, renderSessionClaimResponse(result, nowMs));
2951
3094
  const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
2952
3095
  if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
2953
3096
  return;
2954
3097
  }
2955
3098
  if (first === "session" && second === "claims") {
2956
- const list = await client.claims.list(parseSessionClaimsListArgs(args.slice(2), {}, {}));
3099
+ const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
3100
+ const list = await client.claims.list(selection);
3101
+ assertClaimScopeEcho(selection.scope, list);
2957
3102
  write(list, renderSessionClaimList(list));
2958
3103
  return;
2959
3104
  }
3105
+ if (first === "checkout" && second === "path") {
3106
+ const selection = parseCheckoutPathArgs(args.slice(2), {}, {});
3107
+ const result = await client.checkouts.path(selection);
3108
+ write(result, renderCheckoutPath(result));
3109
+ return;
3110
+ }
2960
3111
  if (first === "session" && second === "process-log") {
2961
3112
  const selection = parseSessionProcessLogArgs(args.slice(2));
2962
3113
  const log = await client.processes.log(selection.runId, {
@@ -3056,7 +3207,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
3056
3207
  throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
3057
3208
  }
3058
3209
  if (json) write({ results: deletion.results }, "");
3059
- if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3210
+ if (deletion.results.some((result) => !result.deleted && !result.accepted))
3211
+ throw new Error(branchDeletionsFailureMessage(deletion.results));
3060
3212
  return;
3061
3213
  }
3062
3214
  if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
@@ -3653,24 +3805,43 @@ function resolveCommandExecution(options, rest) {
3653
3805
  ...parseShellArgs(commandArgs)
3654
3806
  };
3655
3807
  }
3808
+ if (command === "checkout") {
3809
+ if (commandArgs[0] !== "path") throw new Error("Unknown checkout command");
3810
+ const parsed = parseCheckoutPathArgs(commandArgs.slice(1), options);
3811
+ return {
3812
+ kind: "plugin",
3813
+ pluginArgs: [
3814
+ "checkout",
3815
+ "path",
3816
+ `--project=${parsed.projectId}`,
3817
+ `--branch=${parsed.branchName}`,
3818
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3819
+ ]
3820
+ };
3821
+ }
3656
3822
  if (command === "session") {
3657
3823
  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
3824
  const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
3661
- const withoutSessionValue = [];
3825
+ const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
3826
+ const withoutNormalizedValues = [];
3662
3827
  for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
3663
3828
  const arg = commandArgs.slice(1)[index];
3664
- if (arg === "--session") {
3829
+ if (normalizedFlags.has(arg)) {
3665
3830
  index += 1;
3666
3831
  continue;
3667
3832
  }
3668
- if (arg.startsWith("--session=")) continue;
3669
- withoutSessionValue.push(arg);
3833
+ if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
3834
+ withoutNormalizedValues.push(arg);
3670
3835
  }
3671
3836
  return {
3672
3837
  kind: "plugin",
3673
- pluginArgs: ["session", commandArgs[0], ...withoutSessionValue, ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []]
3838
+ pluginArgs: [
3839
+ "session",
3840
+ commandArgs[0],
3841
+ ...withoutNormalizedValues,
3842
+ ...parsed.scope ? [`--project=${parsed.scope.projectId}`, `--branch=${parsed.scope.branchName}`] : [],
3843
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3844
+ ]
3674
3845
  };
3675
3846
  }
3676
3847
  if (commandArgs[0] === "process-log") {
@@ -3844,10 +4015,12 @@ async function main(argv = process.argv.slice(2)) {
3844
4015
  0 && (module.exports = {
3845
4016
  BRANCH_DELETION_POLL_INTERVAL_MS,
3846
4017
  BRANCH_DELETION_WAIT_TIMEOUT_MS,
4018
+ R5dctlClaimScopeMismatchError,
3847
4019
  advanceTransientDevicePollBackoff,
3848
4020
  branchDeletionConfirmationMessage,
3849
4021
  branchDeletionsConfirmationMessage,
3850
4022
  branchDeletionsFailureMessage,
4023
+ claimScopeMatches,
3851
4024
  createBranchThroughPlatform,
3852
4025
  deleteBranchThroughPlatform,
3853
4026
  deleteBranchesThroughPlatform,
@@ -3867,6 +4040,7 @@ async function main(argv = process.argv.slice(2)) {
3867
4040
  parseAnswerQuestionsCommandArgs,
3868
4041
  parseBranchCreateArgs,
3869
4042
  parseBranchDeleteArgs,
4043
+ parseCheckoutPathArgs,
3870
4044
  parseClaimDuration,
3871
4045
  parseConversationForkArgs,
3872
4046
  parseConversationInspectWorkArgs,
@@ -3892,6 +4066,7 @@ async function main(argv = process.argv.slice(2)) {
3892
4066
  renderBranchDeletionStatus,
3893
4067
  renderBranchList,
3894
4068
  renderBranchOperation,
4069
+ renderCheckoutPath,
3895
4070
  renderConversationForkResponse,
3896
4071
  renderConversationHeadResponse,
3897
4072
  renderConversationNodeResponse,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.132",
3
+ "version": "0.0.134",
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",
@@ -397,10 +413,14 @@ function renderCliOnlyCommandHelp(entry) {
397
413
  `;
398
414
  }
399
415
  function renderSharedCommandHelp(pathSegments) {
400
- const entry = SHARED_HELP_ENTRIES.map((candidate) => ({
401
- candidate,
402
- usageSegments: candidate.usage.split(" ").map((segment) => segment.replace(/^\[/, "").replace(/\]$/, "")).filter((segment) => !segment.startsWith("-") && !segment.includes("<") && !segment.startsWith("["))
403
- })).filter(
416
+ const entry = SHARED_HELP_ENTRIES.map((candidate) => {
417
+ const usageSegments = [];
418
+ for (const segment of candidate.usage.split(/\s+/)) {
419
+ if (/^[a-z][a-z-]*$/.test(segment)) usageSegments.push(segment);
420
+ else if (usageSegments.length > 0) break;
421
+ }
422
+ return { candidate, usageSegments };
423
+ }).filter(
404
424
  ({ usageSegments }) => usageSegments.length <= pathSegments.length && usageSegments.every((segment, index) => segment === pathSegments[index])
405
425
  ).sort((left, right) => right.usageSegments.length - left.usageSegments.length)[0]?.candidate;
406
426
  if (!entry) {
@@ -849,13 +869,25 @@ function parseSessionProcessLogArgs(args) {
849
869
  const grep = parsed.values.get("--grep");
850
870
  return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
851
871
  }
872
+ function resolveClaimScope(explicitProject, explicitBranch, options) {
873
+ const project = explicitProject?.trim() || options.project?.trim() || void 0;
874
+ const branch = explicitBranch?.trim() || options.branch?.trim() || void 0;
875
+ if (project === void 0 && branch === void 0) return void 0;
876
+ if (project === void 0 || branch === void 0) {
877
+ throw new Error(
878
+ "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"
879
+ );
880
+ }
881
+ return { projectId: project, branchName: branch };
882
+ }
852
883
  const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
853
884
  function parseClaimDuration(value) {
854
885
  const match = CLAIM_DURATION_PATTERN.exec(value.trim());
855
886
  if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
856
887
  const amount = Number(match[1]);
857
888
  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");
889
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
890
+ throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
859
891
  return seconds;
860
892
  }
861
893
  function resolveClaimSessionId(explicit, options, env) {
@@ -864,7 +896,7 @@ function resolveClaimSessionId(explicit, options, env) {
864
896
  function parseSessionClaimArgs(args, options = {}, env = process.env) {
865
897
  const parsed = parseCommandOperands(
866
898
  args,
867
- /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
899
+ /* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
868
900
  /* @__PURE__ */ new Map([
869
901
  ["--status", "--status"],
870
902
  ["--renew", "--renew"],
@@ -877,7 +909,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
877
909
  if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
878
910
  const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
879
911
  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");
912
+ if (ttl !== void 0 && action !== "acquire" && action !== "renew")
913
+ throw new Error("--ttl is only supported when acquiring or renewing a claim");
881
914
  const note = parsed.values.get("--note");
882
915
  if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
883
916
  const generationValue = parsed.values.get("--generation");
@@ -885,22 +918,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
885
918
  throw new Error("--generation is only supported with --renew or --release");
886
919
  }
887
920
  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");
921
+ if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
922
+ throw new Error("--generation must be a positive integer");
889
923
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
924
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
890
925
  return {
891
926
  action,
892
927
  topic,
893
928
  ...sessionId ? { sessionId } : {},
929
+ ...scope ? { scope } : {},
894
930
  ...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
895
931
  ...note !== void 0 ? { note } : {},
896
932
  ...generation !== void 0 ? { generation } : {}
897
933
  };
898
934
  }
899
935
  function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
900
- const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session"]), /* @__PURE__ */ new Map(), "session claims");
936
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "session claims");
901
937
  if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
902
938
  const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
903
- return sessionId ? { sessionId } : {};
939
+ const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
940
+ return { ...sessionId ? { sessionId } : {}, ...scope ? { scope } : {} };
941
+ }
942
+ function parseCheckoutPathArgs(args, options = {}, env = process.env) {
943
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "checkout path");
944
+ if (parsed.positionals.length > 0) throw new Error(`Unexpected checkout path argument: ${parsed.positionals[0]}`);
945
+ const projectId = parsed.values.get("--project")?.trim() || options.project?.trim();
946
+ const branchName = parsed.values.get("--branch")?.trim() || options.branch?.trim();
947
+ if (!projectId) throw new Error("--project/-p is required for `checkout path`");
948
+ if (!branchName) throw new Error("--branch/-b is required for `checkout path`");
949
+ const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
950
+ return { projectId, branchName, ...sessionId ? { sessionId } : {} };
951
+ }
952
+ class R5dctlClaimScopeMismatchError extends Error {
953
+ constructor(requested, answered) {
954
+ super(
955
+ `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`
956
+ );
957
+ this.name = "R5dctlClaimScopeMismatchError";
958
+ }
959
+ }
960
+ function claimScopeMatches(requested, answered) {
961
+ if (!requested || !answered) return true;
962
+ const projectMatches = answered.projectId === requested.projectId || answered.projectPath === requested.projectId;
963
+ return projectMatches && answered.branchName === requested.branchName;
964
+ }
965
+ function assertClaimScopeEcho(requested, answered) {
966
+ if (!claimScopeMatches(requested, answered)) throw new R5dctlClaimScopeMismatchError(requested, answered);
904
967
  }
905
968
  const CLAIM_OUTCOME_TEXT = {
906
969
  acquired: "acquired",
@@ -913,35 +976,55 @@ const CLAIM_OUTCOME_TEXT = {
913
976
  released: "released"
914
977
  };
915
978
  function formatClaimExpiry(expiresAt, nowMs) {
979
+ if (expiresAt === null) return "while the holder runs (implicit)";
916
980
  const expiresAtMs = Date.parse(expiresAt);
917
981
  if (!Number.isFinite(expiresAtMs)) return expiresAt;
918
982
  return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
919
983
  }
984
+ function describeClaimScope(scope) {
985
+ return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
986
+ }
920
987
  function describeClaimHolder(claim) {
921
988
  const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
922
- return `${claim.holderSessionId}${name}${claim.heldByActor ? " (this session)" : ""}`;
989
+ const self = claim.heldByActor ? " (this session)" : "";
990
+ let origin = "";
991
+ if (claim.holderBranchName !== void 0 && claim.holderBranchName !== null) {
992
+ const native = claim.holderProjectId === claim.projectId && claim.holderBranchName === claim.branchName;
993
+ if (native) origin = claim.implicit ? " (native, running)" : " (native)";
994
+ else if (claim.holderProjectId === claim.projectId) origin = ` (foreign: native to branch ${claim.holderBranchName})`;
995
+ else origin = ` (foreign: native to ${claim.holderProjectId ?? "unknown"}#${claim.holderBranchName})`;
996
+ }
997
+ return `${claim.holderSessionId}${name}${self}${origin}`;
998
+ }
999
+ function describeClaimFence(response) {
1000
+ if (response.fenceExtended === void 0) return void 0;
1001
+ if (response.fenceExtended)
1002
+ return "Fence: commands this session starts while holding the claim also fence the claimed checkout on the bound worker";
1003
+ 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
1004
  }
924
1005
  function renderSessionClaimResponse(response, nowMs = Date.now()) {
925
1006
  const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
926
1007
  if (response.claim) {
927
1008
  lines.push(
928
1009
  `Claim: ${response.claim.topic}`,
929
- `Branch: ${response.claim.branchName}`,
1010
+ `Scope: ${describeClaimScope(response.claim)}`,
930
1011
  `Holder: ${describeClaimHolder(response.claim)}`,
931
1012
  `Generation: ${response.claim.generation}`,
932
1013
  `Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
933
1014
  );
934
1015
  if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
935
1016
  }
1017
+ const fence = describeClaimFence(response);
1018
+ if (fence) lines.push(fence);
936
1019
  return `${lines.join("\n")}
937
1020
  `;
938
1021
  }
939
1022
  function renderSessionClaimList(response, nowMs = Date.now()) {
940
- const header = `Claims on ${response.branchName} (acting session ${response.actorSessionId})`;
1023
+ const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
941
1024
  if (response.claims.length === 0) return `${header}: none
942
1025
  `;
943
1026
  const rows = response.claims.map((claim) => {
944
- const expiry = claim.expired ? "expired" : "expires";
1027
+ const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
945
1028
  const note = claim.note ? ` note: ${claim.note}` : "";
946
1029
  return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
947
1030
  });
@@ -949,6 +1032,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
949
1032
  ${rows.join("\n")}
950
1033
  `;
951
1034
  }
1035
+ function renderCheckoutPath(response) {
1036
+ return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
1037
+ "\n"
1038
+ );
1039
+ }
952
1040
  function assertAuthLoginArgs(args) {
953
1041
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
954
1042
  const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
@@ -1790,6 +1878,12 @@ function renderWorkspaceStatus(status) {
1790
1878
  `Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
1791
1879
  ...status.workers.flatMap((worker) => [
1792
1880
  `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
1881
+ ...worker.hydrationRecovery ? [
1882
+ ` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
1883
+ ` Evidence: ${worker.hydrationRecovery.transactionPath}`,
1884
+ ...(worker.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`),
1885
+ ` After stopping active processes: r5dctl workspace recover-hydration --worker ${JSON.stringify(worker.workerLabel)} --transaction ${worker.hydrationRecovery.transactionId} --confirm`
1886
+ ] : [],
1793
1887
  ...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
1794
1888
  ...renderWorkerCapacity(worker.workerLabel, worker.capacity)
1795
1889
  ])
@@ -1797,6 +1891,15 @@ function renderWorkspaceStatus(status) {
1797
1891
  if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
1798
1892
  lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
1799
1893
  }
1894
+ if (status.latestSync?.hydrationRecovery && !status.workers.some((worker) => worker.workerLabel === status.latestSync.workerLabel)) {
1895
+ lines.push(
1896
+ `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.`,
1897
+ ...(status.latestSync.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`)
1898
+ );
1899
+ }
1900
+ for (const [mountId, skip] of Object.entries(status.latestSync?.mountSkipReasons ?? {})) {
1901
+ for (const retainedPath of skip.retainedPaths ?? []) lines.push(`Retained writer entry for ${mountId}: ${retainedPath}`);
1902
+ }
1800
1903
  return `${lines.join("\n")}
1801
1904
  `;
1802
1905
  }
@@ -2083,11 +2186,17 @@ function renderSessionRunStart(session) {
2083
2186
  lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
2084
2187
  const sessionUrl = responseString(session, "sessionUrl");
2085
2188
  if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
2189
+ lines.push(...formatSessionWarningLines(session));
2086
2190
  const sessionId = responseString(session, "sessionId");
2087
2191
  if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
2088
2192
  return `${lines.join("\n")}
2089
2193
  `;
2090
2194
  }
2195
+ function formatSessionWarningLines(session) {
2196
+ const warnings = session.warnings;
2197
+ if (!Array.isArray(warnings)) return [];
2198
+ return warnings.filter((warning) => typeof warning === "string" && warning.length > 0).map((warning) => `Warning: ${warning}`);
2199
+ }
2091
2200
  function formatBranchStateHint(branch) {
2092
2201
  const target = branch ?? "<branch>";
2093
2202
  const commands = [
@@ -2138,6 +2247,7 @@ function renderSessionRunStatus(session) {
2138
2247
  }
2139
2248
  const childSessions = formatChildSessionsLine(session);
2140
2249
  if (childSessions) lines.push(childSessions);
2250
+ lines.push(...formatSessionWarningLines(session));
2141
2251
  lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
2142
2252
  const error = responseString(session, "error");
2143
2253
  if (error) lines.push(`Error: ${error}`);
@@ -2764,6 +2874,23 @@ async function executeR5dctlCommand(client, json, args) {
2764
2874
  write(result, renderWorkspaceSync(result));
2765
2875
  return;
2766
2876
  }
2877
+ if (first === "workspace" && second === "recover-hydration") {
2878
+ let worker;
2879
+ let transactionId;
2880
+ let confirmed = false;
2881
+ for (let index = 2; index < args.length; index += 1) {
2882
+ const arg = args[index];
2883
+ if (arg === "--worker") worker = requireValue(args[++index], "Missing worker label");
2884
+ else if (arg === "--transaction") transactionId = requireValue(args[++index], "Missing transaction id");
2885
+ else if (arg === "--confirm") confirmed = true;
2886
+ else throw new Error(`Unexpected recovery argument: ${arg}`);
2887
+ }
2888
+ if (!worker || !transactionId || !confirmed)
2889
+ throw new Error("workspace recover-hydration requires --worker, --transaction and --confirm; inspect workspace status first");
2890
+ const result = await client.workspace.recoverHydration({ worker, transactionId, confirmed: true });
2891
+ write(result, renderWorkspaceSync(result));
2892
+ return;
2893
+ }
2767
2894
  if (first === "workspace" && second === "reset") {
2768
2895
  const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
2769
2896
  const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
@@ -2794,7 +2921,7 @@ async function executeR5dctlCommand(client, json, args) {
2794
2921
  }
2795
2922
  if (first === "session" && second === "claim") {
2796
2923
  const claim = parseSessionClaimArgs(args.slice(2), {}, {});
2797
- const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
2924
+ const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
2798
2925
  const nowMs = Date.now();
2799
2926
  if (claim.action === "acquire") {
2800
2927
  const result2 = await client.claims.acquire({
@@ -2803,23 +2930,34 @@ async function executeR5dctlCommand(client, json, args) {
2803
2930
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
2804
2931
  ...claim.note !== void 0 ? { note: claim.note } : {}
2805
2932
  });
2933
+ if (!claimScopeMatches(claim.scope, result2.claim)) {
2934
+ if (result2.outcome === "acquired" && result2.claim.heldByActor) {
2935
+ await client.claims.release({
2936
+ topic: claim.topic,
2937
+ ...claim.sessionId ? { sessionId: claim.sessionId } : {},
2938
+ generation: result2.claim.generation
2939
+ }).catch(() => void 0);
2940
+ }
2941
+ throw new R5dctlClaimScopeMismatchError(claim.scope, result2.claim);
2942
+ }
2806
2943
  write(result2, renderSessionClaimResponse(result2, nowMs));
2807
2944
  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
- );
2945
+ const until = result2.claim.expiresAt === null ? "while it runs (native session)" : `until ${result2.claim.expiresAt}`;
2946
+ throw new Error(`Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} ${until}; not acquired`);
2811
2947
  }
2812
2948
  return;
2813
2949
  }
2814
2950
  if (claim.action === "status") {
2815
2951
  const list = await client.claims.list({ ...scope, topic: claim.topic });
2952
+ assertClaimScopeEcho(claim.scope, list);
2816
2953
  write(list, renderSessionClaimList(list, nowMs));
2817
2954
  return;
2818
2955
  }
2819
2956
  let generation = claim.generation;
2820
- if (generation === void 0) {
2957
+ if (generation === void 0 || claim.scope) {
2821
2958
  const list = await client.claims.list({ ...scope, topic: claim.topic });
2822
- const current = list.claims[0];
2959
+ assertClaimScopeEcho(claim.scope, list);
2960
+ const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
2823
2961
  if (!current) {
2824
2962
  write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
2825
2963
  `);
@@ -2833,7 +2971,7 @@ async function executeR5dctlCommand(client, json, args) {
2833
2971
  );
2834
2972
  throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
2835
2973
  }
2836
- generation = current.generation;
2974
+ generation ??= current.generation;
2837
2975
  }
2838
2976
  const result = claim.action === "renew" ? await client.claims.renew({
2839
2977
  topic: claim.topic,
@@ -2841,16 +2979,25 @@ async function executeR5dctlCommand(client, json, args) {
2841
2979
  generation,
2842
2980
  ...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
2843
2981
  }) : await client.claims.release({ topic: claim.topic, ...scope, generation });
2982
+ assertClaimScopeEcho(claim.scope, result.claim);
2844
2983
  write(result, renderSessionClaimResponse(result, nowMs));
2845
2984
  const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
2846
2985
  if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
2847
2986
  return;
2848
2987
  }
2849
2988
  if (first === "session" && second === "claims") {
2850
- const list = await client.claims.list(parseSessionClaimsListArgs(args.slice(2), {}, {}));
2989
+ const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
2990
+ const list = await client.claims.list(selection);
2991
+ assertClaimScopeEcho(selection.scope, list);
2851
2992
  write(list, renderSessionClaimList(list));
2852
2993
  return;
2853
2994
  }
2995
+ if (first === "checkout" && second === "path") {
2996
+ const selection = parseCheckoutPathArgs(args.slice(2), {}, {});
2997
+ const result = await client.checkouts.path(selection);
2998
+ write(result, renderCheckoutPath(result));
2999
+ return;
3000
+ }
2854
3001
  if (first === "session" && second === "process-log") {
2855
3002
  const selection = parseSessionProcessLogArgs(args.slice(2));
2856
3003
  const log = await client.processes.log(selection.runId, {
@@ -2950,7 +3097,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
2950
3097
  throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
2951
3098
  }
2952
3099
  if (json) write({ results: deletion.results }, "");
2953
- if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3100
+ if (deletion.results.some((result) => !result.deleted && !result.accepted))
3101
+ throw new Error(branchDeletionsFailureMessage(deletion.results));
2954
3102
  return;
2955
3103
  }
2956
3104
  if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
@@ -3547,24 +3695,43 @@ function resolveCommandExecution(options, rest) {
3547
3695
  ...parseShellArgs(commandArgs)
3548
3696
  };
3549
3697
  }
3698
+ if (command === "checkout") {
3699
+ if (commandArgs[0] !== "path") throw new Error("Unknown checkout command");
3700
+ const parsed = parseCheckoutPathArgs(commandArgs.slice(1), options);
3701
+ return {
3702
+ kind: "plugin",
3703
+ pluginArgs: [
3704
+ "checkout",
3705
+ "path",
3706
+ `--project=${parsed.projectId}`,
3707
+ `--branch=${parsed.branchName}`,
3708
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3709
+ ]
3710
+ };
3711
+ }
3550
3712
  if (command === "session") {
3551
3713
  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
3714
  const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
3555
- const withoutSessionValue = [];
3715
+ const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
3716
+ const withoutNormalizedValues = [];
3556
3717
  for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
3557
3718
  const arg = commandArgs.slice(1)[index];
3558
- if (arg === "--session") {
3719
+ if (normalizedFlags.has(arg)) {
3559
3720
  index += 1;
3560
3721
  continue;
3561
3722
  }
3562
- if (arg.startsWith("--session=")) continue;
3563
- withoutSessionValue.push(arg);
3723
+ if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
3724
+ withoutNormalizedValues.push(arg);
3564
3725
  }
3565
3726
  return {
3566
3727
  kind: "plugin",
3567
- pluginArgs: ["session", commandArgs[0], ...withoutSessionValue, ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []]
3728
+ pluginArgs: [
3729
+ "session",
3730
+ commandArgs[0],
3731
+ ...withoutNormalizedValues,
3732
+ ...parsed.scope ? [`--project=${parsed.scope.projectId}`, `--branch=${parsed.scope.branchName}`] : [],
3733
+ ...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
3734
+ ]
3568
3735
  };
3569
3736
  }
3570
3737
  if (commandArgs[0] === "process-log") {
@@ -3737,10 +3904,12 @@ async function main(argv = process.argv.slice(2)) {
3737
3904
  export {
3738
3905
  BRANCH_DELETION_POLL_INTERVAL_MS,
3739
3906
  BRANCH_DELETION_WAIT_TIMEOUT_MS,
3907
+ R5dctlClaimScopeMismatchError,
3740
3908
  advanceTransientDevicePollBackoff,
3741
3909
  branchDeletionConfirmationMessage,
3742
3910
  branchDeletionsConfirmationMessage,
3743
3911
  branchDeletionsFailureMessage,
3912
+ claimScopeMatches,
3744
3913
  createBranchThroughPlatform,
3745
3914
  deleteBranchThroughPlatform,
3746
3915
  deleteBranchesThroughPlatform,
@@ -3760,6 +3929,7 @@ export {
3760
3929
  parseAnswerQuestionsCommandArgs,
3761
3930
  parseBranchCreateArgs,
3762
3931
  parseBranchDeleteArgs,
3932
+ parseCheckoutPathArgs,
3763
3933
  parseClaimDuration,
3764
3934
  parseConversationForkArgs,
3765
3935
  parseConversationInspectWorkArgs,
@@ -3785,6 +3955,7 @@ export {
3785
3955
  renderBranchDeletionStatus,
3786
3956
  renderBranchList,
3787
3957
  renderBranchOperation,
3958
+ renderCheckoutPath,
3788
3959
  renderConversationForkResponse,
3789
3960
  renderConversationHeadResponse,
3790
3961
  renderConversationNodeResponse,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.132",
3
+ "version": "0.0.134",
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.134",
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.134",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"