cyber-mux 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { a as listWorktreesFromGit, c as pruneWorktrees, d as resolveWorktreePath, i as isWorktreeRemovable, l as removeWorktreeSafely, p as nodeExec, r as gitWorktreeAdapter, s as normalizeWorktreePath, t as WorktreeGitError, u as resolvePrimaryRoot } from "./worktree-CoQdRgv1.mjs";
2
- import { _ as envFallback, f as nodeNewId, o as currentPane, r as resolveMuxAdapter, s as probeMultiplexer, t as callerPane } from "./backend-DnrbmL6N.mjs";
1
+ import { a as listWorktreesFromGit, c as provisionWorktree, d as resolvePrimaryRoot, f as resolveWorktreePath, i as isWorktreeRemovable, l as pruneWorktrees, m as nodeExec, r as gitWorktreeAdapter, s as normalizeWorktreePath, t as WorktreeGitError, u as removeWorktreeSafely } from "./worktree-hHuFZkpW.mjs";
2
+ import { f as nodeNewId, o as currentPane, r as resolveMuxAdapter, s as probeMultiplexer, t as callerPane, x as envFallback } from "./backend-Cif257ji.mjs";
3
+ import { AgentLifecycleUnsupportedError, deriveAgentWait } from "./agent.mjs";
3
4
  import { collectPanes, firstPane, isValidTemplateName, listTemplates, nodeTemplateStore, parseTemplate, resolveTemplate as resolveTemplate$1, resolveTree, templateDirs, validateTemplate } from "./template.mjs";
4
5
  import { join, relative, sep } from "node:path";
5
6
  import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
@@ -136,6 +137,29 @@ var AmbiguousPaneError = class extends CliError {
136
137
  this.name = "AmbiguousPaneError";
137
138
  }
138
139
  };
140
+ /** The stable code a missing-pane usage error carries — the same code FAMILY as the ambiguity: both
141
+ * are exit-2 usage errors whose fix is a different pane argument, and both hand the caller the live
142
+ * panes to retry with rather than sending them on a second round trip. */
143
+ const MISSING_PANE_CODE = "missing-pane";
144
+ /**
145
+ * A pane verb invoked with no `<pane>` at all — a usage error (exit 2), the same shape the ambiguity
146
+ * uses. The old "having called no backend" guarantee is deliberately gone (CR 95): the backend IS
147
+ * queried so the live panes can be listed as candidates, since the caller's deterministic next move
148
+ * after "missing pane" is `cyber-mux list`. Folding that answer into the error collapses a two-turn
149
+ * correction into one, exactly as the ambiguity report already does.
150
+ *
151
+ * Carries its `candidates` in `extra` under the same key `AmbiguousPaneError` uses, so the one
152
+ * renderer prints them `<id> <label> <cwd>` with no special case. When the backend could NOT be
153
+ * enumerated the list is empty and the help points at `cyber-mux list` instead of the ids.
154
+ */
155
+ var MissingPaneError = class extends CliError {
156
+ candidates;
157
+ constructor(candidates) {
158
+ super(MISSING_PANE_CODE, "no pane given — this command needs a target pane", candidates.length > 0 ? `pass one of the ids below: ${candidates.map((c) => c.id).join(" ")}` : "list the live panes with: cyber-mux list", 2, { candidates });
159
+ this.candidates = candidates;
160
+ this.name = "MissingPaneError";
161
+ }
162
+ };
139
163
  /**
140
164
  * The ONE renderer — every coded failure reaches stdout through here, and exits.
141
165
  *
@@ -452,6 +476,54 @@ function convert(node, ctx) {
452
476
  if (node.ratio !== void 0) split.ratio = node.ratio;
453
477
  return split;
454
478
  }
479
+ /**
480
+ * A capture asked of a backend that lacks the optional geometry seam it needs — no `describeRegion`
481
+ * for a region capture, no `describeWorkspace` for a `--workspace` one. An absent optional seam member
482
+ * is a refusal, never a guess: a backend that cannot answer cannot be captured and there is nothing to
483
+ * degrade to.
484
+ *
485
+ * PORTABLE and exit-code-free by design. The DECISION to refuse is the library's and lives here, at
486
+ * the one place that sees the adapter — the pure `captureTemplate` above takes rectangles already read
487
+ * and could never make it. How the refusal SURFACES — the exit code, the fix hint, the exact sentence
488
+ * — is the CLI's, which catches this and re-raises its own `backend-unsupported` error. `capability`
489
+ * names which seam was missing so the caller composes the right message without re-deriving it, and
490
+ * the terse `message` is a factual log line, not the user-facing copy.
491
+ */
492
+ var CaptureUnsupportedError = class extends Error {
493
+ backend;
494
+ capability;
495
+ constructor(backend, capability) {
496
+ super(capability === "region" ? `${backend} cannot report a region's geometry` : `${backend} cannot enumerate a workspace's tabs`);
497
+ this.backend = backend;
498
+ this.capability = capability;
499
+ this.name = "CaptureUnsupportedError";
500
+ }
501
+ };
502
+ /**
503
+ * Read the live region around `target` through the adapter and derive its template — the
504
+ * surface-independent orchestrator `template save` drives, and the single home of the region-capture
505
+ * refusal. The optional `regions` seam is where a backend says whether it can report geometry at all;
506
+ * a backend without `describeRegion` is refused HERE (`CaptureUnsupportedError`), before any read,
507
+ * because the pure `captureTemplate` it would feed cannot see the adapter and so cannot make that
508
+ * call. A backend that CAN report reads its region and hands the rectangles to the pure capture
509
+ * unchanged.
510
+ */
511
+ function deriveRegionCapture(adapter, exec, target, opts) {
512
+ const describeRegion = adapter.regions?.describeRegion;
513
+ if (!describeRegion) throw new CaptureUnsupportedError(adapter.name, "region");
514
+ return captureTemplate(describeRegion(exec, target), opts);
515
+ }
516
+ /**
517
+ * Read every tab of the workspace `target` sits in through the adapter and derive a `tabs` template —
518
+ * the `--workspace` orchestrator, and the single home of the workspace-capture refusal. Same shape as
519
+ * `deriveRegionCapture`, one member over: a backend without `describeWorkspace` cannot enumerate a
520
+ * workspace's tabs and is refused HERE (`CaptureUnsupportedError`), before any read.
521
+ */
522
+ function deriveWorkspaceCapture(adapter, exec, target, opts) {
523
+ const describeWorkspace = adapter.regions?.describeWorkspace;
524
+ if (!describeWorkspace) throw new CaptureUnsupportedError(adapter.name, "workspace");
525
+ return captureWorkspaceTemplate(describeWorkspace(exec, target), opts);
526
+ }
455
527
  //#endregion
456
528
  //#region src/template-edit.ts
457
529
  /**
@@ -1224,6 +1296,16 @@ function noMux() {
1224
1296
  function paneNotFound(locator) {
1225
1297
  return new CliError("pane-not-found", `pane "${locator}" matched no live pane`, "list the live panes with: cyber-mux list", 1);
1226
1298
  }
1299
+ /**
1300
+ * `send text`/`send keys` given a pane but no payload — a usage error (exit 2), the fix is to add the
1301
+ * payload. The payload argument is optional at the parser (`[text]`/`[keys...]`) only so a bare
1302
+ * `send text` resolves its MISSING pane through `resolveTarget` and lists candidates rather than
1303
+ * tripping commander's own payload-missing error first; once a pane is resolved, an absent payload is
1304
+ * caught here so the operation never runs on nothing.
1305
+ */
1306
+ function missingSendPayload(verb) {
1307
+ return verb === "text" ? new CliError("missing-argument", "send text needs the text to type", "pass the text: cyber-mux send text <pane> <text>", 2) : new CliError("missing-argument", "send keys needs at least one key to press", "pass one or more keys: cyber-mux send keys <pane> <keys...>", 2);
1308
+ }
1227
1309
  /** A malformed template name — a usage error (exit 2): the fix is a different name, nothing was
1228
1310
  * attempted. The same family a missing required argument is in. */
1229
1311
  function invalidTemplateName(name) {
@@ -1270,6 +1352,17 @@ function reportWorktreeFailure(err) {
1270
1352
  * "no such pane" message this function's to write.
1271
1353
  */
1272
1354
  function resolveTarget(deps, a, locator) {
1355
+ if (!locator) {
1356
+ let listed = [];
1357
+ try {
1358
+ listed = a.listPanes(deps.exec);
1359
+ } catch {}
1360
+ throw new MissingPaneError(listed.map((p) => ({
1361
+ id: p.id,
1362
+ label: p.label ?? null,
1363
+ cwd: p.cwd ?? null
1364
+ })));
1365
+ }
1273
1366
  let panes;
1274
1367
  try {
1275
1368
  panes = a.listPanes(deps.exec);
@@ -1338,7 +1431,7 @@ function paneVerb(locator, body) {
1338
1431
  body();
1339
1432
  } catch (err) {
1340
1433
  if (err instanceof CliError) throw err;
1341
- throw paneNotFound(locator);
1434
+ throw paneNotFound(locator ?? "");
1342
1435
  }
1343
1436
  }
1344
1437
  /**
@@ -1557,17 +1650,14 @@ function templateSaveCommand(deps) {
1557
1650
  const path = join(templateDirs(deps.exec, deps.env)[opts.to], `${name}.json`);
1558
1651
  if (!opts.force && deps.store.read(path) !== null) throw new CliError("template-exists", `template "${name}" already exists at ${path} — pass --force to overwrite it`, "re-run with --force to replace it", 1);
1559
1652
  const a = adapter(deps);
1560
- const describeRegion = a.regions?.describeRegion;
1561
- if (!opts.workspace && !describeRegion) throw new CliError("backend-unsupported", `${a.name} cannot report a region's geometry — template save needs a backend that can`, "run template save on a backend that reports geometry (tmux or herdr)", 1);
1562
- const describeWorkspace = a.regions?.describeWorkspace;
1563
- if (opts.workspace && !describeWorkspace) throw new CliError("backend-unsupported", `${a.name} cannot enumerate a workspace's tabs — template save --workspace needs a backend that can`, "run template save --workspace on a backend that enumerates tabs (tmux or herdr)", 1);
1653
+ if (!(opts.workspace ? a.regions?.describeWorkspace : a.regions?.describeRegion)) throw new CaptureUnsupportedError(a.name, opts.workspace ? "workspace" : "region");
1564
1654
  const target = opts.from ? resolveTarget(deps, a, opts.from) : callerPane(a, deps.env);
1565
1655
  if (!target) throw new CliError("missing-pane", "template save needs a pane to capture the region around — pass --from <pane>, or run it inside one", "pass --from <pane>, or run template save inside a pane", 2);
1566
1656
  const captureOpts = {
1567
1657
  name,
1568
1658
  description: opts.description ?? CAPTURED_DESCRIPTION
1569
1659
  };
1570
- const { template, warnings } = opts.workspace ? captureWorkspaceTemplate(describeWorkspace(deps.exec, target), captureOpts) : captureTemplate(describeRegion(deps.exec, target), captureOpts);
1660
+ const { template, warnings } = opts.workspace ? deriveWorkspaceCapture(a, deps.exec, target, captureOpts) : deriveRegionCapture(a, deps.exec, target, captureOpts);
1571
1661
  deps.store.write(path, `${JSON.stringify(template, null, 2)}\n`);
1572
1662
  for (const warning of warnings) process.stderr.write(`${warning}\n`);
1573
1663
  const entry = opts.workspace ? null : noteTabsLeftOut(deps, a, target, name);
@@ -1581,6 +1671,7 @@ function templateSaveCommand(deps) {
1581
1671
  });
1582
1672
  } catch (err) {
1583
1673
  if (err instanceof CliError) throw err;
1674
+ if (err instanceof CaptureUnsupportedError) throw backendUnsupported(err);
1584
1675
  throw new CliError("unsplittable-region", "this region could not be captured — it is not a tree any sequence of splits could have produced", "template save can only capture a region built by splitting", 1);
1585
1676
  }
1586
1677
  }));
@@ -1599,6 +1690,15 @@ function templateSaveCommand(deps) {
1599
1690
  * right — a window nobody grouped is a workspace of one, and nothing was left out. The `command`
1600
1691
  * re-states the caller's own `name` with `--workspace`, the flag that captures every tab.
1601
1692
  */
1693
+ /**
1694
+ * The CLI surface of the library's geometry-capability refusal: exit 1 (a genuine operation failure,
1695
+ * not a usage error), naming the backend, with the flag-specific fix hint. The DECISION to refuse is
1696
+ * the orchestrator's (`CaptureUnsupportedError`); this composes only the presentation from the two
1697
+ * structured fields the error carries, so the sentence lives in one place per surface.
1698
+ */
1699
+ function backendUnsupported(err) {
1700
+ return err.capability === "region" ? new CliError("backend-unsupported", `${err.backend} cannot report a region's geometry — template save needs a backend that can`, "run template save on a backend that reports geometry (tmux or herdr)", 1) : new CliError("backend-unsupported", `${err.backend} cannot enumerate a workspace's tabs — template save --workspace needs a backend that can`, "run template save --workspace on a backend that enumerates tabs (tmux or herdr)", 1);
1701
+ }
1602
1702
  function noteTabsLeftOut(deps, adapter, target, name) {
1603
1703
  const describeWorkspace = adapter.regions?.describeWorkspace;
1604
1704
  if (!describeWorkspace) return null;
@@ -1928,22 +2028,26 @@ function openCommand(deps) {
1928
2028
  * error — a missing required parameter; see the AXI note in `.agents/spec/axi/README.md`). */
1929
2029
  function sendCommand(deps) {
1930
2030
  const send = new Command("send").description("Drive a pane without taking its turn (text | keys)");
1931
- send.addCommand(new Command("text").description("Type literal text into a pane, pressing no Enter (a key-named word is typed, not pressed)").argument("<pane>", "Target pane id").argument("<text>", "Literal text to type").addOption(FORMAT_OPTION).action(guarded((pane, text) => {
2031
+ send.addCommand(new Command("text").description("Type literal text into a pane, pressing no Enter (a key-named word is typed, not pressed)").argument("[pane]", "Target pane id").argument("[text]", "Literal text to type").addOption(FORMAT_OPTION).action(guarded((pane, text) => {
1932
2032
  paneVerb(pane, () => {
1933
2033
  const a = adapter(deps);
1934
- a.sendText(deps.exec, resolveTarget(deps, a, pane), text);
2034
+ const target = resolveTarget(deps, a, pane);
2035
+ if (text === void 0) throw missingSendPayload("text");
2036
+ a.sendText(deps.exec, target, text);
1935
2037
  });
1936
2038
  })));
1937
- send.addCommand(new Command("keys").description("Press named keys in a pane, typing nothing (Up, Enter, Escape, C-c, F1 …)").argument("<pane>", "Target pane id").argument("<keys...>", "Key names, in order — core vocabulary is portable, anything else is passed to the backend as-is").addOption(FORMAT_OPTION).action(guarded((pane, keys) => {
2039
+ send.addCommand(new Command("keys").description("Press named keys in a pane, typing nothing (Up, Enter, Escape, C-c, F1 …)").argument("[pane]", "Target pane id").argument("[keys...]", "Key names, in order — core vocabulary is portable, anything else is passed to the backend as-is").addOption(FORMAT_OPTION).action(guarded((pane, keys) => {
1938
2040
  paneVerb(pane, () => {
1939
2041
  const a = adapter(deps);
1940
- a.sendKeys(deps.exec, resolveTarget(deps, a, pane), keys);
2042
+ const target = resolveTarget(deps, a, pane);
2043
+ if (keys.length === 0) throw missingSendPayload("keys");
2044
+ a.sendKeys(deps.exec, target, keys);
1941
2045
  });
1942
2046
  })));
1943
2047
  return send;
1944
2048
  }
1945
2049
  function submitCommand(deps) {
1946
- return new Command("submit").description("Take a pane's turn: type the text if given, then always press Enter (no text = bare-Enter flush)").argument("<pane>", "Target pane id").argument("[text]", "Text to type before Enter; omit to flush an already-staged buffer without retyping it").addOption(FORMAT_OPTION).action(guarded((pane, text) => {
2050
+ return new Command("submit").description("Take a pane's turn: type the text if given, then always press Enter (no text = bare-Enter flush)").argument("[pane]", "Target pane id").argument("[text]", "Text to type before Enter; omit to flush an already-staged buffer without retyping it").addOption(FORMAT_OPTION).action(guarded((pane, text) => {
1947
2051
  paneVerb(pane, () => {
1948
2052
  const a = adapter(deps);
1949
2053
  a.submit(deps.exec, resolveTarget(deps, a, pane), text);
@@ -1951,7 +2055,7 @@ function submitCommand(deps) {
1951
2055
  }));
1952
2056
  }
1953
2057
  function readCommand(deps) {
1954
- return new Command("read").description("Capture a pane's output").argument("<pane>", "Target pane id").option("--lines <n>", "Trailing lines to capture", (v) => Number.parseInt(v, 10)).addOption(FORMAT_OPTION).action(guarded((pane, opts) => {
2058
+ return new Command("read").description("Capture a pane's output").argument("[pane]", "Target pane id").option("--lines <n>", "Trailing lines to capture", (v) => Number.parseInt(v, 10)).addOption(FORMAT_OPTION).action(guarded((pane, opts) => {
1955
2059
  paneVerb(pane, () => {
1956
2060
  const a = adapter(deps);
1957
2061
  const t = resolveTarget(deps, a, pane);
@@ -1960,8 +2064,78 @@ function readCommand(deps) {
1960
2064
  });
1961
2065
  }));
1962
2066
  }
2067
+ /** The default deadline a bare `wait` takes. Named, not spelled inline, so the help text and the value
2068
+ * cannot drift. Chosen to be long enough for a harness to boot and print, short enough that an agent
2069
+ * that guessed the wrong pattern gets its answer back inside one turn. */
2070
+ const DEFAULT_WAIT_TIMEOUT_MS = 3e4;
2071
+ /**
2072
+ * The `wait` verb: block until a pane PRINTS something, or the deadline passes.
2073
+ *
2074
+ * Agent-first in the two ways that matter here. First, the verdict is in the EXIT CODE — 0 matched, 1
2075
+ * timed out — so a shell caller branches without parsing anything, exactly as `exists` separates live
2076
+ * from gone. Second, a timeout is a normal answer and prints the same `{matched, output}` payload a
2077
+ * match does, so an agent that guessed wrong sees what the pane actually said instead of an error with
2078
+ * the evidence thrown away.
2079
+ *
2080
+ * It is its own verb rather than `read --wait <pattern>` because the two answer different questions —
2081
+ * `read` reports a snapshot and always succeeds; this one asserts a condition and can fail — and folding
2082
+ * them would make `read`'s exit code depend on a flag.
2083
+ *
2084
+ * The PATTERN is validated before the pane is resolved, which is the one place this verb orders its two
2085
+ * usage errors: a missing pattern is answered without a `listPanes` round trip, and candidate panes are
2086
+ * no use to a caller that has not said what it is waiting for. A missing PANE then takes the same
2087
+ * `MissingPaneError` (candidates listed) every other pane verb takes, from the one chokepoint that owns
2088
+ * it — which is why `[pane]` is optional at the parser and required in fact.
2089
+ */
2090
+ function waitCommand(deps) {
2091
+ return new Command("wait").description("Block until a pane's output matches, or the timeout elapses (exit 0 = matched, 1 = timed out)").argument("[pane]", "Target pane id").option("--match <text>", "Literal substring to wait for").option("--regex <pattern>", "Regular expression to wait for (backend dialect; prefer --match)").option("--timeout <ms>", `Give up after this many ms (default ${DEFAULT_WAIT_TIMEOUT_MS})`, (v) => Number.parseInt(v, 10), DEFAULT_WAIT_TIMEOUT_MS).option("--lines <n>", "Restrict the searched snapshot to this many trailing lines", (v) => Number.parseInt(v, 10)).addOption(FORMAT_OPTION).action(guardedAsync(async (pane, opts) => {
2092
+ const pattern = waitPattern(opts);
2093
+ if (!Number.isFinite(opts.timeout) || opts.timeout < 0) throw invalidWaitTimeout(opts.timeout);
2094
+ const a = adapter(deps);
2095
+ const t = resolveTarget(deps, a, pane);
2096
+ const result = await a.waitForOutput(deps.exec, t, {
2097
+ ...pattern,
2098
+ timeoutMs: opts.timeout,
2099
+ ...opts.lines != null ? { lines: opts.lines } : {}
2100
+ }).catch((err) => {
2101
+ if (err instanceof CliError) throw err;
2102
+ throw paneNotFound(pane ?? "");
2103
+ });
2104
+ output({
2105
+ pane: t.id,
2106
+ ...result
2107
+ }, () => {
2108
+ printFields({
2109
+ pane: t.id,
2110
+ matched: String(result.matched),
2111
+ line: result.matchedLine ?? null
2112
+ });
2113
+ });
2114
+ if (!result.matched) process.exit(1);
2115
+ }));
2116
+ }
2117
+ /** Exactly one of `--match`/`--regex`, as the seam requires — refused HERE so the caller gets this CLI's
2118
+ * coded usage error (exit 2, the fix is a different invocation) rather than the seam's bare throw. */
2119
+ function waitPattern(opts) {
2120
+ if (opts.match != null && opts.regex != null) throw new CliError("usage-error", "--match and --regex are mutually exclusive", "pass one pattern: cyber-mux wait <pane> --match <text>", 2);
2121
+ if (opts.match != null) return { match: opts.match };
2122
+ if (opts.regex != null) {
2123
+ try {
2124
+ new RegExp(opts.regex);
2125
+ } catch {
2126
+ throw new CliError("usage-error", `--regex is not a valid expression: ${opts.regex}`, "pass a valid pattern, or a literal with --match instead", 2);
2127
+ }
2128
+ return { regex: opts.regex };
2129
+ }
2130
+ throw new CliError("usage-error", "wait needs a pattern — pass --match or --regex", "wait for a literal: cyber-mux wait <pane> --match <text>", 2);
2131
+ }
2132
+ /** A `--timeout` that is not a non-negative number — a usage error: nothing was attempted, and the fix
2133
+ * is a different invocation. */
2134
+ function invalidWaitTimeout(timeout) {
2135
+ return new CliError("usage-error", `--timeout must be a non-negative number of milliseconds — got ${timeout}`, "pass milliseconds: cyber-mux wait <pane> --match <text> --timeout 30000", 2);
2136
+ }
1963
2137
  function focusCommand(deps) {
1964
- return new Command("focus").description("Beam the attached client to a pane").argument("<pane>", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
2138
+ return new Command("focus").description("Beam the attached client to a pane").argument("[pane]", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
1965
2139
  paneVerb(pane, () => {
1966
2140
  const a = adapter(deps);
1967
2141
  a.focus(deps.exec, resolveTarget(deps, a, pane));
@@ -1969,7 +2143,7 @@ function focusCommand(deps) {
1969
2143
  }));
1970
2144
  }
1971
2145
  function closeCommand(deps) {
1972
- return new Command("close").description("Close a pane").argument("<pane>", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
2146
+ return new Command("close").description("Close a pane").argument("[pane]", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
1973
2147
  paneVerb(pane, () => {
1974
2148
  const a = adapter(deps);
1975
2149
  a.teardown(deps.exec, resolveTarget(deps, a, pane));
@@ -1979,7 +2153,8 @@ function closeCommand(deps) {
1979
2153
  function listCommand(deps) {
1980
2154
  return new Command("list").description("Enumerate every live pane the current backend can see").addOption(FORMAT_OPTION).action(guarded(() => {
1981
2155
  const panes = adapter(deps).listPanes(deps.exec);
1982
- output({ panes }, () => printTable(panes, [
2156
+ const hasAgent = panes.some((p) => p.agentStatus !== void 0);
2157
+ const cols = [
1983
2158
  {
1984
2159
  label: "pane",
1985
2160
  get: (p) => p.id
@@ -1996,11 +2171,16 @@ function listCommand(deps) {
1996
2171
  label: "cwd",
1997
2172
  get: (p) => p.cwd ?? ""
1998
2173
  }
1999
- ]));
2174
+ ];
2175
+ if (hasAgent) cols.push({
2176
+ label: "agent",
2177
+ get: (p) => p.agentStatus ?? ""
2178
+ });
2179
+ output({ panes }, () => printTable(panes, cols));
2000
2180
  }));
2001
2181
  }
2002
2182
  function existsCommand(deps) {
2003
- return new Command("exists").description("Probe whether a single pane is still live (exit 0 = live, 1 = gone)").argument("<pane>", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
2183
+ return new Command("exists").description("Probe whether a single pane is still live (exit 0 = live, 1 = gone)").argument("[pane]", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
2004
2184
  const a = adapter(deps);
2005
2185
  const t = resolveTarget(deps, a, pane);
2006
2186
  const live = a.paneExists(deps.exec, t);
@@ -2011,6 +2191,87 @@ function existsCommand(deps) {
2011
2191
  if (!live) process.exit(1);
2012
2192
  }));
2013
2193
  }
2194
+ /**
2195
+ * The CLI surface of the library's agent-wait refusal: exit 1 (a genuine operation failure, not a
2196
+ * usage error), naming the backend, with a fix hint pointing at the herdr-only constraint. The
2197
+ * DECISION to refuse is the orchestrator's (`AgentLifecycleUnsupportedError`); this composes only the
2198
+ * presentation, so the sentence lives in one place — the exact mirror of how `backendUnsupported`
2199
+ * surfaces `CaptureUnsupportedError` for `template save`.
2200
+ */
2201
+ function agentUnsupported(err) {
2202
+ return new CliError("backend-unsupported", `${err.backend} cannot wait on agent-lifecycle state — agent wait needs a backend with a native per-pane agent-state feed (herdr)`, "run agent wait on herdr, the only backend with a native agent-state feed", 1);
2203
+ }
2204
+ /**
2205
+ * `agent status <pane>` — a SNAPSHOT that degrades, never refuses. It resolves a pane through the
2206
+ * shared ladder and prints its `agentStatus`, a fact independent of whether the backend can WAIT on it
2207
+ * (that is `agent wait`'s herdr-only capability). A backend with no agent-state feed still answers
2208
+ * truthfully — the pane, with no status — and exits 0: refusing here would turn a caller away from a
2209
+ * fact the backend CAN answer (which pane this is) because of one it can't (what its agent is doing),
2210
+ * and the two are independent.
2211
+ */
2212
+ function agentStatusCommand(deps) {
2213
+ return new Command("status").description("Print a pane's agent-lifecycle status (herdr); degrades to no status on a backend with no feed").argument("[pane]", "Target pane id or label").addOption(FORMAT_OPTION).action(guarded((pane) => {
2214
+ const a = adapter(deps);
2215
+ const target = resolveTarget(deps, a, pane);
2216
+ let agentStatus;
2217
+ try {
2218
+ agentStatus = a.listPanes(deps.exec).find((p) => p.id === target.id)?.agentStatus;
2219
+ } catch {
2220
+ agentStatus = void 0;
2221
+ }
2222
+ output({
2223
+ pane: target.id,
2224
+ agentStatus: agentStatus ?? null
2225
+ }, () => printFields({
2226
+ pane: target.id,
2227
+ agentStatus
2228
+ }));
2229
+ }));
2230
+ }
2231
+ /**
2232
+ * `agent wait <pane>` — a blocking DRIVE of herdr's native `agent wait`, or a refusal naming the
2233
+ * backend on one without the capability. Unlike `agent status`, waiting has no truthful degrade, so a
2234
+ * backend with no agent-lifecycle primitive is refused (`backend-unsupported`, exit 1) rather than
2235
+ * answered with a guess — the exact mirror of how `template save` refuses a geometry-incapable backend.
2236
+ *
2237
+ * The refusal is checked BEFORE the pane is resolved so a name that resolves nowhere never masks the
2238
+ * herdr-only refusal; the orchestrator (`deriveAgentWait`) re-checks the same seam member, that check
2239
+ * being its own library contract. `--until` is repeatable (omit to take herdr's default set) and
2240
+ * `--timeout` is optional (omit for an indefinite wait).
2241
+ */
2242
+ function agentWaitCommand(deps) {
2243
+ return new Command("wait").description("Block until a pane's agent reaches a state (herdr); refused on a backend with no agent-state feed").argument("[pane]", "Target pane id or label").option("--until <status...>", "Agent states any of which ends the wait (idle, working, blocked, done, unknown); omit for herdr’s default").option("--timeout <ms>", "Milliseconds before the wait gives up; omit to wait indefinitely", (v) => Number.parseInt(v, 10)).addOption(FORMAT_OPTION).addHelpText("after", "\nagent wait drives a native per-pane agent-state feed, which only herdr has — tmux, wezterm and\nzellij are refused with backend-unsupported. Use agent status for a snapshot that works everywhere.").action(guarded((pane, opts) => {
2244
+ const a = adapter(deps);
2245
+ if (!a.agentLifecycle) throw agentUnsupported(new AgentLifecycleUnsupportedError(a.name));
2246
+ const target = resolveTarget(deps, a, pane);
2247
+ let reached;
2248
+ try {
2249
+ reached = deriveAgentWait(a, deps.exec, target, {
2250
+ until: opts.until,
2251
+ timeoutMs: opts.timeout
2252
+ });
2253
+ } catch (err) {
2254
+ if (err instanceof CliError) throw err;
2255
+ if (err instanceof AgentLifecycleUnsupportedError) throw agentUnsupported(err);
2256
+ throw new CliError("agent-wait-failed", err instanceof Error ? err.message : String(err), "check the pane still exists with: cyber-mux exists <pane>", 1);
2257
+ }
2258
+ output({
2259
+ pane: target.id,
2260
+ agentStatus: reached
2261
+ }, () => console.log(reached));
2262
+ }));
2263
+ }
2264
+ /**
2265
+ * The `agent` group reaches the herdr agent-lifecycle capability: `status` (a snapshot that degrades
2266
+ * truthfully) and `wait` (a blocking drive of herdr's native wait, refused on every backend without
2267
+ * it). The state feed itself is herdr's; this group is how the CLI reads and blocks on it.
2268
+ */
2269
+ function agentCommand(deps) {
2270
+ const cmd = new Command("agent").description("Inspect and wait on a pane's agent-lifecycle state (herdr)");
2271
+ cmd.addCommand(agentStatusCommand(deps));
2272
+ cmd.addCommand(agentWaitCommand(deps));
2273
+ return cmd;
2274
+ }
2014
2275
  function worktreeAddCommand(deps) {
2015
2276
  return new Command("add").description("Create a git worktree, and open it when given a placement — grouped where the backend can").requiredOption("--branch <branch>", "Branch to create the worktree on").option("--path <path>", "Where to check out the worktree (default: a sibling of the primary checkout)").option("--base <ref>", "Start point for the new branch (default: the current HEAD)").option("--launch <command>", "Command to run in the opened pane; implies --at workspace").addOption(templateOption()).addOption(AT_OPTION).addOption(ENV_OPTION).addOption(LABEL_OPTION).addOption(FORMAT_OPTION).action((opts) => {
2016
2277
  try {
@@ -2085,6 +2346,47 @@ function worktreeAddCommand(deps) {
2085
2346
  }
2086
2347
  });
2087
2348
  }
2349
+ /**
2350
+ * `worktree provision` — reuse a free worktree or create a fresh one, the CLI wiring of the
2351
+ * `provisionWorktree` seam. The verb uses the DEFAULT availability gate (`isWorktreeRemovable`, the
2352
+ * exact set `worktree list` marks `(removable)` and `prune` removes) and offers NO flag to inject a
2353
+ * host predicate — that is the surface divergence from the library seam, which takes one. A host that
2354
+ * must exclude a live-session worktree calls `WorktreeApi.provision` directly. It reports what it did
2355
+ * (`reused` | `created`), the worktree, and on reuse the recycled entry — its prior branch and the
2356
+ * workspace it was open in; `printFields` drops the nullish reuse fields on a create.
2357
+ */
2358
+ function worktreeProvisionCommand(deps) {
2359
+ return new Command("provision").description("Reuse a free worktree or create a fresh one — the default-gate wiring of the provision seam (a worktree `list` marks \"(removable)\"). Reports whether it reused or created").requiredOption("--branch <branch>", "Branch the provisioned worktree ends up on").option("--base <ref>", "Start point for the fresh branch (default: the resolved default branch, then HEAD)").option("--path <path>", "Where a fresh checkout goes when none is free to reuse (default: a sibling of the primary checkout)").addOption(FORMAT_OPTION).action((opts) => {
2360
+ try {
2361
+ const primaryRoot = resolvePrimaryRoot(deps.exec);
2362
+ const path = opts.path ?? resolveWorktreePath(primaryRoot, opts.branch);
2363
+ const result = provisionWorktree(deps.exec, primaryRoot, { create: {
2364
+ path,
2365
+ branch: opts.branch,
2366
+ base: opts.base
2367
+ } });
2368
+ const reused = result.reused ? {
2369
+ root: result.reused.root,
2370
+ branch: result.reused.branch ?? null,
2371
+ workspace: result.reused.workspace ?? null
2372
+ } : null;
2373
+ output({
2374
+ action: result.action,
2375
+ root: result.worktree.root,
2376
+ branch: result.worktree.branch,
2377
+ reused
2378
+ }, () => printFields({
2379
+ action: result.action,
2380
+ root: result.worktree.root,
2381
+ branch: result.worktree.branch,
2382
+ "reused-from": reused ? reused.branch ?? "(detached)" : void 0,
2383
+ "reused-workspace": reused?.workspace ?? void 0
2384
+ }));
2385
+ } catch (err) {
2386
+ reportWorktreeFailure(err);
2387
+ }
2388
+ });
2389
+ }
2088
2390
  function worktreeOpenCommand(deps) {
2089
2391
  return new Command("open").description("Open an existing git worktree — groups it with the repo where the backend can bind").argument("<path>", "Worktree path to open").option("--launch <command>", "Command to run in the opened pane").addOption(AT_OPTION).addOption(ENV_OPTION).addOption(LABEL_OPTION).addOption(FORMAT_OPTION).action((path, opts) => {
2090
2392
  try {
@@ -2198,6 +2500,7 @@ function worktreePruneCommand(deps) {
2198
2500
  function worktreeCommand(deps) {
2199
2501
  const cmd = new Command("worktree").description("Git worktree helpers for spawning/tearing down a session");
2200
2502
  cmd.addCommand(worktreeAddCommand(deps));
2503
+ cmd.addCommand(worktreeProvisionCommand(deps));
2201
2504
  cmd.addCommand(worktreeOpenCommand(deps));
2202
2505
  cmd.addCommand(worktreeListCommand(deps));
2203
2506
  cmd.addCommand(worktreeRemoveCommand(deps));
@@ -2273,12 +2576,14 @@ function buildProgram(cliDeps = DEFAULT_DEPS) {
2273
2576
  program.addCommand(sendCommand(deps));
2274
2577
  program.addCommand(submitCommand(deps));
2275
2578
  program.addCommand(readCommand(deps));
2579
+ program.addCommand(waitCommand(deps));
2276
2580
  program.addCommand(focusCommand(deps));
2277
2581
  program.addCommand(closeCommand(deps));
2278
2582
  program.addCommand(listCommand(deps));
2279
2583
  program.addCommand(existsCommand(deps));
2280
2584
  program.addCommand(worktreeCommand(deps));
2281
2585
  program.addCommand(templateCommand(deps));
2586
+ program.addCommand(agentCommand(deps));
2282
2587
  return exitOverrideTree(program);
2283
2588
  }
2284
2589
  /** The real CLI entry point — called explicitly by `bin/cyber-mux.mjs`, never as an import-time