@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 +212 -37
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +208 -37
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +36 -3
- package/package.json +2 -2
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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)
|
|
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")
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
2915
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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))
|
|
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
|
|
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
|
|
3829
|
+
if (normalizedFlags.has(arg)) {
|
|
3665
3830
|
index += 1;
|
|
3666
3831
|
continue;
|
|
3667
3832
|
}
|
|
3668
|
-
if (arg.startsWith(
|
|
3669
|
-
|
|
3833
|
+
if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
|
|
3834
|
+
withoutNormalizedValues.push(arg);
|
|
3670
3835
|
}
|
|
3671
3836
|
return {
|
|
3672
3837
|
kind: "plugin",
|
|
3673
|
-
pluginArgs: [
|
|
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,
|
package/dist/cjs/package.json
CHANGED
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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)
|
|
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")
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
2809
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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))
|
|
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
|
|
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
|
|
3719
|
+
if (normalizedFlags.has(arg)) {
|
|
3559
3720
|
index += 1;
|
|
3560
3721
|
continue;
|
|
3561
3722
|
}
|
|
3562
|
-
if (arg.startsWith(
|
|
3563
|
-
|
|
3723
|
+
if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
|
|
3724
|
+
withoutNormalizedValues.push(arg);
|
|
3564
3725
|
}
|
|
3565
3726
|
return {
|
|
3566
3727
|
kind: "plugin",
|
|
3567
|
-
pluginArgs: [
|
|
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,
|
package/dist/mjs/package.json
CHANGED
package/dist/types/cli.d.ts
CHANGED
|
@@ -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?:
|
|
220
|
-
export declare function parseSessionClaimsListArgs(args: string[], options?:
|
|
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.
|
|
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.
|
|
29
|
+
"@ricsam/r5d-api": "^0.0.134",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"qrcode": "^1.5.4",
|
|
32
32
|
"ws": "^8.18.3"
|