@ricsam/r5dctl 0.0.132 → 0.0.133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli.cjs +204 -33
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +200 -33
- 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",
|
|
@@ -955,13 +975,25 @@ function parseSessionProcessLogArgs(args) {
|
|
|
955
975
|
const grep = parsed.values.get("--grep");
|
|
956
976
|
return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
|
|
957
977
|
}
|
|
978
|
+
function resolveClaimScope(explicitProject, explicitBranch, options) {
|
|
979
|
+
const project = explicitProject?.trim() || options.project?.trim() || void 0;
|
|
980
|
+
const branch = explicitBranch?.trim() || options.branch?.trim() || void 0;
|
|
981
|
+
if (project === void 0 && branch === void 0) return void 0;
|
|
982
|
+
if (project === void 0 || branch === void 0) {
|
|
983
|
+
throw new Error(
|
|
984
|
+
"A scoped claim needs both -p/--project and --branch/-b; pass both to act on a sibling checkout, or neither for the session's own branch"
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
return { projectId: project, branchName: branch };
|
|
988
|
+
}
|
|
958
989
|
const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
|
|
959
990
|
function parseClaimDuration(value) {
|
|
960
991
|
const match = CLAIM_DURATION_PATTERN.exec(value.trim());
|
|
961
992
|
if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
|
|
962
993
|
const amount = Number(match[1]);
|
|
963
994
|
const seconds = match[2] === "s" ? amount : match[2] === "m" ? amount * 60 : amount * 3600;
|
|
964
|
-
if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
|
|
995
|
+
if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
|
|
996
|
+
throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
|
|
965
997
|
return seconds;
|
|
966
998
|
}
|
|
967
999
|
function resolveClaimSessionId(explicit, options, env) {
|
|
@@ -970,7 +1002,7 @@ function resolveClaimSessionId(explicit, options, env) {
|
|
|
970
1002
|
function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
971
1003
|
const parsed = parseCommandOperands(
|
|
972
1004
|
args,
|
|
973
|
-
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
|
|
1005
|
+
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
|
|
974
1006
|
/* @__PURE__ */ new Map([
|
|
975
1007
|
["--status", "--status"],
|
|
976
1008
|
["--renew", "--renew"],
|
|
@@ -983,7 +1015,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
983
1015
|
if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
|
|
984
1016
|
const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
|
|
985
1017
|
const ttl = parsed.values.get("--ttl");
|
|
986
|
-
if (ttl !== void 0 && action !== "acquire" && action !== "renew")
|
|
1018
|
+
if (ttl !== void 0 && action !== "acquire" && action !== "renew")
|
|
1019
|
+
throw new Error("--ttl is only supported when acquiring or renewing a claim");
|
|
987
1020
|
const note = parsed.values.get("--note");
|
|
988
1021
|
if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
|
|
989
1022
|
const generationValue = parsed.values.get("--generation");
|
|
@@ -991,22 +1024,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
991
1024
|
throw new Error("--generation is only supported with --renew or --release");
|
|
992
1025
|
}
|
|
993
1026
|
const generation = generationValue === void 0 ? void 0 : Number(generationValue);
|
|
994
|
-
if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
|
|
1027
|
+
if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
|
|
1028
|
+
throw new Error("--generation must be a positive integer");
|
|
995
1029
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
1030
|
+
const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
|
|
996
1031
|
return {
|
|
997
1032
|
action,
|
|
998
1033
|
topic,
|
|
999
1034
|
...sessionId ? { sessionId } : {},
|
|
1035
|
+
...scope ? { scope } : {},
|
|
1000
1036
|
...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
|
|
1001
1037
|
...note !== void 0 ? { note } : {},
|
|
1002
1038
|
...generation !== void 0 ? { generation } : {}
|
|
1003
1039
|
};
|
|
1004
1040
|
}
|
|
1005
1041
|
function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
|
|
1006
|
-
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session"]), /* @__PURE__ */ new Map(), "session claims");
|
|
1042
|
+
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "session claims");
|
|
1007
1043
|
if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
|
|
1008
1044
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
1009
|
-
|
|
1045
|
+
const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
|
|
1046
|
+
return { ...sessionId ? { sessionId } : {}, ...scope ? { scope } : {} };
|
|
1047
|
+
}
|
|
1048
|
+
function parseCheckoutPathArgs(args, options = {}, env = process.env) {
|
|
1049
|
+
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "checkout path");
|
|
1050
|
+
if (parsed.positionals.length > 0) throw new Error(`Unexpected checkout path argument: ${parsed.positionals[0]}`);
|
|
1051
|
+
const projectId = parsed.values.get("--project")?.trim() || options.project?.trim();
|
|
1052
|
+
const branchName = parsed.values.get("--branch")?.trim() || options.branch?.trim();
|
|
1053
|
+
if (!projectId) throw new Error("--project/-p is required for `checkout path`");
|
|
1054
|
+
if (!branchName) throw new Error("--branch/-b is required for `checkout path`");
|
|
1055
|
+
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
1056
|
+
return { projectId, branchName, ...sessionId ? { sessionId } : {} };
|
|
1057
|
+
}
|
|
1058
|
+
class R5dctlClaimScopeMismatchError extends Error {
|
|
1059
|
+
constructor(requested, answered) {
|
|
1060
|
+
super(
|
|
1061
|
+
`The server did not apply the requested claim scope ${requested.projectId}#${requested.branchName}; it answered for ${answered.projectPath ?? answered.projectId}#${answered.branchName}. Update the r5d server before using scoped claims`
|
|
1062
|
+
);
|
|
1063
|
+
this.name = "R5dctlClaimScopeMismatchError";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function claimScopeMatches(requested, answered) {
|
|
1067
|
+
if (!requested || !answered) return true;
|
|
1068
|
+
const projectMatches = answered.projectId === requested.projectId || answered.projectPath === requested.projectId;
|
|
1069
|
+
return projectMatches && answered.branchName === requested.branchName;
|
|
1070
|
+
}
|
|
1071
|
+
function assertClaimScopeEcho(requested, answered) {
|
|
1072
|
+
if (!claimScopeMatches(requested, answered)) throw new R5dctlClaimScopeMismatchError(requested, answered);
|
|
1010
1073
|
}
|
|
1011
1074
|
const CLAIM_OUTCOME_TEXT = {
|
|
1012
1075
|
acquired: "acquired",
|
|
@@ -1019,35 +1082,55 @@ const CLAIM_OUTCOME_TEXT = {
|
|
|
1019
1082
|
released: "released"
|
|
1020
1083
|
};
|
|
1021
1084
|
function formatClaimExpiry(expiresAt, nowMs) {
|
|
1085
|
+
if (expiresAt === null) return "while the holder runs (implicit)";
|
|
1022
1086
|
const expiresAtMs = Date.parse(expiresAt);
|
|
1023
1087
|
if (!Number.isFinite(expiresAtMs)) return expiresAt;
|
|
1024
1088
|
return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
|
|
1025
1089
|
}
|
|
1090
|
+
function describeClaimScope(scope) {
|
|
1091
|
+
return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
|
|
1092
|
+
}
|
|
1026
1093
|
function describeClaimHolder(claim) {
|
|
1027
1094
|
const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
|
|
1028
|
-
|
|
1095
|
+
const self = claim.heldByActor ? " (this session)" : "";
|
|
1096
|
+
let origin = "";
|
|
1097
|
+
if (claim.holderBranchName !== void 0 && claim.holderBranchName !== null) {
|
|
1098
|
+
const native = claim.holderProjectId === claim.projectId && claim.holderBranchName === claim.branchName;
|
|
1099
|
+
if (native) origin = claim.implicit ? " (native, running)" : " (native)";
|
|
1100
|
+
else if (claim.holderProjectId === claim.projectId) origin = ` (foreign: native to branch ${claim.holderBranchName})`;
|
|
1101
|
+
else origin = ` (foreign: native to ${claim.holderProjectId ?? "unknown"}#${claim.holderBranchName})`;
|
|
1102
|
+
}
|
|
1103
|
+
return `${claim.holderSessionId}${name}${self}${origin}`;
|
|
1104
|
+
}
|
|
1105
|
+
function describeClaimFence(response) {
|
|
1106
|
+
if (response.fenceExtended === void 0) return void 0;
|
|
1107
|
+
if (response.fenceExtended)
|
|
1108
|
+
return "Fence: commands this session starts while holding the claim also fence the claimed checkout on the bound worker";
|
|
1109
|
+
return `Fence: claim acquired, but ${response.fenceDetail ?? "the bound worker does not fence sibling checkouts (update the worker)"}; the claim still coordinates between sessions`;
|
|
1029
1110
|
}
|
|
1030
1111
|
function renderSessionClaimResponse(response, nowMs = Date.now()) {
|
|
1031
1112
|
const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
|
|
1032
1113
|
if (response.claim) {
|
|
1033
1114
|
lines.push(
|
|
1034
1115
|
`Claim: ${response.claim.topic}`,
|
|
1035
|
-
`
|
|
1116
|
+
`Scope: ${describeClaimScope(response.claim)}`,
|
|
1036
1117
|
`Holder: ${describeClaimHolder(response.claim)}`,
|
|
1037
1118
|
`Generation: ${response.claim.generation}`,
|
|
1038
1119
|
`Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
|
|
1039
1120
|
);
|
|
1040
1121
|
if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
|
|
1041
1122
|
}
|
|
1123
|
+
const fence = describeClaimFence(response);
|
|
1124
|
+
if (fence) lines.push(fence);
|
|
1042
1125
|
return `${lines.join("\n")}
|
|
1043
1126
|
`;
|
|
1044
1127
|
}
|
|
1045
1128
|
function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
1046
|
-
const header = `Claims on ${response
|
|
1129
|
+
const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
|
|
1047
1130
|
if (response.claims.length === 0) return `${header}: none
|
|
1048
1131
|
`;
|
|
1049
1132
|
const rows = response.claims.map((claim) => {
|
|
1050
|
-
const expiry = claim.expired ? "expired" : "expires";
|
|
1133
|
+
const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
|
|
1051
1134
|
const note = claim.note ? ` note: ${claim.note}` : "";
|
|
1052
1135
|
return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
|
|
1053
1136
|
});
|
|
@@ -1055,6 +1138,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
|
1055
1138
|
${rows.join("\n")}
|
|
1056
1139
|
`;
|
|
1057
1140
|
}
|
|
1141
|
+
function renderCheckoutPath(response) {
|
|
1142
|
+
return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
|
|
1143
|
+
"\n"
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1058
1146
|
function assertAuthLoginArgs(args) {
|
|
1059
1147
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
1060
1148
|
const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
|
|
@@ -1896,6 +1984,12 @@ function renderWorkspaceStatus(status) {
|
|
|
1896
1984
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1897
1985
|
...status.workers.flatMap((worker) => [
|
|
1898
1986
|
`Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
|
|
1987
|
+
...worker.hydrationRecovery ? [
|
|
1988
|
+
` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
|
|
1989
|
+
` Evidence: ${worker.hydrationRecovery.transactionPath}`,
|
|
1990
|
+
...(worker.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`),
|
|
1991
|
+
` After stopping active processes: r5dctl workspace recover-hydration --worker ${JSON.stringify(worker.workerLabel)} --transaction ${worker.hydrationRecovery.transactionId} --confirm`
|
|
1992
|
+
] : [],
|
|
1899
1993
|
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
|
|
1900
1994
|
...renderWorkerCapacity(worker.workerLabel, worker.capacity)
|
|
1901
1995
|
])
|
|
@@ -1903,6 +1997,15 @@ function renderWorkspaceStatus(status) {
|
|
|
1903
1997
|
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
1904
1998
|
lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
|
|
1905
1999
|
}
|
|
2000
|
+
if (status.latestSync?.hydrationRecovery && !status.workers.some((worker) => worker.workerLabel === status.latestSync.workerLabel)) {
|
|
2001
|
+
lines.push(
|
|
2002
|
+
`Disconnected worker ${status.latestSync.workerLabel} reported interrupted hydration ${status.latestSync.hydrationRecovery.transactionId}; preserved evidence: ${status.latestSync.hydrationRecovery.transactionPath}. Reconnect it, inspect workspace status, and preserve the transaction before resuming work.`,
|
|
2003
|
+
...(status.latestSync.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`)
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
for (const [mountId, skip] of Object.entries(status.latestSync?.mountSkipReasons ?? {})) {
|
|
2007
|
+
for (const retainedPath of skip.retainedPaths ?? []) lines.push(`Retained writer entry for ${mountId}: ${retainedPath}`);
|
|
2008
|
+
}
|
|
1906
2009
|
return `${lines.join("\n")}
|
|
1907
2010
|
`;
|
|
1908
2011
|
}
|
|
@@ -2189,11 +2292,17 @@ function renderSessionRunStart(session) {
|
|
|
2189
2292
|
lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
|
|
2190
2293
|
const sessionUrl = responseString(session, "sessionUrl");
|
|
2191
2294
|
if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
|
|
2295
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2192
2296
|
const sessionId = responseString(session, "sessionId");
|
|
2193
2297
|
if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
|
|
2194
2298
|
return `${lines.join("\n")}
|
|
2195
2299
|
`;
|
|
2196
2300
|
}
|
|
2301
|
+
function formatSessionWarningLines(session) {
|
|
2302
|
+
const warnings = session.warnings;
|
|
2303
|
+
if (!Array.isArray(warnings)) return [];
|
|
2304
|
+
return warnings.filter((warning) => typeof warning === "string" && warning.length > 0).map((warning) => `Warning: ${warning}`);
|
|
2305
|
+
}
|
|
2197
2306
|
function formatBranchStateHint(branch) {
|
|
2198
2307
|
const target = branch ?? "<branch>";
|
|
2199
2308
|
const commands = [
|
|
@@ -2244,6 +2353,7 @@ function renderSessionRunStatus(session) {
|
|
|
2244
2353
|
}
|
|
2245
2354
|
const childSessions = formatChildSessionsLine(session);
|
|
2246
2355
|
if (childSessions) lines.push(childSessions);
|
|
2356
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2247
2357
|
lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
|
|
2248
2358
|
const error = responseString(session, "error");
|
|
2249
2359
|
if (error) lines.push(`Error: ${error}`);
|
|
@@ -2870,6 +2980,23 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2870
2980
|
write(result, renderWorkspaceSync(result));
|
|
2871
2981
|
return;
|
|
2872
2982
|
}
|
|
2983
|
+
if (first === "workspace" && second === "recover-hydration") {
|
|
2984
|
+
let worker;
|
|
2985
|
+
let transactionId;
|
|
2986
|
+
let confirmed = false;
|
|
2987
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
2988
|
+
const arg = args[index];
|
|
2989
|
+
if (arg === "--worker") worker = requireValue(args[++index], "Missing worker label");
|
|
2990
|
+
else if (arg === "--transaction") transactionId = requireValue(args[++index], "Missing transaction id");
|
|
2991
|
+
else if (arg === "--confirm") confirmed = true;
|
|
2992
|
+
else throw new Error(`Unexpected recovery argument: ${arg}`);
|
|
2993
|
+
}
|
|
2994
|
+
if (!worker || !transactionId || !confirmed)
|
|
2995
|
+
throw new Error("workspace recover-hydration requires --worker, --transaction and --confirm; inspect workspace status first");
|
|
2996
|
+
const result = await client.workspace.recoverHydration({ worker, transactionId, confirmed: true });
|
|
2997
|
+
write(result, renderWorkspaceSync(result));
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
2873
3000
|
if (first === "workspace" && second === "reset") {
|
|
2874
3001
|
const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
|
|
2875
3002
|
const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
|
|
@@ -2900,7 +3027,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2900
3027
|
}
|
|
2901
3028
|
if (first === "session" && second === "claim") {
|
|
2902
3029
|
const claim = parseSessionClaimArgs(args.slice(2), {}, {});
|
|
2903
|
-
const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
|
|
3030
|
+
const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
|
|
2904
3031
|
const nowMs = Date.now();
|
|
2905
3032
|
if (claim.action === "acquire") {
|
|
2906
3033
|
const result2 = await client.claims.acquire({
|
|
@@ -2909,23 +3036,34 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2909
3036
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
|
|
2910
3037
|
...claim.note !== void 0 ? { note: claim.note } : {}
|
|
2911
3038
|
});
|
|
3039
|
+
if (!claimScopeMatches(claim.scope, result2.claim)) {
|
|
3040
|
+
if (result2.outcome === "acquired" && result2.claim.heldByActor) {
|
|
3041
|
+
await client.claims.release({
|
|
3042
|
+
topic: claim.topic,
|
|
3043
|
+
...claim.sessionId ? { sessionId: claim.sessionId } : {},
|
|
3044
|
+
generation: result2.claim.generation
|
|
3045
|
+
}).catch(() => void 0);
|
|
3046
|
+
}
|
|
3047
|
+
throw new R5dctlClaimScopeMismatchError(claim.scope, result2.claim);
|
|
3048
|
+
}
|
|
2912
3049
|
write(result2, renderSessionClaimResponse(result2, nowMs));
|
|
2913
3050
|
if (result2.outcome === "held_by_other") {
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
);
|
|
3051
|
+
const until = result2.claim.expiresAt === null ? "while it runs (native session)" : `until ${result2.claim.expiresAt}`;
|
|
3052
|
+
throw new Error(`Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} ${until}; not acquired`);
|
|
2917
3053
|
}
|
|
2918
3054
|
return;
|
|
2919
3055
|
}
|
|
2920
3056
|
if (claim.action === "status") {
|
|
2921
3057
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
3058
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
2922
3059
|
write(list, renderSessionClaimList(list, nowMs));
|
|
2923
3060
|
return;
|
|
2924
3061
|
}
|
|
2925
3062
|
let generation = claim.generation;
|
|
2926
|
-
if (generation === void 0) {
|
|
3063
|
+
if (generation === void 0 || claim.scope) {
|
|
2927
3064
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
2928
|
-
|
|
3065
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
3066
|
+
const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
|
|
2929
3067
|
if (!current) {
|
|
2930
3068
|
write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
|
|
2931
3069
|
`);
|
|
@@ -2939,7 +3077,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2939
3077
|
);
|
|
2940
3078
|
throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
|
|
2941
3079
|
}
|
|
2942
|
-
generation
|
|
3080
|
+
generation ??= current.generation;
|
|
2943
3081
|
}
|
|
2944
3082
|
const result = claim.action === "renew" ? await client.claims.renew({
|
|
2945
3083
|
topic: claim.topic,
|
|
@@ -2947,16 +3085,25 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2947
3085
|
generation,
|
|
2948
3086
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
|
|
2949
3087
|
}) : await client.claims.release({ topic: claim.topic, ...scope, generation });
|
|
3088
|
+
assertClaimScopeEcho(claim.scope, result.claim);
|
|
2950
3089
|
write(result, renderSessionClaimResponse(result, nowMs));
|
|
2951
3090
|
const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
|
|
2952
3091
|
if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
|
|
2953
3092
|
return;
|
|
2954
3093
|
}
|
|
2955
3094
|
if (first === "session" && second === "claims") {
|
|
2956
|
-
const
|
|
3095
|
+
const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
|
|
3096
|
+
const list = await client.claims.list(selection);
|
|
3097
|
+
assertClaimScopeEcho(selection.scope, list);
|
|
2957
3098
|
write(list, renderSessionClaimList(list));
|
|
2958
3099
|
return;
|
|
2959
3100
|
}
|
|
3101
|
+
if (first === "checkout" && second === "path") {
|
|
3102
|
+
const selection = parseCheckoutPathArgs(args.slice(2), {}, {});
|
|
3103
|
+
const result = await client.checkouts.path(selection);
|
|
3104
|
+
write(result, renderCheckoutPath(result));
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
2960
3107
|
if (first === "session" && second === "process-log") {
|
|
2961
3108
|
const selection = parseSessionProcessLogArgs(args.slice(2));
|
|
2962
3109
|
const log = await client.processes.log(selection.runId, {
|
|
@@ -3056,7 +3203,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
|
|
|
3056
3203
|
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
3057
3204
|
}
|
|
3058
3205
|
if (json) write({ results: deletion.results }, "");
|
|
3059
|
-
if (deletion.results.some((result) => !result.deleted && !result.accepted))
|
|
3206
|
+
if (deletion.results.some((result) => !result.deleted && !result.accepted))
|
|
3207
|
+
throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
3060
3208
|
return;
|
|
3061
3209
|
}
|
|
3062
3210
|
if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
|
|
@@ -3653,24 +3801,43 @@ function resolveCommandExecution(options, rest) {
|
|
|
3653
3801
|
...parseShellArgs(commandArgs)
|
|
3654
3802
|
};
|
|
3655
3803
|
}
|
|
3804
|
+
if (command === "checkout") {
|
|
3805
|
+
if (commandArgs[0] !== "path") throw new Error("Unknown checkout command");
|
|
3806
|
+
const parsed = parseCheckoutPathArgs(commandArgs.slice(1), options);
|
|
3807
|
+
return {
|
|
3808
|
+
kind: "plugin",
|
|
3809
|
+
pluginArgs: [
|
|
3810
|
+
"checkout",
|
|
3811
|
+
"path",
|
|
3812
|
+
`--project=${parsed.projectId}`,
|
|
3813
|
+
`--branch=${parsed.branchName}`,
|
|
3814
|
+
...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
|
|
3815
|
+
]
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3656
3818
|
if (command === "session") {
|
|
3657
3819
|
if (commandArgs[0] === "claim" || commandArgs[0] === "claims") {
|
|
3658
|
-
if (options.project) throw new Error("--project/-p is not supported for `session claim`; the acting session selects the project branch");
|
|
3659
|
-
if (options.branch) throw new Error("--branch/-b is not supported for `session claim`; the acting session selects the branch");
|
|
3660
3820
|
const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
|
|
3661
|
-
const
|
|
3821
|
+
const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
|
|
3822
|
+
const withoutNormalizedValues = [];
|
|
3662
3823
|
for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
|
|
3663
3824
|
const arg = commandArgs.slice(1)[index];
|
|
3664
|
-
if (arg
|
|
3825
|
+
if (normalizedFlags.has(arg)) {
|
|
3665
3826
|
index += 1;
|
|
3666
3827
|
continue;
|
|
3667
3828
|
}
|
|
3668
|
-
if (arg.startsWith(
|
|
3669
|
-
|
|
3829
|
+
if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
|
|
3830
|
+
withoutNormalizedValues.push(arg);
|
|
3670
3831
|
}
|
|
3671
3832
|
return {
|
|
3672
3833
|
kind: "plugin",
|
|
3673
|
-
pluginArgs: [
|
|
3834
|
+
pluginArgs: [
|
|
3835
|
+
"session",
|
|
3836
|
+
commandArgs[0],
|
|
3837
|
+
...withoutNormalizedValues,
|
|
3838
|
+
...parsed.scope ? [`--project=${parsed.scope.projectId}`, `--branch=${parsed.scope.branchName}`] : [],
|
|
3839
|
+
...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
|
|
3840
|
+
]
|
|
3674
3841
|
};
|
|
3675
3842
|
}
|
|
3676
3843
|
if (commandArgs[0] === "process-log") {
|
|
@@ -3844,10 +4011,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3844
4011
|
0 && (module.exports = {
|
|
3845
4012
|
BRANCH_DELETION_POLL_INTERVAL_MS,
|
|
3846
4013
|
BRANCH_DELETION_WAIT_TIMEOUT_MS,
|
|
4014
|
+
R5dctlClaimScopeMismatchError,
|
|
3847
4015
|
advanceTransientDevicePollBackoff,
|
|
3848
4016
|
branchDeletionConfirmationMessage,
|
|
3849
4017
|
branchDeletionsConfirmationMessage,
|
|
3850
4018
|
branchDeletionsFailureMessage,
|
|
4019
|
+
claimScopeMatches,
|
|
3851
4020
|
createBranchThroughPlatform,
|
|
3852
4021
|
deleteBranchThroughPlatform,
|
|
3853
4022
|
deleteBranchesThroughPlatform,
|
|
@@ -3867,6 +4036,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3867
4036
|
parseAnswerQuestionsCommandArgs,
|
|
3868
4037
|
parseBranchCreateArgs,
|
|
3869
4038
|
parseBranchDeleteArgs,
|
|
4039
|
+
parseCheckoutPathArgs,
|
|
3870
4040
|
parseClaimDuration,
|
|
3871
4041
|
parseConversationForkArgs,
|
|
3872
4042
|
parseConversationInspectWorkArgs,
|
|
@@ -3892,6 +4062,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3892
4062
|
renderBranchDeletionStatus,
|
|
3893
4063
|
renderBranchList,
|
|
3894
4064
|
renderBranchOperation,
|
|
4065
|
+
renderCheckoutPath,
|
|
3895
4066
|
renderConversationForkResponse,
|
|
3896
4067
|
renderConversationHeadResponse,
|
|
3897
4068
|
renderConversationNodeResponse,
|
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",
|
|
@@ -849,13 +865,25 @@ function parseSessionProcessLogArgs(args) {
|
|
|
849
865
|
const grep = parsed.values.get("--grep");
|
|
850
866
|
return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
|
|
851
867
|
}
|
|
868
|
+
function resolveClaimScope(explicitProject, explicitBranch, options) {
|
|
869
|
+
const project = explicitProject?.trim() || options.project?.trim() || void 0;
|
|
870
|
+
const branch = explicitBranch?.trim() || options.branch?.trim() || void 0;
|
|
871
|
+
if (project === void 0 && branch === void 0) return void 0;
|
|
872
|
+
if (project === void 0 || branch === void 0) {
|
|
873
|
+
throw new Error(
|
|
874
|
+
"A scoped claim needs both -p/--project and --branch/-b; pass both to act on a sibling checkout, or neither for the session's own branch"
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
return { projectId: project, branchName: branch };
|
|
878
|
+
}
|
|
852
879
|
const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
|
|
853
880
|
function parseClaimDuration(value) {
|
|
854
881
|
const match = CLAIM_DURATION_PATTERN.exec(value.trim());
|
|
855
882
|
if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
|
|
856
883
|
const amount = Number(match[1]);
|
|
857
884
|
const seconds = match[2] === "s" ? amount : match[2] === "m" ? amount * 60 : amount * 3600;
|
|
858
|
-
if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
|
|
885
|
+
if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 86400)
|
|
886
|
+
throw new Error("Invalid --ttl. Use a duration between 1s and 24h");
|
|
859
887
|
return seconds;
|
|
860
888
|
}
|
|
861
889
|
function resolveClaimSessionId(explicit, options, env) {
|
|
@@ -864,7 +892,7 @@ function resolveClaimSessionId(explicit, options, env) {
|
|
|
864
892
|
function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
865
893
|
const parsed = parseCommandOperands(
|
|
866
894
|
args,
|
|
867
|
-
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
|
|
895
|
+
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
|
|
868
896
|
/* @__PURE__ */ new Map([
|
|
869
897
|
["--status", "--status"],
|
|
870
898
|
["--renew", "--renew"],
|
|
@@ -877,7 +905,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
877
905
|
if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
|
|
878
906
|
const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
|
|
879
907
|
const ttl = parsed.values.get("--ttl");
|
|
880
|
-
if (ttl !== void 0 && action !== "acquire" && action !== "renew")
|
|
908
|
+
if (ttl !== void 0 && action !== "acquire" && action !== "renew")
|
|
909
|
+
throw new Error("--ttl is only supported when acquiring or renewing a claim");
|
|
881
910
|
const note = parsed.values.get("--note");
|
|
882
911
|
if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
|
|
883
912
|
const generationValue = parsed.values.get("--generation");
|
|
@@ -885,22 +914,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
885
914
|
throw new Error("--generation is only supported with --renew or --release");
|
|
886
915
|
}
|
|
887
916
|
const generation = generationValue === void 0 ? void 0 : Number(generationValue);
|
|
888
|
-
if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
|
|
917
|
+
if (generation !== void 0 && (!Number.isSafeInteger(generation) || generation < 1))
|
|
918
|
+
throw new Error("--generation must be a positive integer");
|
|
889
919
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
920
|
+
const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
|
|
890
921
|
return {
|
|
891
922
|
action,
|
|
892
923
|
topic,
|
|
893
924
|
...sessionId ? { sessionId } : {},
|
|
925
|
+
...scope ? { scope } : {},
|
|
894
926
|
...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
|
|
895
927
|
...note !== void 0 ? { note } : {},
|
|
896
928
|
...generation !== void 0 ? { generation } : {}
|
|
897
929
|
};
|
|
898
930
|
}
|
|
899
931
|
function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
|
|
900
|
-
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session"]), /* @__PURE__ */ new Map(), "session claims");
|
|
932
|
+
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "session claims");
|
|
901
933
|
if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
|
|
902
934
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
903
|
-
|
|
935
|
+
const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
|
|
936
|
+
return { ...sessionId ? { sessionId } : {}, ...scope ? { scope } : {} };
|
|
937
|
+
}
|
|
938
|
+
function parseCheckoutPathArgs(args, options = {}, env = process.env) {
|
|
939
|
+
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--session", "--project", "--branch"]), /* @__PURE__ */ new Map(), "checkout path");
|
|
940
|
+
if (parsed.positionals.length > 0) throw new Error(`Unexpected checkout path argument: ${parsed.positionals[0]}`);
|
|
941
|
+
const projectId = parsed.values.get("--project")?.trim() || options.project?.trim();
|
|
942
|
+
const branchName = parsed.values.get("--branch")?.trim() || options.branch?.trim();
|
|
943
|
+
if (!projectId) throw new Error("--project/-p is required for `checkout path`");
|
|
944
|
+
if (!branchName) throw new Error("--branch/-b is required for `checkout path`");
|
|
945
|
+
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
946
|
+
return { projectId, branchName, ...sessionId ? { sessionId } : {} };
|
|
947
|
+
}
|
|
948
|
+
class R5dctlClaimScopeMismatchError extends Error {
|
|
949
|
+
constructor(requested, answered) {
|
|
950
|
+
super(
|
|
951
|
+
`The server did not apply the requested claim scope ${requested.projectId}#${requested.branchName}; it answered for ${answered.projectPath ?? answered.projectId}#${answered.branchName}. Update the r5d server before using scoped claims`
|
|
952
|
+
);
|
|
953
|
+
this.name = "R5dctlClaimScopeMismatchError";
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function claimScopeMatches(requested, answered) {
|
|
957
|
+
if (!requested || !answered) return true;
|
|
958
|
+
const projectMatches = answered.projectId === requested.projectId || answered.projectPath === requested.projectId;
|
|
959
|
+
return projectMatches && answered.branchName === requested.branchName;
|
|
960
|
+
}
|
|
961
|
+
function assertClaimScopeEcho(requested, answered) {
|
|
962
|
+
if (!claimScopeMatches(requested, answered)) throw new R5dctlClaimScopeMismatchError(requested, answered);
|
|
904
963
|
}
|
|
905
964
|
const CLAIM_OUTCOME_TEXT = {
|
|
906
965
|
acquired: "acquired",
|
|
@@ -913,35 +972,55 @@ const CLAIM_OUTCOME_TEXT = {
|
|
|
913
972
|
released: "released"
|
|
914
973
|
};
|
|
915
974
|
function formatClaimExpiry(expiresAt, nowMs) {
|
|
975
|
+
if (expiresAt === null) return "while the holder runs (implicit)";
|
|
916
976
|
const expiresAtMs = Date.parse(expiresAt);
|
|
917
977
|
if (!Number.isFinite(expiresAtMs)) return expiresAt;
|
|
918
978
|
return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
|
|
919
979
|
}
|
|
980
|
+
function describeClaimScope(scope) {
|
|
981
|
+
return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
|
|
982
|
+
}
|
|
920
983
|
function describeClaimHolder(claim) {
|
|
921
984
|
const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
|
|
922
|
-
|
|
985
|
+
const self = claim.heldByActor ? " (this session)" : "";
|
|
986
|
+
let origin = "";
|
|
987
|
+
if (claim.holderBranchName !== void 0 && claim.holderBranchName !== null) {
|
|
988
|
+
const native = claim.holderProjectId === claim.projectId && claim.holderBranchName === claim.branchName;
|
|
989
|
+
if (native) origin = claim.implicit ? " (native, running)" : " (native)";
|
|
990
|
+
else if (claim.holderProjectId === claim.projectId) origin = ` (foreign: native to branch ${claim.holderBranchName})`;
|
|
991
|
+
else origin = ` (foreign: native to ${claim.holderProjectId ?? "unknown"}#${claim.holderBranchName})`;
|
|
992
|
+
}
|
|
993
|
+
return `${claim.holderSessionId}${name}${self}${origin}`;
|
|
994
|
+
}
|
|
995
|
+
function describeClaimFence(response) {
|
|
996
|
+
if (response.fenceExtended === void 0) return void 0;
|
|
997
|
+
if (response.fenceExtended)
|
|
998
|
+
return "Fence: commands this session starts while holding the claim also fence the claimed checkout on the bound worker";
|
|
999
|
+
return `Fence: claim acquired, but ${response.fenceDetail ?? "the bound worker does not fence sibling checkouts (update the worker)"}; the claim still coordinates between sessions`;
|
|
923
1000
|
}
|
|
924
1001
|
function renderSessionClaimResponse(response, nowMs = Date.now()) {
|
|
925
1002
|
const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
|
|
926
1003
|
if (response.claim) {
|
|
927
1004
|
lines.push(
|
|
928
1005
|
`Claim: ${response.claim.topic}`,
|
|
929
|
-
`
|
|
1006
|
+
`Scope: ${describeClaimScope(response.claim)}`,
|
|
930
1007
|
`Holder: ${describeClaimHolder(response.claim)}`,
|
|
931
1008
|
`Generation: ${response.claim.generation}`,
|
|
932
1009
|
`Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
|
|
933
1010
|
);
|
|
934
1011
|
if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
|
|
935
1012
|
}
|
|
1013
|
+
const fence = describeClaimFence(response);
|
|
1014
|
+
if (fence) lines.push(fence);
|
|
936
1015
|
return `${lines.join("\n")}
|
|
937
1016
|
`;
|
|
938
1017
|
}
|
|
939
1018
|
function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
940
|
-
const header = `Claims on ${response
|
|
1019
|
+
const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
|
|
941
1020
|
if (response.claims.length === 0) return `${header}: none
|
|
942
1021
|
`;
|
|
943
1022
|
const rows = response.claims.map((claim) => {
|
|
944
|
-
const expiry = claim.expired ? "expired" : "expires";
|
|
1023
|
+
const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
|
|
945
1024
|
const note = claim.note ? ` note: ${claim.note}` : "";
|
|
946
1025
|
return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
|
|
947
1026
|
});
|
|
@@ -949,6 +1028,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
|
949
1028
|
${rows.join("\n")}
|
|
950
1029
|
`;
|
|
951
1030
|
}
|
|
1031
|
+
function renderCheckoutPath(response) {
|
|
1032
|
+
return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
|
|
1033
|
+
"\n"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
952
1036
|
function assertAuthLoginArgs(args) {
|
|
953
1037
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
954
1038
|
const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
|
|
@@ -1790,6 +1874,12 @@ function renderWorkspaceStatus(status) {
|
|
|
1790
1874
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1791
1875
|
...status.workers.flatMap((worker) => [
|
|
1792
1876
|
`Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
|
|
1877
|
+
...worker.hydrationRecovery ? [
|
|
1878
|
+
` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
|
|
1879
|
+
` Evidence: ${worker.hydrationRecovery.transactionPath}`,
|
|
1880
|
+
...(worker.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`),
|
|
1881
|
+
` After stopping active processes: r5dctl workspace recover-hydration --worker ${JSON.stringify(worker.workerLabel)} --transaction ${worker.hydrationRecovery.transactionId} --confirm`
|
|
1882
|
+
] : [],
|
|
1793
1883
|
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
|
|
1794
1884
|
...renderWorkerCapacity(worker.workerLabel, worker.capacity)
|
|
1795
1885
|
])
|
|
@@ -1797,6 +1887,15 @@ function renderWorkspaceStatus(status) {
|
|
|
1797
1887
|
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
1798
1888
|
lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
|
|
1799
1889
|
}
|
|
1890
|
+
if (status.latestSync?.hydrationRecovery && !status.workers.some((worker) => worker.workerLabel === status.latestSync.workerLabel)) {
|
|
1891
|
+
lines.push(
|
|
1892
|
+
`Disconnected worker ${status.latestSync.workerLabel} reported interrupted hydration ${status.latestSync.hydrationRecovery.transactionId}; preserved evidence: ${status.latestSync.hydrationRecovery.transactionPath}. Reconnect it, inspect workspace status, and preserve the transaction before resuming work.`,
|
|
1893
|
+
...(status.latestSync.hydrationRecovery.retainedPaths ?? []).map((retainedPath) => ` Retained writer entry: ${retainedPath}`)
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
for (const [mountId, skip] of Object.entries(status.latestSync?.mountSkipReasons ?? {})) {
|
|
1897
|
+
for (const retainedPath of skip.retainedPaths ?? []) lines.push(`Retained writer entry for ${mountId}: ${retainedPath}`);
|
|
1898
|
+
}
|
|
1800
1899
|
return `${lines.join("\n")}
|
|
1801
1900
|
`;
|
|
1802
1901
|
}
|
|
@@ -2083,11 +2182,17 @@ function renderSessionRunStart(session) {
|
|
|
2083
2182
|
lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
|
|
2084
2183
|
const sessionUrl = responseString(session, "sessionUrl");
|
|
2085
2184
|
if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
|
|
2185
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2086
2186
|
const sessionId = responseString(session, "sessionId");
|
|
2087
2187
|
if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
|
|
2088
2188
|
return `${lines.join("\n")}
|
|
2089
2189
|
`;
|
|
2090
2190
|
}
|
|
2191
|
+
function formatSessionWarningLines(session) {
|
|
2192
|
+
const warnings = session.warnings;
|
|
2193
|
+
if (!Array.isArray(warnings)) return [];
|
|
2194
|
+
return warnings.filter((warning) => typeof warning === "string" && warning.length > 0).map((warning) => `Warning: ${warning}`);
|
|
2195
|
+
}
|
|
2091
2196
|
function formatBranchStateHint(branch) {
|
|
2092
2197
|
const target = branch ?? "<branch>";
|
|
2093
2198
|
const commands = [
|
|
@@ -2138,6 +2243,7 @@ function renderSessionRunStatus(session) {
|
|
|
2138
2243
|
}
|
|
2139
2244
|
const childSessions = formatChildSessionsLine(session);
|
|
2140
2245
|
if (childSessions) lines.push(childSessions);
|
|
2246
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2141
2247
|
lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
|
|
2142
2248
|
const error = responseString(session, "error");
|
|
2143
2249
|
if (error) lines.push(`Error: ${error}`);
|
|
@@ -2764,6 +2870,23 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2764
2870
|
write(result, renderWorkspaceSync(result));
|
|
2765
2871
|
return;
|
|
2766
2872
|
}
|
|
2873
|
+
if (first === "workspace" && second === "recover-hydration") {
|
|
2874
|
+
let worker;
|
|
2875
|
+
let transactionId;
|
|
2876
|
+
let confirmed = false;
|
|
2877
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
2878
|
+
const arg = args[index];
|
|
2879
|
+
if (arg === "--worker") worker = requireValue(args[++index], "Missing worker label");
|
|
2880
|
+
else if (arg === "--transaction") transactionId = requireValue(args[++index], "Missing transaction id");
|
|
2881
|
+
else if (arg === "--confirm") confirmed = true;
|
|
2882
|
+
else throw new Error(`Unexpected recovery argument: ${arg}`);
|
|
2883
|
+
}
|
|
2884
|
+
if (!worker || !transactionId || !confirmed)
|
|
2885
|
+
throw new Error("workspace recover-hydration requires --worker, --transaction and --confirm; inspect workspace status first");
|
|
2886
|
+
const result = await client.workspace.recoverHydration({ worker, transactionId, confirmed: true });
|
|
2887
|
+
write(result, renderWorkspaceSync(result));
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2767
2890
|
if (first === "workspace" && second === "reset") {
|
|
2768
2891
|
const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
|
|
2769
2892
|
const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
|
|
@@ -2794,7 +2917,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2794
2917
|
}
|
|
2795
2918
|
if (first === "session" && second === "claim") {
|
|
2796
2919
|
const claim = parseSessionClaimArgs(args.slice(2), {}, {});
|
|
2797
|
-
const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
|
|
2920
|
+
const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
|
|
2798
2921
|
const nowMs = Date.now();
|
|
2799
2922
|
if (claim.action === "acquire") {
|
|
2800
2923
|
const result2 = await client.claims.acquire({
|
|
@@ -2803,23 +2926,34 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2803
2926
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
|
|
2804
2927
|
...claim.note !== void 0 ? { note: claim.note } : {}
|
|
2805
2928
|
});
|
|
2929
|
+
if (!claimScopeMatches(claim.scope, result2.claim)) {
|
|
2930
|
+
if (result2.outcome === "acquired" && result2.claim.heldByActor) {
|
|
2931
|
+
await client.claims.release({
|
|
2932
|
+
topic: claim.topic,
|
|
2933
|
+
...claim.sessionId ? { sessionId: claim.sessionId } : {},
|
|
2934
|
+
generation: result2.claim.generation
|
|
2935
|
+
}).catch(() => void 0);
|
|
2936
|
+
}
|
|
2937
|
+
throw new R5dctlClaimScopeMismatchError(claim.scope, result2.claim);
|
|
2938
|
+
}
|
|
2806
2939
|
write(result2, renderSessionClaimResponse(result2, nowMs));
|
|
2807
2940
|
if (result2.outcome === "held_by_other") {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
);
|
|
2941
|
+
const until = result2.claim.expiresAt === null ? "while it runs (native session)" : `until ${result2.claim.expiresAt}`;
|
|
2942
|
+
throw new Error(`Claim ${claim.topic} is held by session ${result2.claim.holderSessionId} ${until}; not acquired`);
|
|
2811
2943
|
}
|
|
2812
2944
|
return;
|
|
2813
2945
|
}
|
|
2814
2946
|
if (claim.action === "status") {
|
|
2815
2947
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
2948
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
2816
2949
|
write(list, renderSessionClaimList(list, nowMs));
|
|
2817
2950
|
return;
|
|
2818
2951
|
}
|
|
2819
2952
|
let generation = claim.generation;
|
|
2820
|
-
if (generation === void 0) {
|
|
2953
|
+
if (generation === void 0 || claim.scope) {
|
|
2821
2954
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
2822
|
-
|
|
2955
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
2956
|
+
const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
|
|
2823
2957
|
if (!current) {
|
|
2824
2958
|
write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
|
|
2825
2959
|
`);
|
|
@@ -2833,7 +2967,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2833
2967
|
);
|
|
2834
2968
|
throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
|
|
2835
2969
|
}
|
|
2836
|
-
generation
|
|
2970
|
+
generation ??= current.generation;
|
|
2837
2971
|
}
|
|
2838
2972
|
const result = claim.action === "renew" ? await client.claims.renew({
|
|
2839
2973
|
topic: claim.topic,
|
|
@@ -2841,16 +2975,25 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2841
2975
|
generation,
|
|
2842
2976
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
|
|
2843
2977
|
}) : await client.claims.release({ topic: claim.topic, ...scope, generation });
|
|
2978
|
+
assertClaimScopeEcho(claim.scope, result.claim);
|
|
2844
2979
|
write(result, renderSessionClaimResponse(result, nowMs));
|
|
2845
2980
|
const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
|
|
2846
2981
|
if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
|
|
2847
2982
|
return;
|
|
2848
2983
|
}
|
|
2849
2984
|
if (first === "session" && second === "claims") {
|
|
2850
|
-
const
|
|
2985
|
+
const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
|
|
2986
|
+
const list = await client.claims.list(selection);
|
|
2987
|
+
assertClaimScopeEcho(selection.scope, list);
|
|
2851
2988
|
write(list, renderSessionClaimList(list));
|
|
2852
2989
|
return;
|
|
2853
2990
|
}
|
|
2991
|
+
if (first === "checkout" && second === "path") {
|
|
2992
|
+
const selection = parseCheckoutPathArgs(args.slice(2), {}, {});
|
|
2993
|
+
const result = await client.checkouts.path(selection);
|
|
2994
|
+
write(result, renderCheckoutPath(result));
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2854
2997
|
if (first === "session" && second === "process-log") {
|
|
2855
2998
|
const selection = parseSessionProcessLogArgs(args.slice(2));
|
|
2856
2999
|
const log = await client.processes.log(selection.runId, {
|
|
@@ -2950,7 +3093,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
|
|
|
2950
3093
|
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
2951
3094
|
}
|
|
2952
3095
|
if (json) write({ results: deletion.results }, "");
|
|
2953
|
-
if (deletion.results.some((result) => !result.deleted && !result.accepted))
|
|
3096
|
+
if (deletion.results.some((result) => !result.deleted && !result.accepted))
|
|
3097
|
+
throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
2954
3098
|
return;
|
|
2955
3099
|
}
|
|
2956
3100
|
if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
|
|
@@ -3547,24 +3691,43 @@ function resolveCommandExecution(options, rest) {
|
|
|
3547
3691
|
...parseShellArgs(commandArgs)
|
|
3548
3692
|
};
|
|
3549
3693
|
}
|
|
3694
|
+
if (command === "checkout") {
|
|
3695
|
+
if (commandArgs[0] !== "path") throw new Error("Unknown checkout command");
|
|
3696
|
+
const parsed = parseCheckoutPathArgs(commandArgs.slice(1), options);
|
|
3697
|
+
return {
|
|
3698
|
+
kind: "plugin",
|
|
3699
|
+
pluginArgs: [
|
|
3700
|
+
"checkout",
|
|
3701
|
+
"path",
|
|
3702
|
+
`--project=${parsed.projectId}`,
|
|
3703
|
+
`--branch=${parsed.branchName}`,
|
|
3704
|
+
...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
|
|
3705
|
+
]
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3550
3708
|
if (command === "session") {
|
|
3551
3709
|
if (commandArgs[0] === "claim" || commandArgs[0] === "claims") {
|
|
3552
|
-
if (options.project) throw new Error("--project/-p is not supported for `session claim`; the acting session selects the project branch");
|
|
3553
|
-
if (options.branch) throw new Error("--branch/-b is not supported for `session claim`; the acting session selects the branch");
|
|
3554
3710
|
const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
|
|
3555
|
-
const
|
|
3711
|
+
const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
|
|
3712
|
+
const withoutNormalizedValues = [];
|
|
3556
3713
|
for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
|
|
3557
3714
|
const arg = commandArgs.slice(1)[index];
|
|
3558
|
-
if (arg
|
|
3715
|
+
if (normalizedFlags.has(arg)) {
|
|
3559
3716
|
index += 1;
|
|
3560
3717
|
continue;
|
|
3561
3718
|
}
|
|
3562
|
-
if (arg.startsWith(
|
|
3563
|
-
|
|
3719
|
+
if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
|
|
3720
|
+
withoutNormalizedValues.push(arg);
|
|
3564
3721
|
}
|
|
3565
3722
|
return {
|
|
3566
3723
|
kind: "plugin",
|
|
3567
|
-
pluginArgs: [
|
|
3724
|
+
pluginArgs: [
|
|
3725
|
+
"session",
|
|
3726
|
+
commandArgs[0],
|
|
3727
|
+
...withoutNormalizedValues,
|
|
3728
|
+
...parsed.scope ? [`--project=${parsed.scope.projectId}`, `--branch=${parsed.scope.branchName}`] : [],
|
|
3729
|
+
...parsed.sessionId ? [`--session=${parsed.sessionId}`] : []
|
|
3730
|
+
]
|
|
3568
3731
|
};
|
|
3569
3732
|
}
|
|
3570
3733
|
if (commandArgs[0] === "process-log") {
|
|
@@ -3737,10 +3900,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3737
3900
|
export {
|
|
3738
3901
|
BRANCH_DELETION_POLL_INTERVAL_MS,
|
|
3739
3902
|
BRANCH_DELETION_WAIT_TIMEOUT_MS,
|
|
3903
|
+
R5dctlClaimScopeMismatchError,
|
|
3740
3904
|
advanceTransientDevicePollBackoff,
|
|
3741
3905
|
branchDeletionConfirmationMessage,
|
|
3742
3906
|
branchDeletionsConfirmationMessage,
|
|
3743
3907
|
branchDeletionsFailureMessage,
|
|
3908
|
+
claimScopeMatches,
|
|
3744
3909
|
createBranchThroughPlatform,
|
|
3745
3910
|
deleteBranchThroughPlatform,
|
|
3746
3911
|
deleteBranchesThroughPlatform,
|
|
@@ -3760,6 +3925,7 @@ export {
|
|
|
3760
3925
|
parseAnswerQuestionsCommandArgs,
|
|
3761
3926
|
parseBranchCreateArgs,
|
|
3762
3927
|
parseBranchDeleteArgs,
|
|
3928
|
+
parseCheckoutPathArgs,
|
|
3763
3929
|
parseClaimDuration,
|
|
3764
3930
|
parseConversationForkArgs,
|
|
3765
3931
|
parseConversationInspectWorkArgs,
|
|
@@ -3785,6 +3951,7 @@ export {
|
|
|
3785
3951
|
renderBranchDeletionStatus,
|
|
3786
3952
|
renderBranchList,
|
|
3787
3953
|
renderBranchOperation,
|
|
3954
|
+
renderCheckoutPath,
|
|
3788
3955
|
renderConversationForkResponse,
|
|
3789
3956
|
renderConversationHeadResponse,
|
|
3790
3957
|
renderConversationNodeResponse,
|
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.133",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/cjs/cli.cjs",
|
|
6
6
|
"module": "./dist/mjs/cli.mjs",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"r5dctl": "dist/cjs/main.cjs"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ricsam/r5d-api": "^0.0.
|
|
29
|
+
"@ricsam/r5d-api": "^0.0.133",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"qrcode": "^1.5.4",
|
|
32
32
|
"ws": "^8.18.3"
|