@ricsam/r5dctl 0.0.100 → 0.0.103

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 CHANGED
@@ -88,9 +88,12 @@ r5dctl -s <session-id> answer-env-request --worker <label> --mode agent --model
88
88
 
89
89
  r5dctl -p <project> session start --worker <label> --worktree main --model max "Explore the failure"
90
90
  r5dctl -p <project> session start --worker <label> --new-worktree fix/tests --source worktree:main --model max -f "Fix the failing tests"
91
+ r5dctl -p <project> session start --worker <label> --new-worktree fix/tests --source worktree:main --working-tree clean --model max "Fix the failing tests from main's committed state"
91
92
  r5dctl session status <session-id>
92
93
  r5dctl session prompt --worker <label> --mode agent --model max <session-id> "Also check the migration"
93
94
  r5dctl session stop <session-id>
95
+ r5dctl session process-log <run-id> --tail 200
96
+ r5dctl session process-log <run-id> --grep "error|failed"
94
97
 
95
98
  r5dctl -p <project> -b <branch> shell
96
99
  r5dctl -p <project> -b <branch> shell -c "bun test"
@@ -116,7 +119,11 @@ repository. Trust applies to that workflow path on every Git ref and grants
116
119
  cluster-admin, so protect workflow changes and do not invoke it for untrusted
117
120
  pull requests.
118
121
 
119
- `session start` creates an ordinary agent-mode chat on an explicitly selected existing worktree, or creates a named linked worktree from an explicit `worktree:<branch>` source. It requires project, worker, model, and prompt and never infers them from the current shell. Start returns a durable `provisioning` handle immediately; workspace synchronization, linked-worktree creation, and prompt enqueue continue in the background and are visible through `session status`. Idempotent request retries return the same handle. Prompts return after durable enqueue. 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 Git synchronization finishes asynchronously behind the response.
122
+ `session start` creates an ordinary agent-mode chat on an explicitly selected existing worktree, or creates a named linked worktree from an explicit `worktree:<branch>` source. A new worktree is cut from the source branch's current commit; `--working-tree carry` (the default, `git switch -c` semantics) also copies the source checkout's uncommitted changes into it, while `--working-tree clean` checks out that commit only. The new session's first message states which mode applies, and the start output prints `Source` and `Working tree` lines. A clean request fails with a clear error when the selected worker predates the option, because such a worker would silently carry instead. It requires project, worker, model, and prompt and never infers them from the current shell. Start returns a durable `provisioning` handle immediately; workspace synchronization, linked-worktree creation, and prompt enqueue continue in the background and are visible through `session status`. Idempotent request retries return the same handle. Prompts return after durable enqueue. 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 Git synchronization finishes asynchronously behind the response.
123
+
124
+ `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.
125
+
126
+ `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.
120
127
 
121
128
  Add `-f`/`--follow`/`--watch` to `session start` to poll the provisioning handle and 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.
122
129
 
package/dist/cjs/cli.cjs CHANGED
@@ -57,6 +57,7 @@ __export(cli_exports, {
57
57
  parsePromptArgs: () => parsePromptArgs,
58
58
  parsePsHistoryArgs: () => parsePsHistoryArgs,
59
59
  parsePsListArgs: () => parsePsListArgs,
60
+ parseSessionProcessLogArgs: () => parseSessionProcessLogArgs,
60
61
  parseSessionRunCommand: () => parseSessionRunCommand,
61
62
  parseSetEnvArgs: () => parseSetEnvArgs,
62
63
  parseShellArgs: () => parseShellArgs,
@@ -73,6 +74,7 @@ __export(cli_exports, {
73
74
  renderProcessHistory: () => renderProcessHistory,
74
75
  renderProcessInspection: () => renderProcessInspection,
75
76
  renderProcessList: () => renderProcessList,
77
+ renderProcessLog: () => renderProcessLog,
76
78
  renderSessionRunStart: () => renderSessionRunStart,
77
79
  renderSessionRunStatus: () => renderSessionRunStatus,
78
80
  renderWorkspaceStatus: () => renderWorkspaceStatus,
@@ -155,14 +157,19 @@ const K8S_HELP_TEXT = [
155
157
  ].join("\n");
156
158
  const SESSION_RUN_HELP_TEXT = [
157
159
  "Usage:",
158
- ' r5dctl -p <project> session start --worker <label> --worktree <branch> --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
159
- ' r5dctl -p <project> session start --worker <label> --new-worktree <branch> --source worktree:<branch> --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
160
+ ' r5dctl -p <project> session start --worker <label> --worktree <branch> --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
161
+ ' r5dctl -p <project> session start --worker <label> --new-worktree <branch> --source worktree:<branch> [--working-tree carry|clean] --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
160
162
  " r5dctl session status <session-id>",
161
163
  ' r5dctl session prompt --worker <label> --mode <mode> --model <tier> <session-id> "<prompt>"',
162
164
  " r5dctl session stop <session-id>",
165
+ " r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
163
166
  "",
164
167
  "Create, inspect, resume, and stop ordinary agent sessions.",
168
+ "A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
169
+ "--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
165
170
  "Use -f/--follow/--watch to attach after a start; Ctrl-C detaches without stopping the session.",
171
+ "process-log prints a shell run's stored stdout/stderr from the server: the whole log, its last n lines,",
172
+ "or the lines matching a JavaScript regular expression. The log is not a file on the worker.",
166
173
  ""
167
174
  ].join("\n");
168
175
  const SHARED_HELP_ENTRIES = [
@@ -262,8 +269,8 @@ const SHARED_HELP_ENTRIES = [
262
269
  },
263
270
  {
264
271
  section: "sessions",
265
- usage: '-p <project> session start --worker <label> (--worktree <branch>|--new-worktree <branch> --source worktree:<branch>) --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
266
- description: "Start an ordinary agent session on an explicit existing or new worktree."
272
+ usage: '-p <project> session start --worker <label> (--worktree <branch>|--new-worktree <branch> --source worktree:<branch> [--working-tree carry|clean]) --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
273
+ description: "Start an ordinary agent session on an explicit existing or new worktree. A new worktree carries the source checkout's uncommitted changes unless --working-tree clean is passed. --parent-session links it like start_subagent: the parent is notified when the run finishes."
267
274
  },
268
275
  {
269
276
  section: "sessions",
@@ -280,6 +287,11 @@ const SHARED_HELP_ENTRIES = [
280
287
  usage: "session stop <session-id>",
281
288
  description: "Stop an active session generation."
282
289
  },
290
+ {
291
+ section: "sessions",
292
+ usage: "session process-log <run-id> [--tail <n>] [--grep <pattern>]",
293
+ description: "Print a shell run's stored stdout/stderr from the server: the whole log, its last lines, or lines matching a regular expression."
294
+ },
283
295
  {
284
296
  section: "sessions",
285
297
  usage: '-s <session-id> answer-questions --worker <label> --mode <mode> --model <tier> -a1 "<answer>" ...',
@@ -543,6 +555,12 @@ function parseWorktreeSource(rawValue) {
543
555
  }
544
556
  return { type, branchName };
545
557
  }
558
+ function parseWorkingTreeMode(rawValue) {
559
+ if (rawValue === void 0) return "carry";
560
+ const mode = rawValue.trim();
561
+ if (mode !== "carry" && mode !== "clean") throw new Error("Invalid --working-tree. Expected carry or clean");
562
+ return mode;
563
+ }
546
564
  function requireModel(value) {
547
565
  const model = requireValue(value?.trim(), "--model <tier> is required");
548
566
  if (!MODEL_TIERS.has(model)) throw new Error("Invalid --model. Expected one of: low, medium, high, max");
@@ -568,7 +586,7 @@ function parseSessionRunCommand(options, args) {
568
586
  const project = requireValue(options.project, "--project/-p is required for `session start`");
569
587
  const parsed = parseCommandOperands(
570
588
  commandArgs,
571
- /* @__PURE__ */ new Set(["--worker", "--worktree", "--new-worktree", "--source", "--model"]),
589
+ /* @__PURE__ */ new Set(["--worker", "--worktree", "--new-worktree", "--source", "--working-tree", "--model", "--parent-session"]),
572
590
  /* @__PURE__ */ new Map([
573
591
  ["-f", "--follow"],
574
592
  ["--follow", "--follow"],
@@ -587,6 +605,9 @@ function parseSessionRunCommand(options, args) {
587
605
  }
588
606
  const sourceValue = parsed.values.get("--source");
589
607
  if (worktree && sourceValue) throw new Error("--source is only supported with --new-worktree");
608
+ const workingTreeValue = parsed.values.get("--working-tree");
609
+ if (worktree && workingTreeValue !== void 0) throw new Error("--working-tree is only supported with --new-worktree");
610
+ const parentSessionId = parsed.values.get("--parent-session")?.trim();
590
611
  return {
591
612
  kind: "start",
592
613
  project,
@@ -595,7 +616,8 @@ function parseSessionRunCommand(options, args) {
595
616
  prompt,
596
617
  follow: parsed.booleans.has("--follow"),
597
618
  includeTools: parsed.booleans.has("--tools"),
598
- ...worktree ? { worktree } : { newWorktree, source: parseWorktreeSource(sourceValue) }
619
+ ...parentSessionId ? { parentSessionId } : {},
620
+ ...worktree ? { worktree } : { newWorktree, source: parseWorktreeSource(sourceValue), workingTree: parseWorkingTreeMode(workingTreeValue) }
599
621
  };
600
622
  }
601
623
  rejectSessionRunGlobalScopes(options, false);
@@ -621,6 +643,16 @@ function parseSessionRunCommand(options, args) {
621
643
  }
622
644
  throw new Error(`Unknown session command: ${subcommand}`);
623
645
  }
646
+ function parseSessionProcessLogArgs(args) {
647
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--tail", "--grep"]), /* @__PURE__ */ new Map(), "session process-log");
648
+ const runId = requireValue(parsed.positionals[0], "Process run id is required for `session process-log`");
649
+ if (parsed.positionals.length > 1) throw new Error(`Unexpected session process-log argument: ${parsed.positionals[1]}`);
650
+ const tailValue = parsed.values.get("--tail");
651
+ const tail = tailValue === void 0 ? void 0 : Number(tailValue);
652
+ if (tail !== void 0 && (!Number.isInteger(tail) || tail < 1)) throw new Error("--tail must be a positive integer");
653
+ const grep = parsed.values.get("--grep");
654
+ return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
655
+ }
624
656
  function assertAuthLoginArgs(args) {
625
657
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
626
658
  const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
@@ -1678,6 +1710,32 @@ function renderProcessList(processes, nowMs = Date.now()) {
1678
1710
  function renderProcessHistory(processes, nowMs = Date.now()) {
1679
1711
  return processes.length === 0 ? "No process history.\n" : renderProcessList(processes, nowMs);
1680
1712
  }
1713
+ function renderProcessLog(log) {
1714
+ const sections = [];
1715
+ for (const [name, stream] of [
1716
+ ["stdout", log.stdout],
1717
+ ["stderr", log.stderr]
1718
+ ]) {
1719
+ if (stream.text.length === 0) continue;
1720
+ sections.push(`==> ${name} <==
1721
+ ${stream.text.endsWith("\n") ? stream.text : `${stream.text}
1722
+ `}`);
1723
+ }
1724
+ const selection = [
1725
+ ...log.grep !== void 0 ? [`lines matching /${log.grep}/`] : [],
1726
+ ...log.tailLines !== void 0 ? [`last ${log.tailLines} lines`] : []
1727
+ ];
1728
+ const cut = [log.stdout.truncated ? "stdout" : void 0, log.stderr.truncated ? "stderr" : void 0].filter(Boolean);
1729
+ const summary = [
1730
+ `Run ${log.runId} (${log.status}${log.exitCode !== void 0 ? `, exit ${log.exitCode}` : ""}): `,
1731
+ `stdout ${log.stdout.totalBytes} bytes, stderr ${log.stderr.totalBytes} bytes`,
1732
+ selection.length > 0 ? `; showing ${selection.join(", ")}` : "",
1733
+ cut.length > 0 ? `; ${cut.join(" and ")} cut to ${log.maxBytesPerStream} bytes, narrow with --tail or --grep` : "",
1734
+ sections.length === 0 ? "; no output to show" : ""
1735
+ ].join("");
1736
+ return { text: sections.join(""), summary: `${summary}
1737
+ ` };
1738
+ }
1681
1739
  function renderProcessInspection(process2) {
1682
1740
  return [
1683
1741
  `Run: ${process2.runId}`,
@@ -1831,13 +1889,22 @@ function formatStatusValue(value) {
1831
1889
  }
1832
1890
  return value === void 0 || value === null ? void 0 : String(value);
1833
1891
  }
1892
+ function pushWorktreeOriginLines(lines, session) {
1893
+ const source = responseString(session, "sourceBranch");
1894
+ const mode = responseString(session, "workingTree");
1895
+ if (!source || !mode) return;
1896
+ lines.push(`Source: ${source}`);
1897
+ const detail = mode === "carry" ? `carried ${source}'s uncommitted changes at launch` : mode === "clean" ? `clean checkout of ${source}'s commit at launch` : void 0;
1898
+ lines.push(`Working tree: ${mode}${detail ? ` (${detail})` : ""}`);
1899
+ }
1834
1900
  function renderSessionRunStart(session) {
1835
1901
  const lines = [
1836
1902
  `Session: ${responseString(session, "sessionId") ?? "(unknown)"}`,
1837
1903
  `Status: ${formatStatusValue(session.status) ?? "queued"}`,
1838
- `Worktree: ${responseString(session, "branchName") ?? "(provisioning)"}`,
1839
- `Worker: ${responseString(session, "worker") ?? "(unassigned)"}`
1904
+ `Worktree: ${responseString(session, "branchName") ?? "(provisioning)"}`
1840
1905
  ];
1906
+ pushWorktreeOriginLines(lines, session);
1907
+ lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
1841
1908
  const sessionUrl = responseString(session, "sessionUrl");
1842
1909
  if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
1843
1910
  const sessionId = responseString(session, "sessionId");
@@ -1845,29 +1912,50 @@ function renderSessionRunStart(session) {
1845
1912
  return `${lines.join("\n")}
1846
1913
  `;
1847
1914
  }
1915
+ function formatBranchStateHint(branch) {
1916
+ const target = branch ?? "<branch>";
1917
+ const commands = [
1918
+ `git merge-base <your-branch> ${target}`,
1919
+ `git log --oneline <your-branch>..${target}`,
1920
+ "git status in the branch folder"
1921
+ ];
1922
+ return `inspect the worktree with git (${commands.join("; ")})`;
1923
+ }
1924
+ function formatQueuedAt(value) {
1925
+ const time = Date.parse(value);
1926
+ return Number.isNaN(time) ? value : new Date(time).toISOString().replace(/\.\d{3}Z$/, "Z");
1927
+ }
1928
+ function formatProvisioningQueueLine(session) {
1929
+ const position = session.provisioningQueuePosition;
1930
+ const queuedAt = responseString(session, "provisioningQueuedAt");
1931
+ if (typeof position !== "number" || !Number.isInteger(position) || position < 0 || !queuedAt) return void 0;
1932
+ const since = formatQueuedAt(queuedAt);
1933
+ if (position === 0) return `Provisioning: worker started the branch operation at ${since}`;
1934
+ return `Provisioning: queued behind ${position} worker operation${position === 1 ? "" : "s"} since ${since}`;
1935
+ }
1848
1936
  function renderSessionRunStatus(session) {
1849
1937
  const status = formatStatusValue(session.status) ?? "unknown";
1850
1938
  const lines = [`Session: ${responseString(session, "sessionId") ?? "(unknown)"}`, `Status: ${status}`];
1851
1939
  const runStatus = formatStatusValue(session.runStatus);
1852
1940
  if (runStatus) lines.push(`Run: ${runStatus}`);
1941
+ const provisioningQueue = formatProvisioningQueueLine(session);
1942
+ if (provisioningQueue) lines.push(provisioningQueue);
1853
1943
  const promptDisposition = responseString(session, "promptDisposition");
1854
1944
  if (promptDisposition) lines.push(`Prompt: ${promptDisposition}`);
1855
1945
  const branch = responseString(session, "branchName");
1856
1946
  if (branch) lines.push(`Worktree: ${branch}`);
1947
+ pushWorktreeOriginLines(lines, session);
1857
1948
  for (const [label, key] of [
1858
- ["Workspace head", "workspaceHead"],
1859
- ["Baseline", "baselineCommit"],
1860
- ["Head", "headCommit"],
1861
1949
  ["Worker", "worker"],
1950
+ ["Parent session", "parentSessionId"],
1862
1951
  ["Session", "sessionUrl"]
1863
1952
  ]) {
1864
1953
  const value = responseString(session, key);
1865
1954
  if (value) lines.push(`${label}: ${value}`);
1866
1955
  }
1956
+ lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
1867
1957
  const error = responseString(session, "error");
1868
1958
  if (error) lines.push(`Error: ${error}`);
1869
- const headCommitError = responseString(session, "headCommitError");
1870
- if (headCommitError) lines.push(`Head error: ${headCommitError}`);
1871
1959
  const diffSummary = responseString(session, "diffSummary");
1872
1960
  if (diffSummary) lines.push("", diffSummary);
1873
1961
  const summary = responseString(session, "summary");
@@ -1885,13 +1973,14 @@ async function dispatchSessionRunCommand(client, command) {
1885
1973
  prompt: command.prompt,
1886
1974
  worker: command.worker,
1887
1975
  model: command.model,
1888
- requestId: (0, import_node_crypto.randomUUID)()
1976
+ requestId: (0, import_node_crypto.randomUUID)(),
1977
+ ...command.parentSessionId ? { parentSessionId: command.parentSessionId } : {}
1889
1978
  };
1890
1979
  let input;
1891
1980
  if (command.worktree !== void 0) {
1892
1981
  input = { ...commonInput, worktree: command.worktree };
1893
1982
  } else if (command.newWorktree !== void 0 && command.source !== void 0) {
1894
- input = { ...commonInput, newWorktree: command.newWorktree, source: command.source };
1983
+ input = { ...commonInput, newWorktree: command.newWorktree, source: command.source, workingTree: command.workingTree };
1895
1984
  } else {
1896
1985
  throw new Error("Session start requires an existing or new worktree");
1897
1986
  }
@@ -2531,6 +2620,17 @@ async function executeR5dctlCommand(client, json, args) {
2531
2620
  `);
2532
2621
  return;
2533
2622
  }
2623
+ if (first === "session" && second === "process-log") {
2624
+ const selection = parseSessionProcessLogArgs(args.slice(2));
2625
+ const log = await client.processes.log(selection.runId, {
2626
+ ...selection.tail !== void 0 ? { tail: selection.tail } : {},
2627
+ ...selection.grep !== void 0 ? { grep: selection.grep } : {}
2628
+ });
2629
+ const rendered = renderProcessLog(log);
2630
+ write(log, rendered.text);
2631
+ if (!json) process.stderr.write(rendered.summary);
2632
+ return;
2633
+ }
2534
2634
  if (first === "sessions" && second === "recent") {
2535
2635
  const sessions = await client.sessions.recent(parseRecentSessionArgs(args.slice(2)));
2536
2636
  write(sessions, renderRecentSessionList(sessions));
@@ -3138,6 +3238,20 @@ function resolveCommandExecution(options, rest) {
3138
3238
  };
3139
3239
  }
3140
3240
  if (command === "session") {
3241
+ if (commandArgs[0] === "process-log") {
3242
+ rejectSessionRunGlobalScopes(options, false);
3243
+ const selection = parseSessionProcessLogArgs(commandArgs.slice(1));
3244
+ return {
3245
+ kind: "plugin",
3246
+ pluginArgs: [
3247
+ "session",
3248
+ "process-log",
3249
+ selection.runId,
3250
+ ...selection.tail !== void 0 ? [`--tail=${selection.tail}`] : [],
3251
+ ...selection.grep !== void 0 ? [`--grep=${selection.grep}`] : []
3252
+ ]
3253
+ };
3254
+ }
3141
3255
  return { kind: "session-run", command: parseSessionRunCommand(options, commandArgs) };
3142
3256
  }
3143
3257
  if (command === "answer-questions") {
@@ -3318,6 +3432,7 @@ async function main(argv = process.argv.slice(2)) {
3318
3432
  parsePromptArgs,
3319
3433
  parsePsHistoryArgs,
3320
3434
  parsePsListArgs,
3435
+ parseSessionProcessLogArgs,
3321
3436
  parseSessionRunCommand,
3322
3437
  parseSetEnvArgs,
3323
3438
  parseShellArgs,
@@ -3334,6 +3449,7 @@ async function main(argv = process.argv.slice(2)) {
3334
3449
  renderProcessHistory,
3335
3450
  renderProcessInspection,
3336
3451
  renderProcessList,
3452
+ renderProcessLog,
3337
3453
  renderSessionRunStart,
3338
3454
  renderSessionRunStatus,
3339
3455
  renderWorkspaceStatus,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.100",
3
+ "version": "0.0.103",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -72,14 +72,19 @@ const K8S_HELP_TEXT = [
72
72
  ].join("\n");
73
73
  const SESSION_RUN_HELP_TEXT = [
74
74
  "Usage:",
75
- ' r5dctl -p <project> session start --worker <label> --worktree <branch> --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
76
- ' r5dctl -p <project> session start --worker <label> --new-worktree <branch> --source worktree:<branch> --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
75
+ ' r5dctl -p <project> session start --worker <label> --worktree <branch> --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
76
+ ' r5dctl -p <project> session start --worker <label> --new-worktree <branch> --source worktree:<branch> [--working-tree carry|clean] --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
77
77
  " r5dctl session status <session-id>",
78
78
  ' r5dctl session prompt --worker <label> --mode <mode> --model <tier> <session-id> "<prompt>"',
79
79
  " r5dctl session stop <session-id>",
80
+ " r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
80
81
  "",
81
82
  "Create, inspect, resume, and stop ordinary agent sessions.",
83
+ "A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
84
+ "--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
82
85
  "Use -f/--follow/--watch to attach after a start; Ctrl-C detaches without stopping the session.",
86
+ "process-log prints a shell run's stored stdout/stderr from the server: the whole log, its last n lines,",
87
+ "or the lines matching a JavaScript regular expression. The log is not a file on the worker.",
83
88
  ""
84
89
  ].join("\n");
85
90
  const SHARED_HELP_ENTRIES = [
@@ -179,8 +184,8 @@ const SHARED_HELP_ENTRIES = [
179
184
  },
180
185
  {
181
186
  section: "sessions",
182
- usage: '-p <project> session start --worker <label> (--worktree <branch>|--new-worktree <branch> --source worktree:<branch>) --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
183
- description: "Start an ordinary agent session on an explicit existing or new worktree."
187
+ usage: '-p <project> session start --worker <label> (--worktree <branch>|--new-worktree <branch> --source worktree:<branch> [--working-tree carry|clean]) --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
188
+ description: "Start an ordinary agent session on an explicit existing or new worktree. A new worktree carries the source checkout's uncommitted changes unless --working-tree clean is passed. --parent-session links it like start_subagent: the parent is notified when the run finishes."
184
189
  },
185
190
  {
186
191
  section: "sessions",
@@ -197,6 +202,11 @@ const SHARED_HELP_ENTRIES = [
197
202
  usage: "session stop <session-id>",
198
203
  description: "Stop an active session generation."
199
204
  },
205
+ {
206
+ section: "sessions",
207
+ usage: "session process-log <run-id> [--tail <n>] [--grep <pattern>]",
208
+ description: "Print a shell run's stored stdout/stderr from the server: the whole log, its last lines, or lines matching a regular expression."
209
+ },
200
210
  {
201
211
  section: "sessions",
202
212
  usage: '-s <session-id> answer-questions --worker <label> --mode <mode> --model <tier> -a1 "<answer>" ...',
@@ -460,6 +470,12 @@ function parseWorktreeSource(rawValue) {
460
470
  }
461
471
  return { type, branchName };
462
472
  }
473
+ function parseWorkingTreeMode(rawValue) {
474
+ if (rawValue === void 0) return "carry";
475
+ const mode = rawValue.trim();
476
+ if (mode !== "carry" && mode !== "clean") throw new Error("Invalid --working-tree. Expected carry or clean");
477
+ return mode;
478
+ }
463
479
  function requireModel(value) {
464
480
  const model = requireValue(value?.trim(), "--model <tier> is required");
465
481
  if (!MODEL_TIERS.has(model)) throw new Error("Invalid --model. Expected one of: low, medium, high, max");
@@ -485,7 +501,7 @@ function parseSessionRunCommand(options, args) {
485
501
  const project = requireValue(options.project, "--project/-p is required for `session start`");
486
502
  const parsed = parseCommandOperands(
487
503
  commandArgs,
488
- /* @__PURE__ */ new Set(["--worker", "--worktree", "--new-worktree", "--source", "--model"]),
504
+ /* @__PURE__ */ new Set(["--worker", "--worktree", "--new-worktree", "--source", "--working-tree", "--model", "--parent-session"]),
489
505
  /* @__PURE__ */ new Map([
490
506
  ["-f", "--follow"],
491
507
  ["--follow", "--follow"],
@@ -504,6 +520,9 @@ function parseSessionRunCommand(options, args) {
504
520
  }
505
521
  const sourceValue = parsed.values.get("--source");
506
522
  if (worktree && sourceValue) throw new Error("--source is only supported with --new-worktree");
523
+ const workingTreeValue = parsed.values.get("--working-tree");
524
+ if (worktree && workingTreeValue !== void 0) throw new Error("--working-tree is only supported with --new-worktree");
525
+ const parentSessionId = parsed.values.get("--parent-session")?.trim();
507
526
  return {
508
527
  kind: "start",
509
528
  project,
@@ -512,7 +531,8 @@ function parseSessionRunCommand(options, args) {
512
531
  prompt,
513
532
  follow: parsed.booleans.has("--follow"),
514
533
  includeTools: parsed.booleans.has("--tools"),
515
- ...worktree ? { worktree } : { newWorktree, source: parseWorktreeSource(sourceValue) }
534
+ ...parentSessionId ? { parentSessionId } : {},
535
+ ...worktree ? { worktree } : { newWorktree, source: parseWorktreeSource(sourceValue), workingTree: parseWorkingTreeMode(workingTreeValue) }
516
536
  };
517
537
  }
518
538
  rejectSessionRunGlobalScopes(options, false);
@@ -538,6 +558,16 @@ function parseSessionRunCommand(options, args) {
538
558
  }
539
559
  throw new Error(`Unknown session command: ${subcommand}`);
540
560
  }
561
+ function parseSessionProcessLogArgs(args) {
562
+ const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--tail", "--grep"]), /* @__PURE__ */ new Map(), "session process-log");
563
+ const runId = requireValue(parsed.positionals[0], "Process run id is required for `session process-log`");
564
+ if (parsed.positionals.length > 1) throw new Error(`Unexpected session process-log argument: ${parsed.positionals[1]}`);
565
+ const tailValue = parsed.values.get("--tail");
566
+ const tail = tailValue === void 0 ? void 0 : Number(tailValue);
567
+ if (tail !== void 0 && (!Number.isInteger(tail) || tail < 1)) throw new Error("--tail must be a positive integer");
568
+ const grep = parsed.values.get("--grep");
569
+ return { runId, ...tail !== void 0 ? { tail } : {}, ...grep !== void 0 ? { grep } : {} };
570
+ }
541
571
  function assertAuthLoginArgs(args) {
542
572
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
543
573
  const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
@@ -1595,6 +1625,32 @@ function renderProcessList(processes, nowMs = Date.now()) {
1595
1625
  function renderProcessHistory(processes, nowMs = Date.now()) {
1596
1626
  return processes.length === 0 ? "No process history.\n" : renderProcessList(processes, nowMs);
1597
1627
  }
1628
+ function renderProcessLog(log) {
1629
+ const sections = [];
1630
+ for (const [name, stream] of [
1631
+ ["stdout", log.stdout],
1632
+ ["stderr", log.stderr]
1633
+ ]) {
1634
+ if (stream.text.length === 0) continue;
1635
+ sections.push(`==> ${name} <==
1636
+ ${stream.text.endsWith("\n") ? stream.text : `${stream.text}
1637
+ `}`);
1638
+ }
1639
+ const selection = [
1640
+ ...log.grep !== void 0 ? [`lines matching /${log.grep}/`] : [],
1641
+ ...log.tailLines !== void 0 ? [`last ${log.tailLines} lines`] : []
1642
+ ];
1643
+ const cut = [log.stdout.truncated ? "stdout" : void 0, log.stderr.truncated ? "stderr" : void 0].filter(Boolean);
1644
+ const summary = [
1645
+ `Run ${log.runId} (${log.status}${log.exitCode !== void 0 ? `, exit ${log.exitCode}` : ""}): `,
1646
+ `stdout ${log.stdout.totalBytes} bytes, stderr ${log.stderr.totalBytes} bytes`,
1647
+ selection.length > 0 ? `; showing ${selection.join(", ")}` : "",
1648
+ cut.length > 0 ? `; ${cut.join(" and ")} cut to ${log.maxBytesPerStream} bytes, narrow with --tail or --grep` : "",
1649
+ sections.length === 0 ? "; no output to show" : ""
1650
+ ].join("");
1651
+ return { text: sections.join(""), summary: `${summary}
1652
+ ` };
1653
+ }
1598
1654
  function renderProcessInspection(process2) {
1599
1655
  return [
1600
1656
  `Run: ${process2.runId}`,
@@ -1748,13 +1804,22 @@ function formatStatusValue(value) {
1748
1804
  }
1749
1805
  return value === void 0 || value === null ? void 0 : String(value);
1750
1806
  }
1807
+ function pushWorktreeOriginLines(lines, session) {
1808
+ const source = responseString(session, "sourceBranch");
1809
+ const mode = responseString(session, "workingTree");
1810
+ if (!source || !mode) return;
1811
+ lines.push(`Source: ${source}`);
1812
+ const detail = mode === "carry" ? `carried ${source}'s uncommitted changes at launch` : mode === "clean" ? `clean checkout of ${source}'s commit at launch` : void 0;
1813
+ lines.push(`Working tree: ${mode}${detail ? ` (${detail})` : ""}`);
1814
+ }
1751
1815
  function renderSessionRunStart(session) {
1752
1816
  const lines = [
1753
1817
  `Session: ${responseString(session, "sessionId") ?? "(unknown)"}`,
1754
1818
  `Status: ${formatStatusValue(session.status) ?? "queued"}`,
1755
- `Worktree: ${responseString(session, "branchName") ?? "(provisioning)"}`,
1756
- `Worker: ${responseString(session, "worker") ?? "(unassigned)"}`
1819
+ `Worktree: ${responseString(session, "branchName") ?? "(provisioning)"}`
1757
1820
  ];
1821
+ pushWorktreeOriginLines(lines, session);
1822
+ lines.push(`Worker: ${responseString(session, "worker") ?? "(unassigned)"}`);
1758
1823
  const sessionUrl = responseString(session, "sessionUrl");
1759
1824
  if (sessionUrl) lines.push(`URL: ${sessionUrl}`);
1760
1825
  const sessionId = responseString(session, "sessionId");
@@ -1762,29 +1827,50 @@ function renderSessionRunStart(session) {
1762
1827
  return `${lines.join("\n")}
1763
1828
  `;
1764
1829
  }
1830
+ function formatBranchStateHint(branch) {
1831
+ const target = branch ?? "<branch>";
1832
+ const commands = [
1833
+ `git merge-base <your-branch> ${target}`,
1834
+ `git log --oneline <your-branch>..${target}`,
1835
+ "git status in the branch folder"
1836
+ ];
1837
+ return `inspect the worktree with git (${commands.join("; ")})`;
1838
+ }
1839
+ function formatQueuedAt(value) {
1840
+ const time = Date.parse(value);
1841
+ return Number.isNaN(time) ? value : new Date(time).toISOString().replace(/\.\d{3}Z$/, "Z");
1842
+ }
1843
+ function formatProvisioningQueueLine(session) {
1844
+ const position = session.provisioningQueuePosition;
1845
+ const queuedAt = responseString(session, "provisioningQueuedAt");
1846
+ if (typeof position !== "number" || !Number.isInteger(position) || position < 0 || !queuedAt) return void 0;
1847
+ const since = formatQueuedAt(queuedAt);
1848
+ if (position === 0) return `Provisioning: worker started the branch operation at ${since}`;
1849
+ return `Provisioning: queued behind ${position} worker operation${position === 1 ? "" : "s"} since ${since}`;
1850
+ }
1765
1851
  function renderSessionRunStatus(session) {
1766
1852
  const status = formatStatusValue(session.status) ?? "unknown";
1767
1853
  const lines = [`Session: ${responseString(session, "sessionId") ?? "(unknown)"}`, `Status: ${status}`];
1768
1854
  const runStatus = formatStatusValue(session.runStatus);
1769
1855
  if (runStatus) lines.push(`Run: ${runStatus}`);
1856
+ const provisioningQueue = formatProvisioningQueueLine(session);
1857
+ if (provisioningQueue) lines.push(provisioningQueue);
1770
1858
  const promptDisposition = responseString(session, "promptDisposition");
1771
1859
  if (promptDisposition) lines.push(`Prompt: ${promptDisposition}`);
1772
1860
  const branch = responseString(session, "branchName");
1773
1861
  if (branch) lines.push(`Worktree: ${branch}`);
1862
+ pushWorktreeOriginLines(lines, session);
1774
1863
  for (const [label, key] of [
1775
- ["Workspace head", "workspaceHead"],
1776
- ["Baseline", "baselineCommit"],
1777
- ["Head", "headCommit"],
1778
1864
  ["Worker", "worker"],
1865
+ ["Parent session", "parentSessionId"],
1779
1866
  ["Session", "sessionUrl"]
1780
1867
  ]) {
1781
1868
  const value = responseString(session, key);
1782
1869
  if (value) lines.push(`${label}: ${value}`);
1783
1870
  }
1871
+ lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
1784
1872
  const error = responseString(session, "error");
1785
1873
  if (error) lines.push(`Error: ${error}`);
1786
- const headCommitError = responseString(session, "headCommitError");
1787
- if (headCommitError) lines.push(`Head error: ${headCommitError}`);
1788
1874
  const diffSummary = responseString(session, "diffSummary");
1789
1875
  if (diffSummary) lines.push("", diffSummary);
1790
1876
  const summary = responseString(session, "summary");
@@ -1802,13 +1888,14 @@ async function dispatchSessionRunCommand(client, command) {
1802
1888
  prompt: command.prompt,
1803
1889
  worker: command.worker,
1804
1890
  model: command.model,
1805
- requestId: randomUUID()
1891
+ requestId: randomUUID(),
1892
+ ...command.parentSessionId ? { parentSessionId: command.parentSessionId } : {}
1806
1893
  };
1807
1894
  let input;
1808
1895
  if (command.worktree !== void 0) {
1809
1896
  input = { ...commonInput, worktree: command.worktree };
1810
1897
  } else if (command.newWorktree !== void 0 && command.source !== void 0) {
1811
- input = { ...commonInput, newWorktree: command.newWorktree, source: command.source };
1898
+ input = { ...commonInput, newWorktree: command.newWorktree, source: command.source, workingTree: command.workingTree };
1812
1899
  } else {
1813
1900
  throw new Error("Session start requires an existing or new worktree");
1814
1901
  }
@@ -2448,6 +2535,17 @@ async function executeR5dctlCommand(client, json, args) {
2448
2535
  `);
2449
2536
  return;
2450
2537
  }
2538
+ if (first === "session" && second === "process-log") {
2539
+ const selection = parseSessionProcessLogArgs(args.slice(2));
2540
+ const log = await client.processes.log(selection.runId, {
2541
+ ...selection.tail !== void 0 ? { tail: selection.tail } : {},
2542
+ ...selection.grep !== void 0 ? { grep: selection.grep } : {}
2543
+ });
2544
+ const rendered = renderProcessLog(log);
2545
+ write(log, rendered.text);
2546
+ if (!json) process.stderr.write(rendered.summary);
2547
+ return;
2548
+ }
2451
2549
  if (first === "sessions" && second === "recent") {
2452
2550
  const sessions = await client.sessions.recent(parseRecentSessionArgs(args.slice(2)));
2453
2551
  write(sessions, renderRecentSessionList(sessions));
@@ -3055,6 +3153,20 @@ function resolveCommandExecution(options, rest) {
3055
3153
  };
3056
3154
  }
3057
3155
  if (command === "session") {
3156
+ if (commandArgs[0] === "process-log") {
3157
+ rejectSessionRunGlobalScopes(options, false);
3158
+ const selection = parseSessionProcessLogArgs(commandArgs.slice(1));
3159
+ return {
3160
+ kind: "plugin",
3161
+ pluginArgs: [
3162
+ "session",
3163
+ "process-log",
3164
+ selection.runId,
3165
+ ...selection.tail !== void 0 ? [`--tail=${selection.tail}`] : [],
3166
+ ...selection.grep !== void 0 ? [`--grep=${selection.grep}`] : []
3167
+ ]
3168
+ };
3169
+ }
3058
3170
  return { kind: "session-run", command: parseSessionRunCommand(options, commandArgs) };
3059
3171
  }
3060
3172
  if (command === "answer-questions") {
@@ -3234,6 +3346,7 @@ export {
3234
3346
  parsePromptArgs,
3235
3347
  parsePsHistoryArgs,
3236
3348
  parsePsListArgs,
3349
+ parseSessionProcessLogArgs,
3237
3350
  parseSessionRunCommand,
3238
3351
  parseSetEnvArgs,
3239
3352
  parseShellArgs,
@@ -3250,6 +3363,7 @@ export {
3250
3363
  renderProcessHistory,
3251
3364
  renderProcessInspection,
3252
3365
  renderProcessList,
3366
+ renderProcessLog,
3253
3367
  renderSessionRunStart,
3254
3368
  renderSessionRunStatus,
3255
3369
  renderWorkspaceStatus,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.100",
3
+ "version": "0.0.103",
4
4
  "type": "module"
5
5
  }
@@ -1,4 +1,4 @@
1
- import { R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessRun, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorktreeSource, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
1
+ import { R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
2
2
  export type GlobalOptions = {
3
3
  baseUrl?: string;
4
4
  json: boolean;
@@ -53,14 +53,17 @@ type R5dctlSessionStartCommand = {
53
53
  prompt: string;
54
54
  follow: boolean;
55
55
  includeTools: boolean;
56
+ parentSessionId?: string;
56
57
  } & ({
57
58
  worktree: string;
58
59
  newWorktree?: never;
59
60
  source?: never;
61
+ workingTree?: never;
60
62
  } | {
61
63
  worktree?: never;
62
64
  newWorktree: string;
63
65
  source: R5dctlWorktreeSource;
66
+ workingTree: R5dctlWorkingTreeMode;
64
67
  });
65
68
  export type R5dctlSessionRunCommand = R5dctlSessionStartCommand | {
66
69
  kind: "status";
@@ -77,6 +80,12 @@ export type R5dctlSessionRunCommand = R5dctlSessionStartCommand | {
77
80
  sessionId: string;
78
81
  };
79
82
  export declare function parseSessionRunCommand(options: Pick<GlobalOptions, "project" | "branch" | "session">, args: string[]): R5dctlSessionRunCommand;
83
+ export type R5dctlSessionProcessLogArgs = {
84
+ runId: string;
85
+ tail?: number;
86
+ grep?: string;
87
+ };
88
+ export declare function parseSessionProcessLogArgs(args: string[]): R5dctlSessionProcessLogArgs;
80
89
  export declare function parseGlobalArgs(argv: string[]): {
81
90
  options: GlobalOptions;
82
91
  rest: string[];
@@ -176,6 +185,11 @@ export declare function renderK8sUsageReport(report: K8sUsageReport): string;
176
185
  export declare function formatProcessAge(startedAt: string, nowMs?: number): string;
177
186
  export declare function renderProcessList(processes: R5dctlProcessRun[], nowMs?: number): string;
178
187
  export declare function renderProcessHistory(processes: R5dctlProcessRun[], nowMs?: number): string;
188
+ /** Streams go to stdout under `==> name <==` headers; the selection summary is returned separately for stderr. */
189
+ export declare function renderProcessLog(log: R5dctlProcessLog): {
190
+ text: string;
191
+ summary: string;
192
+ };
179
193
  export declare function renderProcessInspection(process: R5dctlProcessInspection): string;
180
194
  export type EnvSummaryData = Record<string, {
181
195
  description: string;
@@ -191,11 +205,19 @@ export type R5dctlCommandResponse = {
191
205
  sessionId?: string;
192
206
  sessionUrl?: string;
193
207
  branchName?: string;
208
+ /** Launch-time worktree origin of a new-worktree run; absent or null on an existing worktree. */
209
+ sourceBranch?: string | null;
210
+ workingTree?: string | null;
211
+ /** Launch-time hashes: present in --json output but never rendered. */
194
212
  baselineCommit?: string | null;
195
213
  workspaceHead?: string | null;
196
214
  worker?: string;
197
215
  status?: string;
198
216
  runStatus?: unknown;
217
+ /** Worker queue state while a new worktree provisions; older servers and workers omit it. */
218
+ provisioningQueuePosition?: number;
219
+ provisioningQueuedAt?: string;
220
+ /** Legacy fields older servers still send; never rendered because a reported hash goes stale. */
199
221
  headCommit?: string | null;
200
222
  headCommitError?: string;
201
223
  promptDisposition?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.100",
3
+ "version": "0.0.103",
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.100",
29
+ "@ricsam/r5d-api": "^0.0.103",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"