@ricsam/r5dctl 0.0.131 → 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/README.md +3 -0
- package/dist/cjs/cli.cjs +234 -33
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +229 -33
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +46 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -136,6 +136,7 @@ r5dctl session status <session-id>
|
|
|
136
136
|
r5dctl session prompt --worker <label> --mode agent --model max --delivery steer <session-id> "Also check the migration"
|
|
137
137
|
r5dctl session prompt --worker <label> --mode agent --model max --delivery force <session-id> "Stop that and fix the build first"
|
|
138
138
|
r5dctl session stop <session-id>
|
|
139
|
+
r5dctl session set-worker <session-id> --worker <label>
|
|
139
140
|
r5dctl session process-log <run-id> --tail 200
|
|
140
141
|
r5dctl session process-log <run-id> --grep "error|failed"
|
|
141
142
|
|
|
@@ -169,6 +170,8 @@ Delivery is not acknowledgment. If a merge or shared-checkout change depends on
|
|
|
169
170
|
|
|
170
171
|
`session stop` also stops every sub-agent session the stopped session started, directly or through its own children, and `session status` prints `Child sessions: <active> active of <total>` for a session that launched sub-agents.
|
|
171
172
|
|
|
173
|
+
`session set-worker` rebinds a stopped session to another connected worker, exactly like the composer's worker selector: it publishes the current worker's workspace, synchronizes the target worker, persists the binding, and the session resumes there on its next prompt or resume. It is refused with `409` while the session's agent runs and with `403` for a worker credential. A running project agent moves its own session with the `switch_worker` tool instead, which needs no stop; workspace chats are rebound only while stopped. Shell runs, r5dctl shells, browser port forwards, and sub-agents started earlier stay on the worker that started them.
|
|
174
|
+
|
|
172
175
|
`session status` reports the run lifecycle and the launch-time `Source` and `Working tree` facts only. It deliberately prints no commit hash for the worktree, not even the launch baseline: a hash would be stale by the time a caller acts on it, because commits can land between the read and the action. Find the base and the branch's commits with git in the worker checkout instead, for example `git merge-base <your-branch> <branch>`, `git log --oneline <your-branch>..<branch>`, and `git status` in the branch folder, as the `Branch state` line reminds you. While a new worktree is still provisioning, a `Provisioning` line reports where its creation stands in the worker's operation queue and since when (`Provisioning: queued behind 3 worker operations since 2026-09-03T11:41:30Z`); a worker that predates operation receipts leaves that line out.
|
|
173
176
|
|
|
174
177
|
`session process-log` prints a shell run's stored stdout and stderr from the server, where r5d keeps every run's full output; the `logPath` in tool results names that server-side store, not a file on the worker. Without flags it prints the whole log, `--tail <n>` keeps the last n lines, and `--grep <pattern>` keeps the lines matching a JavaScript regular expression (combine both for the last n matches). Each stream is capped at 512 KB per call and printed under a `==> stdout <==` or `==> stderr <==` header; a one-line summary with byte totals, the selection, and any truncation goes to stderr so stdout stays clean for `grep`, `head`, and `tail`. Inside an r5d worker shell the temporary credential can only read runs on that shell's worker.
|
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,
|
|
@@ -95,6 +99,7 @@ __export(cli_exports, {
|
|
|
95
99
|
renderSessionList: () => renderSessionList,
|
|
96
100
|
renderSessionRunStart: () => renderSessionRunStart,
|
|
97
101
|
renderSessionRunStatus: () => renderSessionRunStatus,
|
|
102
|
+
renderSessionSetWorker: () => renderSessionSetWorker,
|
|
98
103
|
renderWorkerCapacity: () => renderWorkerCapacity,
|
|
99
104
|
renderWorkspaceStatus: () => renderWorkspaceStatus,
|
|
100
105
|
renderWorkspaceSync: () => renderWorkspaceSync,
|
|
@@ -175,10 +180,12 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
175
180
|
" r5dctl session status <session-id>",
|
|
176
181
|
' r5dctl session prompt --worker <label> --mode <mode> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
|
|
177
182
|
" r5dctl session stop <session-id>",
|
|
183
|
+
" r5dctl session set-worker <session-id> --worker <label>",
|
|
178
184
|
" r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
|
|
179
|
-
" r5dctl [-s <session-id>] session claim <topic> [--ttl <duration>] [--note <text>]",
|
|
180
|
-
" r5dctl [-s <session-id>] session claim <topic> --status | --renew [--ttl <duration>] | --release",
|
|
181
|
-
" 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",
|
|
182
189
|
"",
|
|
183
190
|
"Create, inspect, resume, and stop ordinary agent sessions.",
|
|
184
191
|
"prompt reaches a busy session by --delivery: steer (default) lands after the agent's next tool result,",
|
|
@@ -191,12 +198,19 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
191
198
|
"on a pause, wait for the recipient's explicit reply before proceeding.",
|
|
192
199
|
"claim takes atomic ownership of a topic within the session's project branch (one winner; a losing call prints the holder and expiry).",
|
|
193
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.",
|
|
194
206
|
"Agent shells act as their own session; user credentials select the acting session with -s or R5D_SESSION_ID.",
|
|
195
207
|
"A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
|
|
196
208
|
"--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
|
|
197
209
|
"Use -f/--follow/--watch to attach after a start; Ctrl-C detaches without stopping the session.",
|
|
198
210
|
"process-log prints a shell run's stored stdout/stderr from the server: the whole log, its last n lines,",
|
|
199
211
|
"or the lines matching a JavaScript regular expression. The log is not a file on the worker.",
|
|
212
|
+
"set-worker rebinds a stopped session to another connected worker: it publishes the current worker's workspace,",
|
|
213
|
+
"synchronizes the target, and the session resumes there. A running agent moves itself with its switch_worker tool.",
|
|
200
214
|
""
|
|
201
215
|
].join("\n");
|
|
202
216
|
const SHARED_HELP_ENTRIES = [
|
|
@@ -211,6 +225,11 @@ const SHARED_HELP_ENTRIES = [
|
|
|
211
225
|
{ section: "auth", usage: "auth api-key list", description: "List API keys for the current account." },
|
|
212
226
|
{ section: "auth", usage: "auth api-key revoke <key-id>", description: "Revoke an API key." },
|
|
213
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
|
+
},
|
|
214
233
|
{
|
|
215
234
|
section: "workspace",
|
|
216
235
|
usage: "workspace sync [--worker <label>]",
|
|
@@ -337,13 +356,23 @@ const SHARED_HELP_ENTRIES = [
|
|
|
337
356
|
},
|
|
338
357
|
{
|
|
339
358
|
section: "sessions",
|
|
340
|
-
usage: "
|
|
341
|
-
description: "
|
|
359
|
+
usage: "session set-worker <session-id> --worker <label>",
|
|
360
|
+
description: "Rebind a stopped session to another connected worker: publish the current worker's workspace, synchronize the target, and resume there. Refused while the agent runs; a running agent switches itself with switch_worker."
|
|
342
361
|
},
|
|
343
362
|
{
|
|
344
363
|
section: "sessions",
|
|
345
|
-
usage: "[-s <session-id>] session
|
|
346
|
-
description: "
|
|
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."
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
section: "sessions",
|
|
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."
|
|
347
376
|
},
|
|
348
377
|
{
|
|
349
378
|
section: "sessions",
|
|
@@ -903,6 +932,12 @@ function parseSessionRunCommand(options, args, env = process.env) {
|
|
|
903
932
|
if (parsed.positionals.length > 1) throw new Error(`Unexpected session ${subcommand} argument: ${parsed.positionals[1]}`);
|
|
904
933
|
return { kind: subcommand, sessionId };
|
|
905
934
|
}
|
|
935
|
+
if (subcommand === "set-worker") {
|
|
936
|
+
const parsed = parseCommandOperands(commandArgs, /* @__PURE__ */ new Set(["--worker"]), /* @__PURE__ */ new Map(), "session set-worker");
|
|
937
|
+
const sessionId = requireValue(parsed.positionals[0], "Session id is required for `session set-worker`");
|
|
938
|
+
if (parsed.positionals.length > 1) throw new Error(`Unexpected session set-worker argument: ${parsed.positionals[1]}`);
|
|
939
|
+
return { kind: "set-worker", sessionId, worker: requireWorker(parsed.values) };
|
|
940
|
+
}
|
|
906
941
|
if (subcommand === "prompt") {
|
|
907
942
|
const parsed = parseCommandOperands(
|
|
908
943
|
commandArgs,
|
|
@@ -940,13 +975,25 @@ function parseSessionProcessLogArgs(args) {
|
|
|
940
975
|
const grep = parsed.values.get("--grep");
|
|
941
976
|
return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
|
|
942
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
|
+
}
|
|
943
989
|
const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
|
|
944
990
|
function parseClaimDuration(value) {
|
|
945
991
|
const match = CLAIM_DURATION_PATTERN.exec(value.trim());
|
|
946
992
|
if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
|
|
947
993
|
const amount = Number(match[1]);
|
|
948
994
|
const seconds = match[2] === "s" ? amount : match[2] === "m" ? amount * 60 : amount * 3600;
|
|
949
|
-
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");
|
|
950
997
|
return seconds;
|
|
951
998
|
}
|
|
952
999
|
function resolveClaimSessionId(explicit, options, env) {
|
|
@@ -955,7 +1002,7 @@ function resolveClaimSessionId(explicit, options, env) {
|
|
|
955
1002
|
function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
956
1003
|
const parsed = parseCommandOperands(
|
|
957
1004
|
args,
|
|
958
|
-
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
|
|
1005
|
+
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
|
|
959
1006
|
/* @__PURE__ */ new Map([
|
|
960
1007
|
["--status", "--status"],
|
|
961
1008
|
["--renew", "--renew"],
|
|
@@ -968,7 +1015,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
968
1015
|
if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
|
|
969
1016
|
const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
|
|
970
1017
|
const ttl = parsed.values.get("--ttl");
|
|
971
|
-
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");
|
|
972
1020
|
const note = parsed.values.get("--note");
|
|
973
1021
|
if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
|
|
974
1022
|
const generationValue = parsed.values.get("--generation");
|
|
@@ -976,22 +1024,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
976
1024
|
throw new Error("--generation is only supported with --renew or --release");
|
|
977
1025
|
}
|
|
978
1026
|
const generation = generationValue === void 0 ? void 0 : Number(generationValue);
|
|
979
|
-
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");
|
|
980
1029
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
1030
|
+
const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
|
|
981
1031
|
return {
|
|
982
1032
|
action,
|
|
983
1033
|
topic,
|
|
984
1034
|
...sessionId ? { sessionId } : {},
|
|
1035
|
+
...scope ? { scope } : {},
|
|
985
1036
|
...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
|
|
986
1037
|
...note !== void 0 ? { note } : {},
|
|
987
1038
|
...generation !== void 0 ? { generation } : {}
|
|
988
1039
|
};
|
|
989
1040
|
}
|
|
990
1041
|
function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
|
|
991
|
-
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");
|
|
992
1043
|
if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
|
|
993
1044
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
994
|
-
|
|
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);
|
|
995
1073
|
}
|
|
996
1074
|
const CLAIM_OUTCOME_TEXT = {
|
|
997
1075
|
acquired: "acquired",
|
|
@@ -1004,35 +1082,55 @@ const CLAIM_OUTCOME_TEXT = {
|
|
|
1004
1082
|
released: "released"
|
|
1005
1083
|
};
|
|
1006
1084
|
function formatClaimExpiry(expiresAt, nowMs) {
|
|
1085
|
+
if (expiresAt === null) return "while the holder runs (implicit)";
|
|
1007
1086
|
const expiresAtMs = Date.parse(expiresAt);
|
|
1008
1087
|
if (!Number.isFinite(expiresAtMs)) return expiresAt;
|
|
1009
1088
|
return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
|
|
1010
1089
|
}
|
|
1090
|
+
function describeClaimScope(scope) {
|
|
1091
|
+
return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
|
|
1092
|
+
}
|
|
1011
1093
|
function describeClaimHolder(claim) {
|
|
1012
1094
|
const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
|
|
1013
|
-
|
|
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`;
|
|
1014
1110
|
}
|
|
1015
1111
|
function renderSessionClaimResponse(response, nowMs = Date.now()) {
|
|
1016
1112
|
const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
|
|
1017
1113
|
if (response.claim) {
|
|
1018
1114
|
lines.push(
|
|
1019
1115
|
`Claim: ${response.claim.topic}`,
|
|
1020
|
-
`
|
|
1116
|
+
`Scope: ${describeClaimScope(response.claim)}`,
|
|
1021
1117
|
`Holder: ${describeClaimHolder(response.claim)}`,
|
|
1022
1118
|
`Generation: ${response.claim.generation}`,
|
|
1023
1119
|
`Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
|
|
1024
1120
|
);
|
|
1025
1121
|
if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
|
|
1026
1122
|
}
|
|
1123
|
+
const fence = describeClaimFence(response);
|
|
1124
|
+
if (fence) lines.push(fence);
|
|
1027
1125
|
return `${lines.join("\n")}
|
|
1028
1126
|
`;
|
|
1029
1127
|
}
|
|
1030
1128
|
function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
1031
|
-
const header = `Claims on ${response
|
|
1129
|
+
const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
|
|
1032
1130
|
if (response.claims.length === 0) return `${header}: none
|
|
1033
1131
|
`;
|
|
1034
1132
|
const rows = response.claims.map((claim) => {
|
|
1035
|
-
const expiry = claim.expired ? "expired" : "expires";
|
|
1133
|
+
const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
|
|
1036
1134
|
const note = claim.note ? ` note: ${claim.note}` : "";
|
|
1037
1135
|
return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
|
|
1038
1136
|
});
|
|
@@ -1040,6 +1138,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
|
1040
1138
|
${rows.join("\n")}
|
|
1041
1139
|
`;
|
|
1042
1140
|
}
|
|
1141
|
+
function renderCheckoutPath(response) {
|
|
1142
|
+
return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
|
|
1143
|
+
"\n"
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1043
1146
|
function assertAuthLoginArgs(args) {
|
|
1044
1147
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
1045
1148
|
const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
|
|
@@ -1881,6 +1984,12 @@ function renderWorkspaceStatus(status) {
|
|
|
1881
1984
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1882
1985
|
...status.workers.flatMap((worker) => [
|
|
1883
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
|
+
] : [],
|
|
1884
1993
|
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
|
|
1885
1994
|
...renderWorkerCapacity(worker.workerLabel, worker.capacity)
|
|
1886
1995
|
])
|
|
@@ -1888,6 +1997,15 @@ function renderWorkspaceStatus(status) {
|
|
|
1888
1997
|
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
1889
1998
|
lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
|
|
1890
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
|
+
}
|
|
1891
2009
|
return `${lines.join("\n")}
|
|
1892
2010
|
`;
|
|
1893
2011
|
}
|
|
@@ -2174,11 +2292,17 @@ function renderSessionRunStart(session) {
|
|
|
2174
2292
|
lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
|
|
2175
2293
|
const sessionUrl = responseString(session, "sessionUrl");
|
|
2176
2294
|
if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
|
|
2295
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2177
2296
|
const sessionId = responseString(session, "sessionId");
|
|
2178
2297
|
if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
|
|
2179
2298
|
return `${lines.join("\n")}
|
|
2180
2299
|
`;
|
|
2181
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
|
+
}
|
|
2182
2306
|
function formatBranchStateHint(branch) {
|
|
2183
2307
|
const target = branch ?? "<branch>";
|
|
2184
2308
|
const commands = [
|
|
@@ -2229,6 +2353,7 @@ function renderSessionRunStatus(session) {
|
|
|
2229
2353
|
}
|
|
2230
2354
|
const childSessions = formatChildSessionsLine(session);
|
|
2231
2355
|
if (childSessions) lines.push(childSessions);
|
|
2356
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2232
2357
|
lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
|
|
2233
2358
|
const error = responseString(session, "error");
|
|
2234
2359
|
if (error) lines.push(`Error: ${error}`);
|
|
@@ -2279,9 +2404,23 @@ async function dispatchSessionRunCommand(client, command) {
|
|
|
2279
2404
|
});
|
|
2280
2405
|
return { data: data2, human: renderSessionRunStatus(data2) };
|
|
2281
2406
|
}
|
|
2407
|
+
if (command.kind === "set-worker") {
|
|
2408
|
+
const data2 = await client.sessions.setWorker(command.sessionId, { worker: command.worker });
|
|
2409
|
+
return { data: data2, human: renderSessionSetWorker(data2) };
|
|
2410
|
+
}
|
|
2282
2411
|
const data = await client.sessions.stop(command.sessionId);
|
|
2283
2412
|
return { data, human: renderSessionRunStatus(data) };
|
|
2284
2413
|
}
|
|
2414
|
+
function renderSessionSetWorker(response) {
|
|
2415
|
+
const previous = typeof response.previousWorker === "string" && response.previousWorker.length > 0 ? response.previousWorker : null;
|
|
2416
|
+
const lines = [
|
|
2417
|
+
`Session: ${responseString(response, "sessionId") ?? "(unknown)"}`,
|
|
2418
|
+
`Worker: ${responseString(response, "worker") ?? "(unassigned)"}${previous ? ` (was ${previous})` : ""}`,
|
|
2419
|
+
"The session resumes on this worker; shell runs and sub-agents started earlier stay where they ran."
|
|
2420
|
+
];
|
|
2421
|
+
return `${lines.join("\n")}
|
|
2422
|
+
`;
|
|
2423
|
+
}
|
|
2285
2424
|
async function waitForSessionProvisioning(client, initial, options = {}) {
|
|
2286
2425
|
const sessionId = requireValue(responseString(initial, "sessionId"), "Session start did not return a session id");
|
|
2287
2426
|
const wait = options.wait ?? import_promises.setTimeout;
|
|
@@ -2841,6 +2980,23 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2841
2980
|
write(result, renderWorkspaceSync(result));
|
|
2842
2981
|
return;
|
|
2843
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
|
+
}
|
|
2844
3000
|
if (first === "workspace" && second === "reset") {
|
|
2845
3001
|
const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
|
|
2846
3002
|
const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
|
|
@@ -2871,7 +3027,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2871
3027
|
}
|
|
2872
3028
|
if (first === "session" && second === "claim") {
|
|
2873
3029
|
const claim = parseSessionClaimArgs(args.slice(2), {}, {});
|
|
2874
|
-
const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
|
|
3030
|
+
const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
|
|
2875
3031
|
const nowMs = Date.now();
|
|
2876
3032
|
if (claim.action === "acquire") {
|
|
2877
3033
|
const result2 = await client.claims.acquire({
|
|
@@ -2880,23 +3036,34 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2880
3036
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
|
|
2881
3037
|
...claim.note !== void 0 ? { note: claim.note } : {}
|
|
2882
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
|
+
}
|
|
2883
3049
|
write(result2, renderSessionClaimResponse(result2, nowMs));
|
|
2884
3050
|
if (result2.outcome === "held_by_other") {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
);
|
|
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`);
|
|
2888
3053
|
}
|
|
2889
3054
|
return;
|
|
2890
3055
|
}
|
|
2891
3056
|
if (claim.action === "status") {
|
|
2892
3057
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
3058
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
2893
3059
|
write(list, renderSessionClaimList(list, nowMs));
|
|
2894
3060
|
return;
|
|
2895
3061
|
}
|
|
2896
3062
|
let generation = claim.generation;
|
|
2897
|
-
if (generation === void 0) {
|
|
3063
|
+
if (generation === void 0 || claim.scope) {
|
|
2898
3064
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
2899
|
-
|
|
3065
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
3066
|
+
const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
|
|
2900
3067
|
if (!current) {
|
|
2901
3068
|
write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
|
|
2902
3069
|
`);
|
|
@@ -2910,7 +3077,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2910
3077
|
);
|
|
2911
3078
|
throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
|
|
2912
3079
|
}
|
|
2913
|
-
generation
|
|
3080
|
+
generation ??= current.generation;
|
|
2914
3081
|
}
|
|
2915
3082
|
const result = claim.action === "renew" ? await client.claims.renew({
|
|
2916
3083
|
topic: claim.topic,
|
|
@@ -2918,16 +3085,25 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2918
3085
|
generation,
|
|
2919
3086
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
|
|
2920
3087
|
}) : await client.claims.release({ topic: claim.topic, ...scope, generation });
|
|
3088
|
+
assertClaimScopeEcho(claim.scope, result.claim);
|
|
2921
3089
|
write(result, renderSessionClaimResponse(result, nowMs));
|
|
2922
3090
|
const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
|
|
2923
3091
|
if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
|
|
2924
3092
|
return;
|
|
2925
3093
|
}
|
|
2926
3094
|
if (first === "session" && second === "claims") {
|
|
2927
|
-
const
|
|
3095
|
+
const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
|
|
3096
|
+
const list = await client.claims.list(selection);
|
|
3097
|
+
assertClaimScopeEcho(selection.scope, list);
|
|
2928
3098
|
write(list, renderSessionClaimList(list));
|
|
2929
3099
|
return;
|
|
2930
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
|
+
}
|
|
2931
3107
|
if (first === "session" && second === "process-log") {
|
|
2932
3108
|
const selection = parseSessionProcessLogArgs(args.slice(2));
|
|
2933
3109
|
const log = await client.processes.log(selection.runId, {
|
|
@@ -3027,7 +3203,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
|
|
|
3027
3203
|
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
3028
3204
|
}
|
|
3029
3205
|
if (json) write({ results: deletion.results }, "");
|
|
3030
|
-
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));
|
|
3031
3208
|
return;
|
|
3032
3209
|
}
|
|
3033
3210
|
if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
|
|
@@ -3624,24 +3801,43 @@ function resolveCommandExecution(options, rest) {
|
|
|
3624
3801
|
...parseShellArgs(commandArgs)
|
|
3625
3802
|
};
|
|
3626
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
|
+
}
|
|
3627
3818
|
if (command === "session") {
|
|
3628
3819
|
if (commandArgs[0] === "claim" || commandArgs[0] === "claims") {
|
|
3629
|
-
if (options.project) throw new Error("--project/-p is not supported for `session claim`; the acting session selects the project branch");
|
|
3630
|
-
if (options.branch) throw new Error("--branch/-b is not supported for `session claim`; the acting session selects the branch");
|
|
3631
3820
|
const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
|
|
3632
|
-
const
|
|
3821
|
+
const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
|
|
3822
|
+
const withoutNormalizedValues = [];
|
|
3633
3823
|
for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
|
|
3634
3824
|
const arg = commandArgs.slice(1)[index];
|
|
3635
|
-
if (arg
|
|
3825
|
+
if (normalizedFlags.has(arg)) {
|
|
3636
3826
|
index += 1;
|
|
3637
3827
|
continue;
|
|
3638
3828
|
}
|
|
3639
|
-
if (arg.startsWith(
|
|
3640
|
-
|
|
3829
|
+
if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
|
|
3830
|
+
withoutNormalizedValues.push(arg);
|
|
3641
3831
|
}
|
|
3642
3832
|
return {
|
|
3643
3833
|
kind: "plugin",
|
|
3644
|
-
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
|
+
]
|
|
3645
3841
|
};
|
|
3646
3842
|
}
|
|
3647
3843
|
if (commandArgs[0] === "process-log") {
|
|
@@ -3815,10 +4011,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3815
4011
|
0 && (module.exports = {
|
|
3816
4012
|
BRANCH_DELETION_POLL_INTERVAL_MS,
|
|
3817
4013
|
BRANCH_DELETION_WAIT_TIMEOUT_MS,
|
|
4014
|
+
R5dctlClaimScopeMismatchError,
|
|
3818
4015
|
advanceTransientDevicePollBackoff,
|
|
3819
4016
|
branchDeletionConfirmationMessage,
|
|
3820
4017
|
branchDeletionsConfirmationMessage,
|
|
3821
4018
|
branchDeletionsFailureMessage,
|
|
4019
|
+
claimScopeMatches,
|
|
3822
4020
|
createBranchThroughPlatform,
|
|
3823
4021
|
deleteBranchThroughPlatform,
|
|
3824
4022
|
deleteBranchesThroughPlatform,
|
|
@@ -3838,6 +4036,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3838
4036
|
parseAnswerQuestionsCommandArgs,
|
|
3839
4037
|
parseBranchCreateArgs,
|
|
3840
4038
|
parseBranchDeleteArgs,
|
|
4039
|
+
parseCheckoutPathArgs,
|
|
3841
4040
|
parseClaimDuration,
|
|
3842
4041
|
parseConversationForkArgs,
|
|
3843
4042
|
parseConversationInspectWorkArgs,
|
|
@@ -3863,6 +4062,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3863
4062
|
renderBranchDeletionStatus,
|
|
3864
4063
|
renderBranchList,
|
|
3865
4064
|
renderBranchOperation,
|
|
4065
|
+
renderCheckoutPath,
|
|
3866
4066
|
renderConversationForkResponse,
|
|
3867
4067
|
renderConversationHeadResponse,
|
|
3868
4068
|
renderConversationNodeResponse,
|
|
@@ -3880,6 +4080,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3880
4080
|
renderSessionList,
|
|
3881
4081
|
renderSessionRunStart,
|
|
3882
4082
|
renderSessionRunStatus,
|
|
4083
|
+
renderSessionSetWorker,
|
|
3883
4084
|
renderWorkerCapacity,
|
|
3884
4085
|
renderWorkspaceStatus,
|
|
3885
4086
|
renderWorkspaceSync,
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/cli.mjs
CHANGED
|
@@ -70,10 +70,12 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
70
70
|
" r5dctl session status <session-id>",
|
|
71
71
|
' r5dctl session prompt --worker <label> --mode <mode> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
|
|
72
72
|
" r5dctl session stop <session-id>",
|
|
73
|
+
" r5dctl session set-worker <session-id> --worker <label>",
|
|
73
74
|
" r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
|
|
74
|
-
" r5dctl [-s <session-id>] session claim <topic> [--ttl <duration>] [--note <text>]",
|
|
75
|
-
" r5dctl [-s <session-id>] session claim <topic> --status | --renew [--ttl <duration>] | --release",
|
|
76
|
-
" 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",
|
|
77
79
|
"",
|
|
78
80
|
"Create, inspect, resume, and stop ordinary agent sessions.",
|
|
79
81
|
"prompt reaches a busy session by --delivery: steer (default) lands after the agent's next tool result,",
|
|
@@ -86,12 +88,19 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
86
88
|
"on a pause, wait for the recipient's explicit reply before proceeding.",
|
|
87
89
|
"claim takes atomic ownership of a topic within the session's project branch (one winner; a losing call prints the holder and expiry).",
|
|
88
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.",
|
|
89
96
|
"Agent shells act as their own session; user credentials select the acting session with -s or R5D_SESSION_ID.",
|
|
90
97
|
"A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
|
|
91
98
|
"--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
|
|
92
99
|
"Use -f/--follow/--watch to attach after a start; Ctrl-C detaches without stopping the session.",
|
|
93
100
|
"process-log prints a shell run's stored stdout/stderr from the server: the whole log, its last n lines,",
|
|
94
101
|
"or the lines matching a JavaScript regular expression. The log is not a file on the worker.",
|
|
102
|
+
"set-worker rebinds a stopped session to another connected worker: it publishes the current worker's workspace,",
|
|
103
|
+
"synchronizes the target, and the session resumes there. A running agent moves itself with its switch_worker tool.",
|
|
95
104
|
""
|
|
96
105
|
].join("\n");
|
|
97
106
|
const SHARED_HELP_ENTRIES = [
|
|
@@ -106,6 +115,11 @@ const SHARED_HELP_ENTRIES = [
|
|
|
106
115
|
{ section: "auth", usage: "auth api-key list", description: "List API keys for the current account." },
|
|
107
116
|
{ section: "auth", usage: "auth api-key revoke <key-id>", description: "Revoke an API key." },
|
|
108
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
|
+
},
|
|
109
123
|
{
|
|
110
124
|
section: "workspace",
|
|
111
125
|
usage: "workspace sync [--worker <label>]",
|
|
@@ -232,13 +246,23 @@ const SHARED_HELP_ENTRIES = [
|
|
|
232
246
|
},
|
|
233
247
|
{
|
|
234
248
|
section: "sessions",
|
|
235
|
-
usage: "
|
|
236
|
-
description: "
|
|
249
|
+
usage: "session set-worker <session-id> --worker <label>",
|
|
250
|
+
description: "Rebind a stopped session to another connected worker: publish the current worker's workspace, synchronize the target, and resume there. Refused while the agent runs; a running agent switches itself with switch_worker."
|
|
237
251
|
},
|
|
238
252
|
{
|
|
239
253
|
section: "sessions",
|
|
240
|
-
usage: "[-s <session-id>] session
|
|
241
|
-
description: "
|
|
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."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
section: "sessions",
|
|
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."
|
|
242
266
|
},
|
|
243
267
|
{
|
|
244
268
|
section: "sessions",
|
|
@@ -798,6 +822,12 @@ function parseSessionRunCommand(options, args, env = process.env) {
|
|
|
798
822
|
if (parsed.positionals.length > 1) throw new Error(`Unexpected session ${subcommand} argument: ${parsed.positionals[1]}`);
|
|
799
823
|
return { kind: subcommand, sessionId };
|
|
800
824
|
}
|
|
825
|
+
if (subcommand === "set-worker") {
|
|
826
|
+
const parsed = parseCommandOperands(commandArgs, /* @__PURE__ */ new Set(["--worker"]), /* @__PURE__ */ new Map(), "session set-worker");
|
|
827
|
+
const sessionId = requireValue(parsed.positionals[0], "Session id is required for `session set-worker`");
|
|
828
|
+
if (parsed.positionals.length > 1) throw new Error(`Unexpected session set-worker argument: ${parsed.positionals[1]}`);
|
|
829
|
+
return { kind: "set-worker", sessionId, worker: requireWorker(parsed.values) };
|
|
830
|
+
}
|
|
801
831
|
if (subcommand === "prompt") {
|
|
802
832
|
const parsed = parseCommandOperands(
|
|
803
833
|
commandArgs,
|
|
@@ -835,13 +865,25 @@ function parseSessionProcessLogArgs(args) {
|
|
|
835
865
|
const grep = parsed.values.get("--grep");
|
|
836
866
|
return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
|
|
837
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
|
+
}
|
|
838
879
|
const CLAIM_DURATION_PATTERN = /^(\d+)([smh])$/;
|
|
839
880
|
function parseClaimDuration(value) {
|
|
840
881
|
const match = CLAIM_DURATION_PATTERN.exec(value.trim());
|
|
841
882
|
if (!match) throw new Error("Invalid --ttl. Expected a duration such as 90s, 30m, or 2h");
|
|
842
883
|
const amount = Number(match[1]);
|
|
843
884
|
const seconds = match[2] === "s" ? amount : match[2] === "m" ? amount * 60 : amount * 3600;
|
|
844
|
-
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");
|
|
845
887
|
return seconds;
|
|
846
888
|
}
|
|
847
889
|
function resolveClaimSessionId(explicit, options, env) {
|
|
@@ -850,7 +892,7 @@ function resolveClaimSessionId(explicit, options, env) {
|
|
|
850
892
|
function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
851
893
|
const parsed = parseCommandOperands(
|
|
852
894
|
args,
|
|
853
|
-
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session"]),
|
|
895
|
+
/* @__PURE__ */ new Set(["--ttl", "--note", "--generation", "--session", "--project", "--branch"]),
|
|
854
896
|
/* @__PURE__ */ new Map([
|
|
855
897
|
["--status", "--status"],
|
|
856
898
|
["--renew", "--renew"],
|
|
@@ -863,7 +905,8 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
863
905
|
if (parsed.booleans.size > 1) throw new Error("Use only one of --status, --renew, or --release");
|
|
864
906
|
const action = parsed.booleans.has("--status") ? "status" : parsed.booleans.has("--renew") ? "renew" : parsed.booleans.has("--release") ? "release" : "acquire";
|
|
865
907
|
const ttl = parsed.values.get("--ttl");
|
|
866
|
-
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");
|
|
867
910
|
const note = parsed.values.get("--note");
|
|
868
911
|
if (note !== void 0 && action !== "acquire") throw new Error("--note is only supported when acquiring a claim");
|
|
869
912
|
const generationValue = parsed.values.get("--generation");
|
|
@@ -871,22 +914,52 @@ function parseSessionClaimArgs(args, options = {}, env = process.env) {
|
|
|
871
914
|
throw new Error("--generation is only supported with --renew or --release");
|
|
872
915
|
}
|
|
873
916
|
const generation = generationValue === void 0 ? void 0 : Number(generationValue);
|
|
874
|
-
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");
|
|
875
919
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
920
|
+
const scope = resolveClaimScope(parsed.values.get("--project"), parsed.values.get("--branch"), options);
|
|
876
921
|
return {
|
|
877
922
|
action,
|
|
878
923
|
topic,
|
|
879
924
|
...sessionId ? { sessionId } : {},
|
|
925
|
+
...scope ? { scope } : {},
|
|
880
926
|
...ttl !== void 0 ? { ttlSeconds: parseClaimDuration(ttl) } : {},
|
|
881
927
|
...note !== void 0 ? { note } : {},
|
|
882
928
|
...generation !== void 0 ? { generation } : {}
|
|
883
929
|
};
|
|
884
930
|
}
|
|
885
931
|
function parseSessionClaimsListArgs(args, options = {}, env = process.env) {
|
|
886
|
-
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");
|
|
887
933
|
if (parsed.positionals.length > 0) throw new Error(`Unexpected session claims argument: ${parsed.positionals[0]}`);
|
|
888
934
|
const sessionId = resolveClaimSessionId(parsed.values.get("--session"), options, env);
|
|
889
|
-
|
|
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);
|
|
890
963
|
}
|
|
891
964
|
const CLAIM_OUTCOME_TEXT = {
|
|
892
965
|
acquired: "acquired",
|
|
@@ -899,35 +972,55 @@ const CLAIM_OUTCOME_TEXT = {
|
|
|
899
972
|
released: "released"
|
|
900
973
|
};
|
|
901
974
|
function formatClaimExpiry(expiresAt, nowMs) {
|
|
975
|
+
if (expiresAt === null) return "while the holder runs (implicit)";
|
|
902
976
|
const expiresAtMs = Date.parse(expiresAt);
|
|
903
977
|
if (!Number.isFinite(expiresAtMs)) return expiresAt;
|
|
904
978
|
return expiresAtMs <= nowMs ? `${expiresAt} (expired ${formatProcessAge(expiresAt, nowMs)} ago)` : `${expiresAt} (in ${formatProcessAge(new Date(nowMs).toISOString(), expiresAtMs)})`;
|
|
905
979
|
}
|
|
980
|
+
function describeClaimScope(scope) {
|
|
981
|
+
return `${scope.projectPath ?? scope.projectId}#${scope.branchName}`;
|
|
982
|
+
}
|
|
906
983
|
function describeClaimHolder(claim) {
|
|
907
984
|
const name = claim.holderSessionName ? ` ${JSON.stringify(claim.holderSessionName)}` : "";
|
|
908
|
-
|
|
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`;
|
|
909
1000
|
}
|
|
910
1001
|
function renderSessionClaimResponse(response, nowMs = Date.now()) {
|
|
911
1002
|
const lines = [`Outcome: ${CLAIM_OUTCOME_TEXT[response.outcome] ?? response.outcome}`, `Acting session: ${response.actorSessionId}`];
|
|
912
1003
|
if (response.claim) {
|
|
913
1004
|
lines.push(
|
|
914
1005
|
`Claim: ${response.claim.topic}`,
|
|
915
|
-
`
|
|
1006
|
+
`Scope: ${describeClaimScope(response.claim)}`,
|
|
916
1007
|
`Holder: ${describeClaimHolder(response.claim)}`,
|
|
917
1008
|
`Generation: ${response.claim.generation}`,
|
|
918
1009
|
`Expires: ${formatClaimExpiry(response.claim.expiresAt, nowMs)}`
|
|
919
1010
|
);
|
|
920
1011
|
if (response.claim.note) lines.push(`Note: ${response.claim.note}`);
|
|
921
1012
|
}
|
|
1013
|
+
const fence = describeClaimFence(response);
|
|
1014
|
+
if (fence) lines.push(fence);
|
|
922
1015
|
return `${lines.join("\n")}
|
|
923
1016
|
`;
|
|
924
1017
|
}
|
|
925
1018
|
function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
926
|
-
const header = `Claims on ${response
|
|
1019
|
+
const header = `Claims on ${describeClaimScope(response)} (acting session ${response.actorSessionId})`;
|
|
927
1020
|
if (response.claims.length === 0) return `${header}: none
|
|
928
1021
|
`;
|
|
929
1022
|
const rows = response.claims.map((claim) => {
|
|
930
|
-
const expiry = claim.expired ? "expired" : "expires";
|
|
1023
|
+
const expiry = claim.expired ? "expired" : claim.expiresAt === null ? "held" : "expires";
|
|
931
1024
|
const note = claim.note ? ` note: ${claim.note}` : "";
|
|
932
1025
|
return `- ${claim.topic} holder ${describeClaimHolder(claim)} generation ${claim.generation} ${expiry} ${formatClaimExpiry(claim.expiresAt, nowMs)}${note}`;
|
|
933
1026
|
});
|
|
@@ -935,6 +1028,11 @@ function renderSessionClaimList(response, nowMs = Date.now()) {
|
|
|
935
1028
|
${rows.join("\n")}
|
|
936
1029
|
`;
|
|
937
1030
|
}
|
|
1031
|
+
function renderCheckoutPath(response) {
|
|
1032
|
+
return [`Checkout: ${response.projectPath}#${response.branchName}`, `Worker: ${response.worker}`, `Path: ${response.path}`, ""].join(
|
|
1033
|
+
"\n"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
938
1036
|
function assertAuthLoginArgs(args) {
|
|
939
1037
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
940
1038
|
const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
|
|
@@ -1776,6 +1874,12 @@ function renderWorkspaceStatus(status) {
|
|
|
1776
1874
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1777
1875
|
...status.workers.flatMap((worker) => [
|
|
1778
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
|
+
] : [],
|
|
1779
1883
|
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
|
|
1780
1884
|
...renderWorkerCapacity(worker.workerLabel, worker.capacity)
|
|
1781
1885
|
])
|
|
@@ -1783,6 +1887,15 @@ function renderWorkspaceStatus(status) {
|
|
|
1783
1887
|
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
1784
1888
|
lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
|
|
1785
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
|
+
}
|
|
1786
1899
|
return `${lines.join("\n")}
|
|
1787
1900
|
`;
|
|
1788
1901
|
}
|
|
@@ -2069,11 +2182,17 @@ function renderSessionRunStart(session) {
|
|
|
2069
2182
|
lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
|
|
2070
2183
|
const sessionUrl = responseString(session, "sessionUrl");
|
|
2071
2184
|
if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
|
|
2185
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2072
2186
|
const sessionId = responseString(session, "sessionId");
|
|
2073
2187
|
if (sessionId) lines.push("", `Next: r5dctl session status ${sessionId}`);
|
|
2074
2188
|
return `${lines.join("\n")}
|
|
2075
2189
|
`;
|
|
2076
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
|
+
}
|
|
2077
2196
|
function formatBranchStateHint(branch) {
|
|
2078
2197
|
const target = branch ?? "<branch>";
|
|
2079
2198
|
const commands = [
|
|
@@ -2124,6 +2243,7 @@ function renderSessionRunStatus(session) {
|
|
|
2124
2243
|
}
|
|
2125
2244
|
const childSessions = formatChildSessionsLine(session);
|
|
2126
2245
|
if (childSessions) lines.push(childSessions);
|
|
2246
|
+
lines.push(...formatSessionWarningLines(session));
|
|
2127
2247
|
lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
|
|
2128
2248
|
const error = responseString(session, "error");
|
|
2129
2249
|
if (error) lines.push(`Error: ${error}`);
|
|
@@ -2174,9 +2294,23 @@ async function dispatchSessionRunCommand(client, command) {
|
|
|
2174
2294
|
});
|
|
2175
2295
|
return { data: data2, human: renderSessionRunStatus(data2) };
|
|
2176
2296
|
}
|
|
2297
|
+
if (command.kind === "set-worker") {
|
|
2298
|
+
const data2 = await client.sessions.setWorker(command.sessionId, { worker: command.worker });
|
|
2299
|
+
return { data: data2, human: renderSessionSetWorker(data2) };
|
|
2300
|
+
}
|
|
2177
2301
|
const data = await client.sessions.stop(command.sessionId);
|
|
2178
2302
|
return { data, human: renderSessionRunStatus(data) };
|
|
2179
2303
|
}
|
|
2304
|
+
function renderSessionSetWorker(response) {
|
|
2305
|
+
const previous = typeof response.previousWorker === "string" && response.previousWorker.length > 0 ? response.previousWorker : null;
|
|
2306
|
+
const lines = [
|
|
2307
|
+
`Session: ${responseString(response, "sessionId") ?? "(unknown)"}`,
|
|
2308
|
+
`Worker: ${responseString(response, "worker") ?? "(unassigned)"}${previous ? ` (was ${previous})` : ""}`,
|
|
2309
|
+
"The session resumes on this worker; shell runs and sub-agents started earlier stay where they ran."
|
|
2310
|
+
];
|
|
2311
|
+
return `${lines.join("\n")}
|
|
2312
|
+
`;
|
|
2313
|
+
}
|
|
2180
2314
|
async function waitForSessionProvisioning(client, initial, options = {}) {
|
|
2181
2315
|
const sessionId = requireValue(responseString(initial, "sessionId"), "Session start did not return a session id");
|
|
2182
2316
|
const wait = options.wait ?? sleep;
|
|
@@ -2736,6 +2870,23 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2736
2870
|
write(result, renderWorkspaceSync(result));
|
|
2737
2871
|
return;
|
|
2738
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
|
+
}
|
|
2739
2890
|
if (first === "workspace" && second === "reset") {
|
|
2740
2891
|
const parsed = parseWorkspaceWorkerArgs(args.slice(2), { allowConfirm: true });
|
|
2741
2892
|
const result = await client.workspace.reset({ worker: parsed.worker, confirmed: true });
|
|
@@ -2766,7 +2917,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2766
2917
|
}
|
|
2767
2918
|
if (first === "session" && second === "claim") {
|
|
2768
2919
|
const claim = parseSessionClaimArgs(args.slice(2), {}, {});
|
|
2769
|
-
const scope = claim.sessionId ? { sessionId: claim.sessionId } : {};
|
|
2920
|
+
const scope = { ...claim.sessionId ? { sessionId: claim.sessionId } : {}, ...claim.scope ? { scope: claim.scope } : {} };
|
|
2770
2921
|
const nowMs = Date.now();
|
|
2771
2922
|
if (claim.action === "acquire") {
|
|
2772
2923
|
const result2 = await client.claims.acquire({
|
|
@@ -2775,23 +2926,34 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2775
2926
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {},
|
|
2776
2927
|
...claim.note !== void 0 ? { note: claim.note } : {}
|
|
2777
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
|
+
}
|
|
2778
2939
|
write(result2, renderSessionClaimResponse(result2, nowMs));
|
|
2779
2940
|
if (result2.outcome === "held_by_other") {
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
);
|
|
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`);
|
|
2783
2943
|
}
|
|
2784
2944
|
return;
|
|
2785
2945
|
}
|
|
2786
2946
|
if (claim.action === "status") {
|
|
2787
2947
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
2948
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
2788
2949
|
write(list, renderSessionClaimList(list, nowMs));
|
|
2789
2950
|
return;
|
|
2790
2951
|
}
|
|
2791
2952
|
let generation = claim.generation;
|
|
2792
|
-
if (generation === void 0) {
|
|
2953
|
+
if (generation === void 0 || claim.scope) {
|
|
2793
2954
|
const list = await client.claims.list({ ...scope, topic: claim.topic });
|
|
2794
|
-
|
|
2955
|
+
assertClaimScopeEcho(claim.scope, list);
|
|
2956
|
+
const current = list.claims.find((entry) => entry.heldByActor) ?? list.claims[0];
|
|
2795
2957
|
if (!current) {
|
|
2796
2958
|
write({ outcome: "missing", actorSessionId: list.actorSessionId, claim: null }, `No claim on ${claim.topic}.
|
|
2797
2959
|
`);
|
|
@@ -2805,7 +2967,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2805
2967
|
);
|
|
2806
2968
|
throw new Error(`Claim ${claim.topic} is held by session ${current.holderSessionId}, not by ${list.actorSessionId}`);
|
|
2807
2969
|
}
|
|
2808
|
-
generation
|
|
2970
|
+
generation ??= current.generation;
|
|
2809
2971
|
}
|
|
2810
2972
|
const result = claim.action === "renew" ? await client.claims.renew({
|
|
2811
2973
|
topic: claim.topic,
|
|
@@ -2813,16 +2975,25 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2813
2975
|
generation,
|
|
2814
2976
|
...claim.ttlSeconds !== void 0 ? { ttlSeconds: claim.ttlSeconds } : {}
|
|
2815
2977
|
}) : await client.claims.release({ topic: claim.topic, ...scope, generation });
|
|
2978
|
+
assertClaimScopeEcho(claim.scope, result.claim);
|
|
2816
2979
|
write(result, renderSessionClaimResponse(result, nowMs));
|
|
2817
2980
|
const succeeded = result.outcome === "renewed" || result.outcome === "released" || claim.action === "release" && result.outcome === "missing";
|
|
2818
2981
|
if (!succeeded) throw new Error(`Claim ${claim.topic}: ${CLAIM_OUTCOME_TEXT[result.outcome] ?? result.outcome}`);
|
|
2819
2982
|
return;
|
|
2820
2983
|
}
|
|
2821
2984
|
if (first === "session" && second === "claims") {
|
|
2822
|
-
const
|
|
2985
|
+
const selection = parseSessionClaimsListArgs(args.slice(2), {}, {});
|
|
2986
|
+
const list = await client.claims.list(selection);
|
|
2987
|
+
assertClaimScopeEcho(selection.scope, list);
|
|
2823
2988
|
write(list, renderSessionClaimList(list));
|
|
2824
2989
|
return;
|
|
2825
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
|
+
}
|
|
2826
2997
|
if (first === "session" && second === "process-log") {
|
|
2827
2998
|
const selection = parseSessionProcessLogArgs(args.slice(2));
|
|
2828
2999
|
const log = await client.processes.log(selection.runId, {
|
|
@@ -2922,7 +3093,8 @@ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error
|
|
|
2922
3093
|
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
2923
3094
|
}
|
|
2924
3095
|
if (json) write({ results: deletion.results }, "");
|
|
2925
|
-
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));
|
|
2926
3098
|
return;
|
|
2927
3099
|
}
|
|
2928
3100
|
if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
|
|
@@ -3519,24 +3691,43 @@ function resolveCommandExecution(options, rest) {
|
|
|
3519
3691
|
...parseShellArgs(commandArgs)
|
|
3520
3692
|
};
|
|
3521
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
|
+
}
|
|
3522
3708
|
if (command === "session") {
|
|
3523
3709
|
if (commandArgs[0] === "claim" || commandArgs[0] === "claims") {
|
|
3524
|
-
if (options.project) throw new Error("--project/-p is not supported for `session claim`; the acting session selects the project branch");
|
|
3525
|
-
if (options.branch) throw new Error("--branch/-b is not supported for `session claim`; the acting session selects the branch");
|
|
3526
3710
|
const parsed = commandArgs[0] === "claim" ? parseSessionClaimArgs(commandArgs.slice(1), options) : parseSessionClaimsListArgs(commandArgs.slice(1), options);
|
|
3527
|
-
const
|
|
3711
|
+
const normalizedFlags = /* @__PURE__ */ new Set(["--session", "--project", "--branch"]);
|
|
3712
|
+
const withoutNormalizedValues = [];
|
|
3528
3713
|
for (let index = 0; index < commandArgs.slice(1).length; index += 1) {
|
|
3529
3714
|
const arg = commandArgs.slice(1)[index];
|
|
3530
|
-
if (arg
|
|
3715
|
+
if (normalizedFlags.has(arg)) {
|
|
3531
3716
|
index += 1;
|
|
3532
3717
|
continue;
|
|
3533
3718
|
}
|
|
3534
|
-
if (arg.startsWith(
|
|
3535
|
-
|
|
3719
|
+
if ([...normalizedFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
|
|
3720
|
+
withoutNormalizedValues.push(arg);
|
|
3536
3721
|
}
|
|
3537
3722
|
return {
|
|
3538
3723
|
kind: "plugin",
|
|
3539
|
-
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
|
+
]
|
|
3540
3731
|
};
|
|
3541
3732
|
}
|
|
3542
3733
|
if (commandArgs[0] === "process-log") {
|
|
@@ -3709,10 +3900,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3709
3900
|
export {
|
|
3710
3901
|
BRANCH_DELETION_POLL_INTERVAL_MS,
|
|
3711
3902
|
BRANCH_DELETION_WAIT_TIMEOUT_MS,
|
|
3903
|
+
R5dctlClaimScopeMismatchError,
|
|
3712
3904
|
advanceTransientDevicePollBackoff,
|
|
3713
3905
|
branchDeletionConfirmationMessage,
|
|
3714
3906
|
branchDeletionsConfirmationMessage,
|
|
3715
3907
|
branchDeletionsFailureMessage,
|
|
3908
|
+
claimScopeMatches,
|
|
3716
3909
|
createBranchThroughPlatform,
|
|
3717
3910
|
deleteBranchThroughPlatform,
|
|
3718
3911
|
deleteBranchesThroughPlatform,
|
|
@@ -3732,6 +3925,7 @@ export {
|
|
|
3732
3925
|
parseAnswerQuestionsCommandArgs,
|
|
3733
3926
|
parseBranchCreateArgs,
|
|
3734
3927
|
parseBranchDeleteArgs,
|
|
3928
|
+
parseCheckoutPathArgs,
|
|
3735
3929
|
parseClaimDuration,
|
|
3736
3930
|
parseConversationForkArgs,
|
|
3737
3931
|
parseConversationInspectWorkArgs,
|
|
@@ -3757,6 +3951,7 @@ export {
|
|
|
3757
3951
|
renderBranchDeletionStatus,
|
|
3758
3952
|
renderBranchList,
|
|
3759
3953
|
renderBranchOperation,
|
|
3954
|
+
renderCheckoutPath,
|
|
3760
3955
|
renderConversationForkResponse,
|
|
3761
3956
|
renderConversationHeadResponse,
|
|
3762
3957
|
renderConversationNodeResponse,
|
|
@@ -3774,6 +3969,7 @@ export {
|
|
|
3774
3969
|
renderSessionList,
|
|
3775
3970
|
renderSessionRunStart,
|
|
3776
3971
|
renderSessionRunStatus,
|
|
3972
|
+
renderSessionSetWorker,
|
|
3777
3973
|
renderWorkerCapacity,
|
|
3778
3974
|
renderWorkspaceStatus,
|
|
3779
3975
|
renderWorkspaceSync,
|
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;
|
|
@@ -84,6 +84,10 @@ export type R5dctlSessionRunCommand = R5dctlSessionStartCommand | {
|
|
|
84
84
|
} | {
|
|
85
85
|
kind: "stop";
|
|
86
86
|
sessionId: string;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "set-worker";
|
|
89
|
+
sessionId: string;
|
|
90
|
+
worker: string;
|
|
87
91
|
};
|
|
88
92
|
export type R5dctlBranchCreateArgs = {
|
|
89
93
|
source: R5dctlWorktreeSource;
|
|
@@ -205,23 +209,54 @@ export type R5dctlSessionClaimArgs = {
|
|
|
205
209
|
topic: string;
|
|
206
210
|
/** The acting session; omitted lets a worker credential act as its own verified session. */
|
|
207
211
|
sessionId?: string;
|
|
212
|
+
/** A sibling checkout to claim on instead of the acting session's own branch. */
|
|
213
|
+
scope?: R5dctlSessionClaimScope;
|
|
208
214
|
ttlSeconds?: number;
|
|
209
215
|
note?: string;
|
|
210
216
|
/** Claim generation observed by the caller; renew and release are fenced to it. */
|
|
211
217
|
generation?: number;
|
|
212
218
|
};
|
|
219
|
+
type ClaimScopeOptions = Pick<GlobalOptions, "session" | "project" | "branch">;
|
|
213
220
|
/** `90s`, `30m`, `2h`; the server clamps to between one minute and one day. */
|
|
214
221
|
export declare function parseClaimDuration(value: string): number;
|
|
215
|
-
export declare function parseSessionClaimArgs(args: string[], options?:
|
|
216
|
-
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>): {
|
|
224
|
+
sessionId?: string;
|
|
225
|
+
scope?: R5dctlSessionClaimScope;
|
|
226
|
+
};
|
|
227
|
+
export type R5dctlCheckoutPathArgs = {
|
|
228
|
+
projectId: string;
|
|
229
|
+
branchName: string;
|
|
217
230
|
sessionId?: string;
|
|
218
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;
|
|
219
251
|
export declare function renderSessionClaimResponse(response: {
|
|
220
252
|
outcome: string;
|
|
221
253
|
actorSessionId: string;
|
|
222
254
|
claim: R5dctlSessionClaim | null;
|
|
255
|
+
fenceExtended?: boolean;
|
|
256
|
+
fenceDetail?: string;
|
|
223
257
|
}, nowMs?: number): string;
|
|
224
258
|
export declare function renderSessionClaimList(response: R5dctlSessionClaimListResponse, nowMs?: number): string;
|
|
259
|
+
export declare function renderCheckoutPath(response: R5dctlCheckoutPathResponse): string;
|
|
225
260
|
export declare function parseGlobalArgs(argv: string[]): {
|
|
226
261
|
options: GlobalOptions;
|
|
227
262
|
rest: string[];
|
|
@@ -312,6 +347,10 @@ export type R5dctlCommandResponse = {
|
|
|
312
347
|
baselineCommit?: string | null;
|
|
313
348
|
workspaceHead?: string | null;
|
|
314
349
|
worker?: string;
|
|
350
|
+
/** The binding a `session set-worker` replaced; null when the session had none. */
|
|
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;
|
|
315
354
|
status?: string;
|
|
316
355
|
runStatus?: unknown;
|
|
317
356
|
/** Worker queue state while a new worktree provisions; older servers and workers omit it. */
|
|
@@ -347,6 +386,9 @@ export type R5dctlSessionRunClient = {
|
|
|
347
386
|
delivery?: R5dctlPromptDelivery;
|
|
348
387
|
}) => Promise<R5dctlCommandResponse>;
|
|
349
388
|
stop: (sessionId: string) => Promise<R5dctlCommandResponse>;
|
|
389
|
+
setWorker: (sessionId: string, input: {
|
|
390
|
+
worker: string;
|
|
391
|
+
}) => Promise<R5dctlCommandResponse>;
|
|
350
392
|
};
|
|
351
393
|
};
|
|
352
394
|
export declare function renderSessionRunStart(session: R5dctlCommandResponse): string;
|
|
@@ -356,6 +398,7 @@ export type R5dctlCommandDispatchResult = {
|
|
|
356
398
|
human: string;
|
|
357
399
|
};
|
|
358
400
|
export declare function dispatchSessionRunCommand(client: R5dctlSessionRunClient, command: R5dctlSessionRunCommand): Promise<R5dctlCommandDispatchResult>;
|
|
401
|
+
export declare function renderSessionSetWorker(response: R5dctlCommandResponse): string;
|
|
359
402
|
export declare function waitForSessionProvisioning(client: Pick<R5dctlSessionRunClient, "sessions">, initial: R5dctlCommandResponse, options?: {
|
|
360
403
|
intervalMs?: number;
|
|
361
404
|
wait?: (milliseconds: number) => Promise<unknown>;
|
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"
|