@ricsam/r5dctl 0.0.71 → 0.0.73
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 +15 -3
- package/dist/cjs/cli.cjs +171 -40
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +168 -39
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +32 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -71,10 +71,12 @@ r5dctl -s <session-id> conversation --system # include system prompt m
|
|
|
71
71
|
r5dctl -s <session-id> conversation --raw --tools --system # exact raw agent request transcript
|
|
72
72
|
r5dctl -s <session-id> conversation -f # print current transcript, then follow new output (--watch also works)
|
|
73
73
|
r5dctl conversation overview <session-id> # dialogue/thinking with tool work omitted
|
|
74
|
+
r5dctl conversation overview <session-id> --skeleton # user messages, decisions, and per-turn work markers only
|
|
74
75
|
r5dctl conversation inspect-node <session-id> <node-id> # raw persisted node plus traversal metadata
|
|
75
76
|
r5dctl conversation inspect-work <session-id> <work-id> # compact call list for one omitted turn
|
|
76
77
|
r5dctl conversation inspect-work <session-id> <work-id> --summary
|
|
77
|
-
r5dctl conversation inspect-work <session-id> <work-id> --full
|
|
78
|
+
r5dctl conversation inspect-work <session-id> <work-id> --full --calls 12-20
|
|
79
|
+
r5dctl conversation inspect-work <session-id> <work-id> --include-polls
|
|
78
80
|
r5dctl conversation move-head <session-id> <node-id> # rewind or replay the transcript; files stay put
|
|
79
81
|
r5dctl conversation fork <session-id> <node-id> # copy the branch up to a node into a new session
|
|
80
82
|
r5dctl conversation fork <session-id> <node-id> --name "Retry with tests"
|
|
@@ -91,6 +93,8 @@ r5dctl session stop <session-id>
|
|
|
91
93
|
r5dctl -p <project> merge fix/tests
|
|
92
94
|
r5dctl -p <project> merge --worker <label> --create-conflict-worktree fix/tests
|
|
93
95
|
r5dctl -p <project> merge --worker <label> --launch-conflict-resolution-agent fix/tests
|
|
96
|
+
r5dctl -p <project> merge --worker <label> --launch-conflict-resolution-agent --no-wait fix/tests
|
|
97
|
+
r5dctl -p <project> merge status <attempt-id>
|
|
94
98
|
r5dctl -p <project> merge finish --worker <label> conflicts/fix-tests --summary "Resolved the conflicts"
|
|
95
99
|
|
|
96
100
|
r5dctl -p <project> -b <branch> shell
|
|
@@ -107,12 +111,16 @@ r5dctl k8s usage --window 7d
|
|
|
107
111
|
r5dctl k8s usage --workload default/api --window 7d --json
|
|
108
112
|
```
|
|
109
113
|
|
|
110
|
-
`session start` creates an ordinary agent-mode chat on an explicitly selected existing worktree, or creates a named worktree from an explicit `worktree:<branch>` source. It requires project, worker, model, and prompt and never infers them from the current shell. Starts and prompts return after durable enqueue while execution continues asynchronously. Task specialization belongs in the prompt; there are no predefined debug, explore, research, or test agents.
|
|
114
|
+
`session start` creates an ordinary agent-mode chat on an explicitly selected existing worktree, or creates a named worktree from an explicit `worktree:<branch>` source. It requires project, worker, model, and prompt and never infers them from the current shell. Starts and prompts return after durable enqueue while execution continues asynchronously. Task specialization belongs in the prompt; there are no predefined debug, explore, research, or test agents. `session stop` returns as soon as the agent run is stopped; the stopped worktree's workspace publication finishes asynchronously behind the response, and `merge` republishes at its own boundary before reading the worktree.
|
|
111
115
|
|
|
112
116
|
Add `-f`/`--follow`/`--watch` to `session start` to attach after enqueue, or to `conversation` to print the current transcript and follow new output. Assistant text is written to stdout and lifecycle/tool progress to stderr; `--tools` includes tool calls and results. With global `--json`, follow mode emits one JSON event per line. Ctrl-C only detaches and never stops the session. The CLI reconnects transiently interrupted event streams and resynchronizes committed conversation revisions without repeating assistant text.
|
|
113
117
|
|
|
114
118
|
`merge` merges a managed source worktree into its recorded parent. By default conflicts are reported without creating more work. `--create-conflict-worktree` prepares a manual conflict worktree, while `--launch-conflict-resolution-agent` starts the dedicated merge-conflict resolver; those strategies are mutually exclusive. A worker-bound shell credential supplies its own worker automatically; otherwise pass `--worker`. Finish a reviewed manual resolution with `merge finish`.
|
|
115
119
|
|
|
120
|
+
Merging is lockless and optimistic: nothing blocks agents from continuing to work on the involved branches. Every merge pins the branch tips it previewed and publishes through an atomic compare-and-swap, and a concurrent publication simply retries. When a resolver agent finishes after the target advanced, the platform automatically continues the merge — publishing directly if the refreshed merge is conflict-free, or launching a fresh resolver attempt (whose prompt references the superseded session's resolution) — repeating until one resolution publishes. Superseded attempts surface the follow-up attempt id in `merge status`. Only manual conflict worktrees stop at `stale`: re-run `merge` to start a fresh resolution.
|
|
121
|
+
|
|
122
|
+
The server answers merge requests as soon as the result is durably recorded; the slow per-member workspace propagation always completes asynchronously behind the response. For a clean merge that means the merged commit is already published when the command returns. For a conflict strategy the server responds with the merge attempt in a `provisioning` state and prepares the conflict worktree (and starts the resolver agent) in the background; the CLI polls the attempt until the worktree is ready — and, for the agent strategy, until the resolver agent is running — before printing the final result. Pass `--no-wait` to skip the wait and get the attempt id immediately, and use `merge status <attempt-id>` at any time to re-check a provisioning, failed, or completed attempt (a provisioning failure surfaces there as a `failed` attempt with its reason). Ctrl-C during the wait only detaches the CLI; provisioning continues server-side.
|
|
123
|
+
|
|
116
124
|
`shell` connects to the same live worker PTY as the web Shell tab. Without `-c` it attaches the local terminal interactively. With `-c`/`--command`, it runs the supplied shell expression, streams combined PTY output, and exits with the remote command's status. Because command output uses a PTY, stdout and stderr are combined and programs may emit colors or other terminal control sequences.
|
|
117
125
|
|
|
118
126
|
`ps list` shows active long-running commands managed by r5d. It does not expose the worker host's general OS process table or interactive PTYs. Global project, branch, and session options filter the list, while `--worker` selects a worker label. `ps stop` sends `SIGTERM` to the selected run. Inside an r5d-provided worker shell, the temporary credential can only list and stop runs on that shell's worker.
|
|
@@ -132,7 +140,11 @@ Conversation output is human-readable by default and excludes both the system pr
|
|
|
132
140
|
|
|
133
141
|
When available, conversation rendering uses the newest complete persisted model-request snapshot on the active conversation branch, so its mode, tier, model, thinking mode, tools, and messages reflect the request that actually ran. Legacy or empty sessions without snapshots fall back to reconstruction from the latest conversation context.
|
|
134
142
|
|
|
135
|
-
`conversation overview` is a deterministic active-branch projection for recovering earlier work after a fresh-context boundary. It keeps user messages, injected context, assistant thinking/commentary/final responses, and user decisions, while replacing each turn's tool batches with a stable work ID and numbered call ranges. Adjacent tool batches with no visible content between them are coalesced into one range.
|
|
143
|
+
`conversation overview` is a deterministic active-branch projection for recovering earlier work after a fresh-context boundary. It keeps user messages, injected context, assistant thinking/commentary/final responses, and user decisions, while replacing each turn's tool batches with a stable work ID and numbered call ranges. Adjacent tool batches with no visible content between them are coalesced into one range. `--skeleton` drops thinking and commentary too, keeping only dialogue, questions/answers, terminal responses, and one work marker per turn — the fastest first read of a long session. When the last turn is unfinished, both views end with a `[Turn in progress: ...]` footer naming the pending state and last node, so "still running" is distinguishable from "ended without a response".
|
|
144
|
+
|
|
145
|
+
Use `conversation inspect-work` with that session and work ID to inspect the omitted calls. Inspection is compact by default; `--summary` adds bounded targets/outcomes and `--full` renders persisted inputs and results with the normal tool renderers. The explicit `--compact`, `--summary`, and `--full` flags are mutually exclusive. `--calls <n[-m]>` selects a server-side call range while keeping the original call numbers, so a `calls 83–90 omitted` marker in the overview can be inspected directly. Compact and summary output are designed to be narrowed with `grep`, `head`, `tail`, `sed`, or `cat -n`.
|
|
146
|
+
|
|
147
|
+
Persisted shell results are dispatch-time snapshots, so a long-running command's stored status can go stale. Rendering joins each unresolved run against the durable process-run store: summary rows gain `final_status=`/`final_exitCode=` next to the stale snapshot fields, `--full` appends a `## Final process outcomes` section with byte counts and output tails, and `shell_poll` calls whose run already finished are folded into one `[calls N–M folded: ...]` annotation carrying the final status. `--include-polls` restores the raw poll rows; call numbering never changes either way.
|
|
136
148
|
|
|
137
149
|
`conversation inspect-node` reads one exact persisted conversation node as formatted JSON. Its header includes the previous node, every direct next node, the next node on the active branch, whether the selected node is active, and the containing `work_...` ID when one is available. This allows callers that start with only a copied chat node ID to traverse nearby nodes or zoom out with `conversation inspect-work`.
|
|
138
150
|
|
package/dist/cjs/cli.cjs
CHANGED
|
@@ -42,13 +42,15 @@ __export(cli_exports, {
|
|
|
42
42
|
getR5dctlVersion: () => getR5dctlVersion,
|
|
43
43
|
getTransientDevicePollDelay: () => getTransientDevicePollDelay,
|
|
44
44
|
handleAuthLogin: () => handleAuthLogin,
|
|
45
|
+
isMergeAttemptSettled: () => isMergeAttemptSettled,
|
|
45
46
|
main: () => main,
|
|
46
47
|
parseAnswerEnvRequestCommandArgs: () => parseAnswerEnvRequestCommandArgs,
|
|
47
48
|
parseAnswerFlags: () => parseAnswerFlags,
|
|
48
49
|
parseAnswerQuestionsCommandArgs: () => parseAnswerQuestionsCommandArgs,
|
|
49
50
|
parseConversationForkArgs: () => parseConversationForkArgs,
|
|
51
|
+
parseConversationInspectWorkArgs: () => parseConversationInspectWorkArgs,
|
|
52
|
+
parseConversationOverviewArgs: () => parseConversationOverviewArgs,
|
|
50
53
|
parseConversationRenderArgs: () => parseConversationRenderArgs,
|
|
51
|
-
parseConversationWorkDetailArgs: () => parseConversationWorkDetailArgs,
|
|
52
54
|
parseEnvFlags: () => parseEnvFlags,
|
|
53
55
|
parseEnvRequestResponseArgs: () => parseEnvRequestResponseArgs,
|
|
54
56
|
parseGetEnvsArgs: () => parseGetEnvsArgs,
|
|
@@ -95,16 +97,8 @@ var import_promises = require("node:timers/promises");
|
|
|
95
97
|
var import_dotenv = require("dotenv");
|
|
96
98
|
var import_r5d_api = require("@ricsam/r5d-api");
|
|
97
99
|
var import_shell = require("./shell.cjs");
|
|
98
|
-
const CHAT_MODES = /* @__PURE__ */ new Set([
|
|
99
|
-
|
|
100
|
-
"plan",
|
|
101
|
-
"build",
|
|
102
|
-
"agent",
|
|
103
|
-
"security_review",
|
|
104
|
-
"large_diff_remediation",
|
|
105
|
-
"merge_conflict_resolution"
|
|
106
|
-
]);
|
|
107
|
-
const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "build", "agent"]);
|
|
100
|
+
const CHAT_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent", "large_diff_remediation", "merge_conflict_resolution"]);
|
|
101
|
+
const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent"]);
|
|
108
102
|
const MODEL_TIERS = /* @__PURE__ */ new Set(["low", "medium", "high", "max"]);
|
|
109
103
|
const R5DCTL_PACKAGE_NAME = "@ricsam/r5dctl";
|
|
110
104
|
const CLI_GLOBAL_OPTION_HELP = [
|
|
@@ -173,10 +167,12 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
173
167
|
].join("\n");
|
|
174
168
|
const MERGE_HELP_TEXT = [
|
|
175
169
|
"Usage:",
|
|
176
|
-
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
170
|
+
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
171
|
+
" r5dctl -p <project> merge status <attempt-id>",
|
|
177
172
|
' r5dctl -p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
178
173
|
"",
|
|
179
174
|
"Merge a managed worktree into its recorded parent worktree.",
|
|
175
|
+
"Conflict worktrees provision in the background; merge waits by default and --no-wait returns the attempt immediately.",
|
|
180
176
|
""
|
|
181
177
|
].join("\n");
|
|
182
178
|
const SHARED_HELP_ENTRIES = [
|
|
@@ -251,8 +247,8 @@ const SHARED_HELP_ENTRIES = [
|
|
|
251
247
|
},
|
|
252
248
|
{
|
|
253
249
|
section: "sessions",
|
|
254
|
-
usage: "conversation overview <session-id>",
|
|
255
|
-
description: "Read dialogue and thinking while replacing tool work with inspectable ranges."
|
|
250
|
+
usage: "conversation overview <session-id> [--skeleton]",
|
|
251
|
+
description: "Read dialogue and thinking while replacing tool work with inspectable ranges; --skeleton keeps only dialogue, decisions, and per-turn work markers."
|
|
256
252
|
},
|
|
257
253
|
{
|
|
258
254
|
section: "sessions",
|
|
@@ -261,8 +257,8 @@ const SHARED_HELP_ENTRIES = [
|
|
|
261
257
|
},
|
|
262
258
|
{
|
|
263
259
|
section: "sessions",
|
|
264
|
-
usage: "conversation inspect-work <session-id> <work-id> [--compact|--summary|--full]",
|
|
265
|
-
description: "Inspect one turn's omitted tool work at the selected detail."
|
|
260
|
+
usage: "conversation inspect-work <session-id> <work-id> [--compact|--summary|--full] [--calls <n[-m]>] [--include-polls]",
|
|
261
|
+
description: "Inspect one turn's omitted tool work at the selected detail; finished shell polls are folded unless --include-polls."
|
|
266
262
|
},
|
|
267
263
|
{
|
|
268
264
|
section: "sessions",
|
|
@@ -327,9 +323,14 @@ const SHARED_HELP_ENTRIES = [
|
|
|
327
323
|
{ section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
|
|
328
324
|
{
|
|
329
325
|
section: "merge",
|
|
330
|
-
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
326
|
+
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
331
327
|
description: "Merge a worktree into its recorded parent, with an explicit conflict strategy."
|
|
332
328
|
},
|
|
329
|
+
{
|
|
330
|
+
section: "merge",
|
|
331
|
+
usage: "-p <project> merge status <attempt-id>",
|
|
332
|
+
description: "Show a merge attempt's provisioning and resolution status."
|
|
333
|
+
},
|
|
333
334
|
{
|
|
334
335
|
section: "merge",
|
|
335
336
|
usage: '-p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
@@ -566,7 +567,7 @@ function requireModel(value) {
|
|
|
566
567
|
function requireSessionMode(value) {
|
|
567
568
|
const mode = requireValue(value?.trim(), "--mode <mode> is required");
|
|
568
569
|
if (!R5DCTL_SESSION_MODES.has(mode)) {
|
|
569
|
-
throw new Error("Invalid --mode. Expected one of: ask, plan,
|
|
570
|
+
throw new Error("Invalid --mode. Expected one of: ask, plan, agent");
|
|
570
571
|
}
|
|
571
572
|
return mode;
|
|
572
573
|
}
|
|
@@ -652,12 +653,19 @@ function parseMergeCommand(options, args) {
|
|
|
652
653
|
...parsed2.values.get("--summary")?.trim() ? { summary: parsed2.values.get("--summary").trim() } : {}
|
|
653
654
|
};
|
|
654
655
|
}
|
|
656
|
+
if (args[0] === "status") {
|
|
657
|
+
const parsed2 = parseCommandOperands(args.slice(1), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), "merge status");
|
|
658
|
+
const attemptId = requireValue(parsed2.positionals[0], "Attempt id is required for `merge status`");
|
|
659
|
+
if (parsed2.positionals.length > 1) throw new Error(`Unexpected merge status argument: ${parsed2.positionals[1]}`);
|
|
660
|
+
return { kind: "status", project, attemptId };
|
|
661
|
+
}
|
|
655
662
|
const parsed = parseCommandOperands(
|
|
656
663
|
args,
|
|
657
664
|
/* @__PURE__ */ new Set(["--worker"]),
|
|
658
665
|
/* @__PURE__ */ new Map([
|
|
659
666
|
["--create-conflict-worktree", "--create-conflict-worktree"],
|
|
660
|
-
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"]
|
|
667
|
+
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"],
|
|
668
|
+
["--no-wait", "--no-wait"]
|
|
661
669
|
]),
|
|
662
670
|
"merge"
|
|
663
671
|
);
|
|
@@ -670,7 +678,14 @@ function parseMergeCommand(options, args) {
|
|
|
670
678
|
}
|
|
671
679
|
const conflictMode = createWorktree ? "worktree" : launchAgent ? "agent" : "none";
|
|
672
680
|
const worker = parsed.values.get("--worker")?.trim();
|
|
673
|
-
return {
|
|
681
|
+
return {
|
|
682
|
+
kind: "merge",
|
|
683
|
+
project,
|
|
684
|
+
sourceWorktree,
|
|
685
|
+
conflictMode,
|
|
686
|
+
...worker ? { worker } : {},
|
|
687
|
+
...parsed.booleans.has("--no-wait") ? { noWait: true } : {}
|
|
688
|
+
};
|
|
674
689
|
}
|
|
675
690
|
function assertAuthLoginArgs(args) {
|
|
676
691
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
@@ -1958,17 +1973,22 @@ function renderSessionRunStatus(session) {
|
|
|
1958
1973
|
function renderWorktreeMerge(result, input) {
|
|
1959
1974
|
const lines = [];
|
|
1960
1975
|
appendMergeDetails(lines, result);
|
|
1976
|
+
const status = responseString(result, "status");
|
|
1961
1977
|
const conflictWorktree = responseString(result, "conflictWorktree");
|
|
1962
1978
|
const resolverSessionId = responseString(result, "resolverSessionId");
|
|
1979
|
+
const attemptId = responseString(result, "attemptId");
|
|
1963
1980
|
const worker = responseString(result, "worker") ?? input.worker;
|
|
1964
1981
|
if (resolverSessionId) lines.push("", `Resolver session: ${resolverSessionId}`, ` r5dctl session stop ${resolverSessionId}`);
|
|
1965
|
-
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(
|
|
1982
|
+
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(status ?? "")) {
|
|
1966
1983
|
lines.push(
|
|
1967
1984
|
"",
|
|
1968
1985
|
"Finish after resolving and reviewing the conflict worktree:",
|
|
1969
1986
|
` r5dctl -p ${input.project} merge finish --worker ${worker} ${conflictWorktree} --summary "<summary>"`
|
|
1970
1987
|
);
|
|
1971
1988
|
}
|
|
1989
|
+
if (attemptId && (status === "provisioning" || status === "resolving" && result.resolverAgentRunning === false)) {
|
|
1990
|
+
lines.push("", "Check progress:", ` r5dctl -p ${input.project} merge status ${attemptId}`);
|
|
1991
|
+
}
|
|
1972
1992
|
return `${lines.join("\n")}
|
|
1973
1993
|
`;
|
|
1974
1994
|
}
|
|
@@ -2008,13 +2028,64 @@ async function dispatchSessionRunCommand(client, command) {
|
|
|
2008
2028
|
const data = await client.sessions.stop(command.sessionId);
|
|
2009
2029
|
return { data, human: renderSessionRunStatus(data) };
|
|
2010
2030
|
}
|
|
2011
|
-
|
|
2031
|
+
const MERGE_POLL_INTERVAL_MS = 2e3;
|
|
2032
|
+
const MERGE_POLL_TIMEOUT_MS = 20 * 6e4;
|
|
2033
|
+
const MERGE_POLL_MAX_CONSECUTIVE_FAILURES = 5;
|
|
2034
|
+
function isMergeAttemptSettled(response) {
|
|
2035
|
+
const status = responseString(response, "status");
|
|
2036
|
+
if (!status) return true;
|
|
2037
|
+
if (status === "resolving") return response.resolverAgentRunning !== false;
|
|
2038
|
+
return status !== "provisioning";
|
|
2039
|
+
}
|
|
2040
|
+
async function pollMergeAttemptUntilSettled(client, project, attemptId, initial, options) {
|
|
2041
|
+
const intervalMs = options.pollIntervalMs ?? MERGE_POLL_INTERVAL_MS;
|
|
2042
|
+
const timeoutMs = options.pollTimeoutMs ?? MERGE_POLL_TIMEOUT_MS;
|
|
2043
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
2044
|
+
const onProgress = options.onProgress ?? (() => {
|
|
2045
|
+
});
|
|
2046
|
+
let latest = initial;
|
|
2047
|
+
let worktreeReadyReported = latest.worktreeReady === true;
|
|
2048
|
+
let consecutiveFailures = 0;
|
|
2049
|
+
let waitedMs = 0;
|
|
2050
|
+
while (!isMergeAttemptSettled(latest)) {
|
|
2051
|
+
if (waitedMs >= timeoutMs) {
|
|
2052
|
+
onProgress(`Still waiting after ${Math.round(timeoutMs / 6e4)} minutes; check later with: r5dctl -p ${project} merge status ${attemptId}`);
|
|
2053
|
+
return latest;
|
|
2054
|
+
}
|
|
2055
|
+
await sleep2(intervalMs);
|
|
2056
|
+
waitedMs += intervalMs;
|
|
2057
|
+
try {
|
|
2058
|
+
latest = await client.projects.worktrees.mergeAttempt(project, attemptId);
|
|
2059
|
+
consecutiveFailures = 0;
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
consecutiveFailures += 1;
|
|
2062
|
+
if (consecutiveFailures >= MERGE_POLL_MAX_CONSECUTIVE_FAILURES) throw error;
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (!worktreeReadyReported && latest.worktreeReady === true) {
|
|
2066
|
+
worktreeReadyReported = true;
|
|
2067
|
+
onProgress("Conflict worktree ready.");
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return latest;
|
|
2071
|
+
}
|
|
2072
|
+
async function dispatchMergeCommand(client, command, pollOptions = {}) {
|
|
2012
2073
|
if (command.kind === "merge") {
|
|
2013
2074
|
const data2 = await client.projects.worktrees.merge(command.project, command.sourceWorktree, {
|
|
2014
2075
|
conflictMode: command.conflictMode,
|
|
2015
2076
|
...command.worker ? { worker: command.worker } : {}
|
|
2016
2077
|
});
|
|
2017
|
-
|
|
2078
|
+
const attemptId = responseString(data2, "attemptId");
|
|
2079
|
+
if (command.noWait || !attemptId || isMergeAttemptSettled(data2)) {
|
|
2080
|
+
return { data: data2, human: renderWorktreeMerge(data2, command) };
|
|
2081
|
+
}
|
|
2082
|
+
pollOptions.onProgress?.(`Provisioning the conflict worktree (attempt ${attemptId})...`);
|
|
2083
|
+
const settled = await pollMergeAttemptUntilSettled(client, command.project, attemptId, data2, pollOptions);
|
|
2084
|
+
return { data: settled, human: renderWorktreeMerge(settled, command) };
|
|
2085
|
+
}
|
|
2086
|
+
if (command.kind === "status") {
|
|
2087
|
+
const data2 = await client.projects.worktrees.mergeAttempt(command.project, command.attemptId);
|
|
2088
|
+
return { data: data2, human: renderWorktreeMerge(data2, { project: command.project }) };
|
|
2018
2089
|
}
|
|
2019
2090
|
const data = await client.projects.worktrees.finishMerge(command.project, command.conflictWorktree, {
|
|
2020
2091
|
worker: command.worker,
|
|
@@ -2422,21 +2493,57 @@ async function followR5dctlSession(client, sessionId, options) {
|
|
|
2422
2493
|
}
|
|
2423
2494
|
return 0;
|
|
2424
2495
|
}
|
|
2425
|
-
function
|
|
2496
|
+
function parseConversationInspectWorkArgs(args) {
|
|
2426
2497
|
let detail = "compact";
|
|
2427
2498
|
let selected = false;
|
|
2428
|
-
|
|
2499
|
+
let calls;
|
|
2500
|
+
let includePolls = false;
|
|
2501
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2502
|
+
const arg = args[index];
|
|
2429
2503
|
const candidate = arg === "--compact" ? "compact" : arg === "--summary" ? "summary" : arg === "--full" ? "full" : void 0;
|
|
2430
|
-
if (
|
|
2431
|
-
|
|
2504
|
+
if (candidate) {
|
|
2505
|
+
if (selected) {
|
|
2506
|
+
throw new Error("Use only one of --compact, --summary, or --full");
|
|
2507
|
+
}
|
|
2508
|
+
detail = candidate;
|
|
2509
|
+
selected = true;
|
|
2510
|
+
continue;
|
|
2432
2511
|
}
|
|
2433
|
-
if (
|
|
2434
|
-
|
|
2512
|
+
if (arg === "--calls") {
|
|
2513
|
+
if (calls !== void 0) {
|
|
2514
|
+
throw new Error("Use --calls only once");
|
|
2515
|
+
}
|
|
2516
|
+
index += 1;
|
|
2517
|
+
const value = requireValue(args[index], "Missing value for --calls");
|
|
2518
|
+
if (!/^[0-9]+(-[0-9]+)?$/.test(value)) {
|
|
2519
|
+
throw new Error(`Invalid --calls range: ${value} (use N or N-M)`);
|
|
2520
|
+
}
|
|
2521
|
+
calls = value;
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
if (arg === "--include-polls") {
|
|
2525
|
+
if (includePolls) {
|
|
2526
|
+
throw new Error("Use --include-polls only once");
|
|
2527
|
+
}
|
|
2528
|
+
includePolls = true;
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
throw new Error(`Unknown inspect-work argument: ${arg}`);
|
|
2532
|
+
}
|
|
2533
|
+
return { detail, ...calls !== void 0 ? { calls } : {}, includePolls };
|
|
2534
|
+
}
|
|
2535
|
+
function parseConversationOverviewArgs(args) {
|
|
2536
|
+
let skeleton = false;
|
|
2537
|
+
for (const arg of args) {
|
|
2538
|
+
if (arg !== "--skeleton") {
|
|
2539
|
+
throw new Error(`Unknown conversation overview argument: ${arg}`);
|
|
2435
2540
|
}
|
|
2436
|
-
|
|
2437
|
-
|
|
2541
|
+
if (skeleton) {
|
|
2542
|
+
throw new Error("Use --skeleton only once");
|
|
2543
|
+
}
|
|
2544
|
+
skeleton = true;
|
|
2438
2545
|
}
|
|
2439
|
-
return
|
|
2546
|
+
return { skeleton };
|
|
2440
2547
|
}
|
|
2441
2548
|
function parseConversationForkArgs(args) {
|
|
2442
2549
|
let name;
|
|
@@ -2665,10 +2772,9 @@ Sessions: ${result.sessions.length}
|
|
|
2665
2772
|
return;
|
|
2666
2773
|
}
|
|
2667
2774
|
if (first === "conversation" && second === "overview") {
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
}
|
|
2671
|
-
const read = await client.sessions.conversationOverview(requireValue(args[2], "Missing session id"));
|
|
2775
|
+
const sessionId = requireValue(args[2], "Missing session id");
|
|
2776
|
+
const { skeleton } = parseConversationOverviewArgs(args.slice(3));
|
|
2777
|
+
const read = await client.sessions.conversationOverview(sessionId, skeleton ? { view: "skeleton" } : {});
|
|
2672
2778
|
write(read, renderConversationOverviewResponse(read));
|
|
2673
2779
|
return;
|
|
2674
2780
|
}
|
|
@@ -2703,8 +2809,12 @@ Sessions: ${result.sessions.length}
|
|
|
2703
2809
|
if (first === "conversation" && second === "inspect-work") {
|
|
2704
2810
|
const sessionId = requireValue(args[2], "Missing session id");
|
|
2705
2811
|
const workId = requireValue(args[3], "Missing work id");
|
|
2706
|
-
const
|
|
2707
|
-
const read = await client.sessions.inspectConversationWork(sessionId, workId, {
|
|
2812
|
+
const options = parseConversationInspectWorkArgs(args.slice(4));
|
|
2813
|
+
const read = await client.sessions.inspectConversationWork(sessionId, workId, {
|
|
2814
|
+
detail: options.detail,
|
|
2815
|
+
...options.calls !== void 0 ? { calls: options.calls } : {},
|
|
2816
|
+
...options.includePolls ? { polls: "include" } : {}
|
|
2817
|
+
});
|
|
2708
2818
|
write(read, renderConversationWorkResponse(read));
|
|
2709
2819
|
return;
|
|
2710
2820
|
}
|
|
@@ -3220,6 +3330,14 @@ async function runCommand(argv) {
|
|
|
3220
3330
|
return 0;
|
|
3221
3331
|
}
|
|
3222
3332
|
if (execution.kind === "session-run") {
|
|
3333
|
+
if (!options.json) {
|
|
3334
|
+
if (execution.command.kind === "start") {
|
|
3335
|
+
process.stderr.write("Starting session (publishing the workspace and provisioning the worktree)...\n");
|
|
3336
|
+
} else if (execution.command.kind === "stop") {
|
|
3337
|
+
process.stderr.write(`Stopping session ${execution.command.sessionId}...
|
|
3338
|
+
`);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3223
3341
|
const result = await dispatchSessionRunCommand(client, execution.command);
|
|
3224
3342
|
if (execution.command.kind !== "start" || !execution.command.follow) {
|
|
3225
3343
|
writeDataOutput(options.json, result.data, result.human);
|
|
@@ -3236,7 +3354,18 @@ async function runCommand(argv) {
|
|
|
3236
3354
|
});
|
|
3237
3355
|
}
|
|
3238
3356
|
if (execution.kind === "merge") {
|
|
3239
|
-
|
|
3357
|
+
if (!options.json) {
|
|
3358
|
+
if (execution.command.kind === "merge") {
|
|
3359
|
+
process.stderr.write("Merging (publishing the workspace and computing the merge)...\n");
|
|
3360
|
+
} else if (execution.command.kind === "finish") {
|
|
3361
|
+
process.stderr.write(`Finalizing ${execution.command.conflictWorktree} (publishing the resolver workspace)...
|
|
3362
|
+
`);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
const result = await dispatchMergeCommand(client, execution.command, {
|
|
3366
|
+
...options.json ? {} : { onProgress: (line) => process.stderr.write(`${line}
|
|
3367
|
+
`) }
|
|
3368
|
+
});
|
|
3240
3369
|
writeDataOutput(options.json, result.data, result.human);
|
|
3241
3370
|
return 0;
|
|
3242
3371
|
}
|
|
@@ -3312,13 +3441,15 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3312
3441
|
getR5dctlVersion,
|
|
3313
3442
|
getTransientDevicePollDelay,
|
|
3314
3443
|
handleAuthLogin,
|
|
3444
|
+
isMergeAttemptSettled,
|
|
3315
3445
|
main,
|
|
3316
3446
|
parseAnswerEnvRequestCommandArgs,
|
|
3317
3447
|
parseAnswerFlags,
|
|
3318
3448
|
parseAnswerQuestionsCommandArgs,
|
|
3319
3449
|
parseConversationForkArgs,
|
|
3450
|
+
parseConversationInspectWorkArgs,
|
|
3451
|
+
parseConversationOverviewArgs,
|
|
3320
3452
|
parseConversationRenderArgs,
|
|
3321
|
-
parseConversationWorkDetailArgs,
|
|
3322
3453
|
parseEnvFlags,
|
|
3323
3454
|
parseEnvRequestResponseArgs,
|
|
3324
3455
|
parseGetEnvsArgs,
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/cli.mjs
CHANGED
|
@@ -11,16 +11,8 @@ import {
|
|
|
11
11
|
R5dctlClient
|
|
12
12
|
} from "@ricsam/r5d-api";
|
|
13
13
|
import { runR5dctlShell } from "./shell.mjs";
|
|
14
|
-
const CHAT_MODES = /* @__PURE__ */ new Set([
|
|
15
|
-
|
|
16
|
-
"plan",
|
|
17
|
-
"build",
|
|
18
|
-
"agent",
|
|
19
|
-
"security_review",
|
|
20
|
-
"large_diff_remediation",
|
|
21
|
-
"merge_conflict_resolution"
|
|
22
|
-
]);
|
|
23
|
-
const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "build", "agent"]);
|
|
14
|
+
const CHAT_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent", "large_diff_remediation", "merge_conflict_resolution"]);
|
|
15
|
+
const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent"]);
|
|
24
16
|
const MODEL_TIERS = /* @__PURE__ */ new Set(["low", "medium", "high", "max"]);
|
|
25
17
|
const R5DCTL_PACKAGE_NAME = "@ricsam/r5dctl";
|
|
26
18
|
const CLI_GLOBAL_OPTION_HELP = [
|
|
@@ -89,10 +81,12 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
89
81
|
].join("\n");
|
|
90
82
|
const MERGE_HELP_TEXT = [
|
|
91
83
|
"Usage:",
|
|
92
|
-
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
84
|
+
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
85
|
+
" r5dctl -p <project> merge status <attempt-id>",
|
|
93
86
|
' r5dctl -p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
94
87
|
"",
|
|
95
88
|
"Merge a managed worktree into its recorded parent worktree.",
|
|
89
|
+
"Conflict worktrees provision in the background; merge waits by default and --no-wait returns the attempt immediately.",
|
|
96
90
|
""
|
|
97
91
|
].join("\n");
|
|
98
92
|
const SHARED_HELP_ENTRIES = [
|
|
@@ -167,8 +161,8 @@ const SHARED_HELP_ENTRIES = [
|
|
|
167
161
|
},
|
|
168
162
|
{
|
|
169
163
|
section: "sessions",
|
|
170
|
-
usage: "conversation overview <session-id>",
|
|
171
|
-
description: "Read dialogue and thinking while replacing tool work with inspectable ranges."
|
|
164
|
+
usage: "conversation overview <session-id> [--skeleton]",
|
|
165
|
+
description: "Read dialogue and thinking while replacing tool work with inspectable ranges; --skeleton keeps only dialogue, decisions, and per-turn work markers."
|
|
172
166
|
},
|
|
173
167
|
{
|
|
174
168
|
section: "sessions",
|
|
@@ -177,8 +171,8 @@ const SHARED_HELP_ENTRIES = [
|
|
|
177
171
|
},
|
|
178
172
|
{
|
|
179
173
|
section: "sessions",
|
|
180
|
-
usage: "conversation inspect-work <session-id> <work-id> [--compact|--summary|--full]",
|
|
181
|
-
description: "Inspect one turn's omitted tool work at the selected detail."
|
|
174
|
+
usage: "conversation inspect-work <session-id> <work-id> [--compact|--summary|--full] [--calls <n[-m]>] [--include-polls]",
|
|
175
|
+
description: "Inspect one turn's omitted tool work at the selected detail; finished shell polls are folded unless --include-polls."
|
|
182
176
|
},
|
|
183
177
|
{
|
|
184
178
|
section: "sessions",
|
|
@@ -243,9 +237,14 @@ const SHARED_HELP_ENTRIES = [
|
|
|
243
237
|
{ section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
|
|
244
238
|
{
|
|
245
239
|
section: "merge",
|
|
246
|
-
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
240
|
+
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
247
241
|
description: "Merge a worktree into its recorded parent, with an explicit conflict strategy."
|
|
248
242
|
},
|
|
243
|
+
{
|
|
244
|
+
section: "merge",
|
|
245
|
+
usage: "-p <project> merge status <attempt-id>",
|
|
246
|
+
description: "Show a merge attempt's provisioning and resolution status."
|
|
247
|
+
},
|
|
249
248
|
{
|
|
250
249
|
section: "merge",
|
|
251
250
|
usage: '-p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
@@ -482,7 +481,7 @@ function requireModel(value) {
|
|
|
482
481
|
function requireSessionMode(value) {
|
|
483
482
|
const mode = requireValue(value?.trim(), "--mode <mode> is required");
|
|
484
483
|
if (!R5DCTL_SESSION_MODES.has(mode)) {
|
|
485
|
-
throw new Error("Invalid --mode. Expected one of: ask, plan,
|
|
484
|
+
throw new Error("Invalid --mode. Expected one of: ask, plan, agent");
|
|
486
485
|
}
|
|
487
486
|
return mode;
|
|
488
487
|
}
|
|
@@ -568,12 +567,19 @@ function parseMergeCommand(options, args) {
|
|
|
568
567
|
...parsed2.values.get("--summary")?.trim() ? { summary: parsed2.values.get("--summary").trim() } : {}
|
|
569
568
|
};
|
|
570
569
|
}
|
|
570
|
+
if (args[0] === "status") {
|
|
571
|
+
const parsed2 = parseCommandOperands(args.slice(1), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), "merge status");
|
|
572
|
+
const attemptId = requireValue(parsed2.positionals[0], "Attempt id is required for `merge status`");
|
|
573
|
+
if (parsed2.positionals.length > 1) throw new Error(`Unexpected merge status argument: ${parsed2.positionals[1]}`);
|
|
574
|
+
return { kind: "status", project, attemptId };
|
|
575
|
+
}
|
|
571
576
|
const parsed = parseCommandOperands(
|
|
572
577
|
args,
|
|
573
578
|
/* @__PURE__ */ new Set(["--worker"]),
|
|
574
579
|
/* @__PURE__ */ new Map([
|
|
575
580
|
["--create-conflict-worktree", "--create-conflict-worktree"],
|
|
576
|
-
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"]
|
|
581
|
+
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"],
|
|
582
|
+
["--no-wait", "--no-wait"]
|
|
577
583
|
]),
|
|
578
584
|
"merge"
|
|
579
585
|
);
|
|
@@ -586,7 +592,14 @@ function parseMergeCommand(options, args) {
|
|
|
586
592
|
}
|
|
587
593
|
const conflictMode = createWorktree ? "worktree" : launchAgent ? "agent" : "none";
|
|
588
594
|
const worker = parsed.values.get("--worker")?.trim();
|
|
589
|
-
return {
|
|
595
|
+
return {
|
|
596
|
+
kind: "merge",
|
|
597
|
+
project,
|
|
598
|
+
sourceWorktree,
|
|
599
|
+
conflictMode,
|
|
600
|
+
...worker ? { worker } : {},
|
|
601
|
+
...parsed.booleans.has("--no-wait") ? { noWait: true } : {}
|
|
602
|
+
};
|
|
590
603
|
}
|
|
591
604
|
function assertAuthLoginArgs(args) {
|
|
592
605
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
@@ -1874,17 +1887,22 @@ function renderSessionRunStatus(session) {
|
|
|
1874
1887
|
function renderWorktreeMerge(result, input) {
|
|
1875
1888
|
const lines = [];
|
|
1876
1889
|
appendMergeDetails(lines, result);
|
|
1890
|
+
const status = responseString(result, "status");
|
|
1877
1891
|
const conflictWorktree = responseString(result, "conflictWorktree");
|
|
1878
1892
|
const resolverSessionId = responseString(result, "resolverSessionId");
|
|
1893
|
+
const attemptId = responseString(result, "attemptId");
|
|
1879
1894
|
const worker = responseString(result, "worker") ?? input.worker;
|
|
1880
1895
|
if (resolverSessionId) lines.push("", `Resolver session: ${resolverSessionId}`, ` r5dctl session stop ${resolverSessionId}`);
|
|
1881
|
-
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(
|
|
1896
|
+
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(status ?? "")) {
|
|
1882
1897
|
lines.push(
|
|
1883
1898
|
"",
|
|
1884
1899
|
"Finish after resolving and reviewing the conflict worktree:",
|
|
1885
1900
|
` r5dctl -p ${input.project} merge finish --worker ${worker} ${conflictWorktree} --summary "<summary>"`
|
|
1886
1901
|
);
|
|
1887
1902
|
}
|
|
1903
|
+
if (attemptId && (status === "provisioning" || status === "resolving" && result.resolverAgentRunning === false)) {
|
|
1904
|
+
lines.push("", "Check progress:", ` r5dctl -p ${input.project} merge status ${attemptId}`);
|
|
1905
|
+
}
|
|
1888
1906
|
return `${lines.join("\n")}
|
|
1889
1907
|
`;
|
|
1890
1908
|
}
|
|
@@ -1924,13 +1942,64 @@ async function dispatchSessionRunCommand(client, command) {
|
|
|
1924
1942
|
const data = await client.sessions.stop(command.sessionId);
|
|
1925
1943
|
return { data, human: renderSessionRunStatus(data) };
|
|
1926
1944
|
}
|
|
1927
|
-
|
|
1945
|
+
const MERGE_POLL_INTERVAL_MS = 2e3;
|
|
1946
|
+
const MERGE_POLL_TIMEOUT_MS = 20 * 6e4;
|
|
1947
|
+
const MERGE_POLL_MAX_CONSECUTIVE_FAILURES = 5;
|
|
1948
|
+
function isMergeAttemptSettled(response) {
|
|
1949
|
+
const status = responseString(response, "status");
|
|
1950
|
+
if (!status) return true;
|
|
1951
|
+
if (status === "resolving") return response.resolverAgentRunning !== false;
|
|
1952
|
+
return status !== "provisioning";
|
|
1953
|
+
}
|
|
1954
|
+
async function pollMergeAttemptUntilSettled(client, project, attemptId, initial, options) {
|
|
1955
|
+
const intervalMs = options.pollIntervalMs ?? MERGE_POLL_INTERVAL_MS;
|
|
1956
|
+
const timeoutMs = options.pollTimeoutMs ?? MERGE_POLL_TIMEOUT_MS;
|
|
1957
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1958
|
+
const onProgress = options.onProgress ?? (() => {
|
|
1959
|
+
});
|
|
1960
|
+
let latest = initial;
|
|
1961
|
+
let worktreeReadyReported = latest.worktreeReady === true;
|
|
1962
|
+
let consecutiveFailures = 0;
|
|
1963
|
+
let waitedMs = 0;
|
|
1964
|
+
while (!isMergeAttemptSettled(latest)) {
|
|
1965
|
+
if (waitedMs >= timeoutMs) {
|
|
1966
|
+
onProgress(`Still waiting after ${Math.round(timeoutMs / 6e4)} minutes; check later with: r5dctl -p ${project} merge status ${attemptId}`);
|
|
1967
|
+
return latest;
|
|
1968
|
+
}
|
|
1969
|
+
await sleep2(intervalMs);
|
|
1970
|
+
waitedMs += intervalMs;
|
|
1971
|
+
try {
|
|
1972
|
+
latest = await client.projects.worktrees.mergeAttempt(project, attemptId);
|
|
1973
|
+
consecutiveFailures = 0;
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
consecutiveFailures += 1;
|
|
1976
|
+
if (consecutiveFailures >= MERGE_POLL_MAX_CONSECUTIVE_FAILURES) throw error;
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
if (!worktreeReadyReported && latest.worktreeReady === true) {
|
|
1980
|
+
worktreeReadyReported = true;
|
|
1981
|
+
onProgress("Conflict worktree ready.");
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return latest;
|
|
1985
|
+
}
|
|
1986
|
+
async function dispatchMergeCommand(client, command, pollOptions = {}) {
|
|
1928
1987
|
if (command.kind === "merge") {
|
|
1929
1988
|
const data2 = await client.projects.worktrees.merge(command.project, command.sourceWorktree, {
|
|
1930
1989
|
conflictMode: command.conflictMode,
|
|
1931
1990
|
...command.worker ? { worker: command.worker } : {}
|
|
1932
1991
|
});
|
|
1933
|
-
|
|
1992
|
+
const attemptId = responseString(data2, "attemptId");
|
|
1993
|
+
if (command.noWait || !attemptId || isMergeAttemptSettled(data2)) {
|
|
1994
|
+
return { data: data2, human: renderWorktreeMerge(data2, command) };
|
|
1995
|
+
}
|
|
1996
|
+
pollOptions.onProgress?.(`Provisioning the conflict worktree (attempt ${attemptId})...`);
|
|
1997
|
+
const settled = await pollMergeAttemptUntilSettled(client, command.project, attemptId, data2, pollOptions);
|
|
1998
|
+
return { data: settled, human: renderWorktreeMerge(settled, command) };
|
|
1999
|
+
}
|
|
2000
|
+
if (command.kind === "status") {
|
|
2001
|
+
const data2 = await client.projects.worktrees.mergeAttempt(command.project, command.attemptId);
|
|
2002
|
+
return { data: data2, human: renderWorktreeMerge(data2, { project: command.project }) };
|
|
1934
2003
|
}
|
|
1935
2004
|
const data = await client.projects.worktrees.finishMerge(command.project, command.conflictWorktree, {
|
|
1936
2005
|
worker: command.worker,
|
|
@@ -2338,21 +2407,57 @@ async function followR5dctlSession(client, sessionId, options) {
|
|
|
2338
2407
|
}
|
|
2339
2408
|
return 0;
|
|
2340
2409
|
}
|
|
2341
|
-
function
|
|
2410
|
+
function parseConversationInspectWorkArgs(args) {
|
|
2342
2411
|
let detail = "compact";
|
|
2343
2412
|
let selected = false;
|
|
2344
|
-
|
|
2413
|
+
let calls;
|
|
2414
|
+
let includePolls = false;
|
|
2415
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2416
|
+
const arg = args[index];
|
|
2345
2417
|
const candidate = arg === "--compact" ? "compact" : arg === "--summary" ? "summary" : arg === "--full" ? "full" : void 0;
|
|
2346
|
-
if (
|
|
2347
|
-
|
|
2418
|
+
if (candidate) {
|
|
2419
|
+
if (selected) {
|
|
2420
|
+
throw new Error("Use only one of --compact, --summary, or --full");
|
|
2421
|
+
}
|
|
2422
|
+
detail = candidate;
|
|
2423
|
+
selected = true;
|
|
2424
|
+
continue;
|
|
2348
2425
|
}
|
|
2349
|
-
if (
|
|
2350
|
-
|
|
2426
|
+
if (arg === "--calls") {
|
|
2427
|
+
if (calls !== void 0) {
|
|
2428
|
+
throw new Error("Use --calls only once");
|
|
2429
|
+
}
|
|
2430
|
+
index += 1;
|
|
2431
|
+
const value = requireValue(args[index], "Missing value for --calls");
|
|
2432
|
+
if (!/^[0-9]+(-[0-9]+)?$/.test(value)) {
|
|
2433
|
+
throw new Error(`Invalid --calls range: ${value} (use N or N-M)`);
|
|
2434
|
+
}
|
|
2435
|
+
calls = value;
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
if (arg === "--include-polls") {
|
|
2439
|
+
if (includePolls) {
|
|
2440
|
+
throw new Error("Use --include-polls only once");
|
|
2441
|
+
}
|
|
2442
|
+
includePolls = true;
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
throw new Error(`Unknown inspect-work argument: ${arg}`);
|
|
2446
|
+
}
|
|
2447
|
+
return { detail, ...calls !== void 0 ? { calls } : {}, includePolls };
|
|
2448
|
+
}
|
|
2449
|
+
function parseConversationOverviewArgs(args) {
|
|
2450
|
+
let skeleton = false;
|
|
2451
|
+
for (const arg of args) {
|
|
2452
|
+
if (arg !== "--skeleton") {
|
|
2453
|
+
throw new Error(`Unknown conversation overview argument: ${arg}`);
|
|
2351
2454
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2455
|
+
if (skeleton) {
|
|
2456
|
+
throw new Error("Use --skeleton only once");
|
|
2457
|
+
}
|
|
2458
|
+
skeleton = true;
|
|
2354
2459
|
}
|
|
2355
|
-
return
|
|
2460
|
+
return { skeleton };
|
|
2356
2461
|
}
|
|
2357
2462
|
function parseConversationForkArgs(args) {
|
|
2358
2463
|
let name;
|
|
@@ -2581,10 +2686,9 @@ Sessions: ${result.sessions.length}
|
|
|
2581
2686
|
return;
|
|
2582
2687
|
}
|
|
2583
2688
|
if (first === "conversation" && second === "overview") {
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
2587
|
-
const read = await client.sessions.conversationOverview(requireValue(args[2], "Missing session id"));
|
|
2689
|
+
const sessionId = requireValue(args[2], "Missing session id");
|
|
2690
|
+
const { skeleton } = parseConversationOverviewArgs(args.slice(3));
|
|
2691
|
+
const read = await client.sessions.conversationOverview(sessionId, skeleton ? { view: "skeleton" } : {});
|
|
2588
2692
|
write(read, renderConversationOverviewResponse(read));
|
|
2589
2693
|
return;
|
|
2590
2694
|
}
|
|
@@ -2619,8 +2723,12 @@ Sessions: ${result.sessions.length}
|
|
|
2619
2723
|
if (first === "conversation" && second === "inspect-work") {
|
|
2620
2724
|
const sessionId = requireValue(args[2], "Missing session id");
|
|
2621
2725
|
const workId = requireValue(args[3], "Missing work id");
|
|
2622
|
-
const
|
|
2623
|
-
const read = await client.sessions.inspectConversationWork(sessionId, workId, {
|
|
2726
|
+
const options = parseConversationInspectWorkArgs(args.slice(4));
|
|
2727
|
+
const read = await client.sessions.inspectConversationWork(sessionId, workId, {
|
|
2728
|
+
detail: options.detail,
|
|
2729
|
+
...options.calls !== void 0 ? { calls: options.calls } : {},
|
|
2730
|
+
...options.includePolls ? { polls: "include" } : {}
|
|
2731
|
+
});
|
|
2624
2732
|
write(read, renderConversationWorkResponse(read));
|
|
2625
2733
|
return;
|
|
2626
2734
|
}
|
|
@@ -3136,6 +3244,14 @@ async function runCommand(argv) {
|
|
|
3136
3244
|
return 0;
|
|
3137
3245
|
}
|
|
3138
3246
|
if (execution.kind === "session-run") {
|
|
3247
|
+
if (!options.json) {
|
|
3248
|
+
if (execution.command.kind === "start") {
|
|
3249
|
+
process.stderr.write("Starting session (publishing the workspace and provisioning the worktree)...\n");
|
|
3250
|
+
} else if (execution.command.kind === "stop") {
|
|
3251
|
+
process.stderr.write(`Stopping session ${execution.command.sessionId}...
|
|
3252
|
+
`);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3139
3255
|
const result = await dispatchSessionRunCommand(client, execution.command);
|
|
3140
3256
|
if (execution.command.kind !== "start" || !execution.command.follow) {
|
|
3141
3257
|
writeDataOutput(options.json, result.data, result.human);
|
|
@@ -3152,7 +3268,18 @@ async function runCommand(argv) {
|
|
|
3152
3268
|
});
|
|
3153
3269
|
}
|
|
3154
3270
|
if (execution.kind === "merge") {
|
|
3155
|
-
|
|
3271
|
+
if (!options.json) {
|
|
3272
|
+
if (execution.command.kind === "merge") {
|
|
3273
|
+
process.stderr.write("Merging (publishing the workspace and computing the merge)...\n");
|
|
3274
|
+
} else if (execution.command.kind === "finish") {
|
|
3275
|
+
process.stderr.write(`Finalizing ${execution.command.conflictWorktree} (publishing the resolver workspace)...
|
|
3276
|
+
`);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
const result = await dispatchMergeCommand(client, execution.command, {
|
|
3280
|
+
...options.json ? {} : { onProgress: (line) => process.stderr.write(`${line}
|
|
3281
|
+
`) }
|
|
3282
|
+
});
|
|
3156
3283
|
writeDataOutput(options.json, result.data, result.human);
|
|
3157
3284
|
return 0;
|
|
3158
3285
|
}
|
|
@@ -3227,13 +3354,15 @@ export {
|
|
|
3227
3354
|
getR5dctlVersion,
|
|
3228
3355
|
getTransientDevicePollDelay,
|
|
3229
3356
|
handleAuthLogin,
|
|
3357
|
+
isMergeAttemptSettled,
|
|
3230
3358
|
main,
|
|
3231
3359
|
parseAnswerEnvRequestCommandArgs,
|
|
3232
3360
|
parseAnswerFlags,
|
|
3233
3361
|
parseAnswerQuestionsCommandArgs,
|
|
3234
3362
|
parseConversationForkArgs,
|
|
3363
|
+
parseConversationInspectWorkArgs,
|
|
3364
|
+
parseConversationOverviewArgs,
|
|
3235
3365
|
parseConversationRenderArgs,
|
|
3236
|
-
parseConversationWorkDetailArgs,
|
|
3237
3366
|
parseEnvFlags,
|
|
3238
3367
|
parseEnvRequestResponseArgs,
|
|
3239
3368
|
parseGetEnvsArgs,
|
package/dist/mjs/package.json
CHANGED
package/dist/types/cli.d.ts
CHANGED
|
@@ -85,6 +85,11 @@ export type R5dctlMergeCommand = {
|
|
|
85
85
|
sourceWorktree: string;
|
|
86
86
|
worker?: string;
|
|
87
87
|
conflictMode: R5dctlMergeConflictMode;
|
|
88
|
+
noWait?: boolean;
|
|
89
|
+
} | {
|
|
90
|
+
kind: "status";
|
|
91
|
+
project: string;
|
|
92
|
+
attemptId: string;
|
|
88
93
|
} | {
|
|
89
94
|
kind: "finish";
|
|
90
95
|
project: string;
|
|
@@ -229,6 +234,8 @@ export type R5dctlCommandResponse = {
|
|
|
229
234
|
conflictedFiles?: string[];
|
|
230
235
|
failureReason?: string;
|
|
231
236
|
message?: string;
|
|
237
|
+
worktreeReady?: boolean;
|
|
238
|
+
resolverAgentRunning?: boolean;
|
|
232
239
|
};
|
|
233
240
|
export type R5dctlSessionRunClient = {
|
|
234
241
|
projects: {
|
|
@@ -259,6 +266,7 @@ export type R5dctlMergeClient = {
|
|
|
259
266
|
worker: string;
|
|
260
267
|
summary?: string;
|
|
261
268
|
}) => Promise<R5dctlCommandResponse>;
|
|
269
|
+
mergeAttempt: (projectRef: string, attemptId: string) => Promise<R5dctlCommandResponse>;
|
|
262
270
|
};
|
|
263
271
|
};
|
|
264
272
|
};
|
|
@@ -273,7 +281,21 @@ export type R5dctlCommandDispatchResult = {
|
|
|
273
281
|
human: string;
|
|
274
282
|
};
|
|
275
283
|
export declare function dispatchSessionRunCommand(client: R5dctlSessionRunClient, command: R5dctlSessionRunCommand): Promise<R5dctlCommandDispatchResult>;
|
|
276
|
-
export
|
|
284
|
+
export type R5dctlMergePollOptions = {
|
|
285
|
+
onProgress?: (line: string) => void;
|
|
286
|
+
pollIntervalMs?: number;
|
|
287
|
+
pollTimeoutMs?: number;
|
|
288
|
+
sleep?: (ms: number) => Promise<void>;
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Whether a merge attempt response is settled for the CLI: terminal, waiting
|
|
292
|
+
* on the user (a ready manual conflict worktree), or actively resolving with
|
|
293
|
+
* its agent running. Anything else is still provisioning or starting. A
|
|
294
|
+
* "resolving" response without the resolverAgentRunning field comes from a
|
|
295
|
+
* server that starts the agent in-request, so it counts as settled.
|
|
296
|
+
*/
|
|
297
|
+
export declare function isMergeAttemptSettled(response: R5dctlCommandResponse): boolean;
|
|
298
|
+
export declare function dispatchMergeCommand(client: R5dctlMergeClient, command: R5dctlMergeCommand, pollOptions?: R5dctlMergePollOptions): Promise<R5dctlCommandDispatchResult>;
|
|
277
299
|
export declare function parseConversationRenderArgs(args: string[]): ConversationRenderOptions;
|
|
278
300
|
export declare function renderConversationResponse(read: R5dctlConversationResponse): string;
|
|
279
301
|
type R5dctlFollowClient = {
|
|
@@ -298,7 +320,15 @@ export type R5dctlFollowOptions = {
|
|
|
298
320
|
maxReconnects?: number;
|
|
299
321
|
};
|
|
300
322
|
export declare function followR5dctlSession(client: R5dctlFollowClient, sessionId: string, options: R5dctlFollowOptions): Promise<number>;
|
|
301
|
-
export
|
|
323
|
+
export type ConversationInspectWorkOptions = {
|
|
324
|
+
detail: R5dctlConversationWorkDetail;
|
|
325
|
+
calls?: string;
|
|
326
|
+
includePolls: boolean;
|
|
327
|
+
};
|
|
328
|
+
export declare function parseConversationInspectWorkArgs(args: string[]): ConversationInspectWorkOptions;
|
|
329
|
+
export declare function parseConversationOverviewArgs(args: string[]): {
|
|
330
|
+
skeleton: boolean;
|
|
331
|
+
};
|
|
302
332
|
export declare function parseConversationForkArgs(args: string[]): string | undefined;
|
|
303
333
|
export declare function renderConversationOverviewResponse(read: R5dctlConversationOverviewResponse): string;
|
|
304
334
|
export declare function renderConversationNodeResponse(read: R5dctlConversationNodeResponse): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5dctl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.73",
|
|
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.73",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"qrcode": "^1.5.4",
|
|
32
32
|
"ws": "^8.18.3"
|