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.
@@ -0,0 +1,66 @@
1
+ import { t as Exec } from "./exec-B81m4yjz.mjs";
2
+ import { d as MuxTarget, n as AgentStatus, o as MuxAdapter, r as AgentWaitOptions, t as AgentLifecycle } from "./mux-CoYDrk3v.mjs";
3
+ //#region src/agent.d.ts
4
+ /**
5
+ * An agent-lifecycle wait asked of a backend that has no native agent-state primitive (`agent wait`
6
+ * on tmux, wezterm or zellij). An absent `agentLifecycle` seam member is a REFUSAL, never a guess: a
7
+ * wait built from `read()` polling would silently disagree with herdr's own state derivation on the
8
+ * same question, and a wait has no truthful degrade the way a snapshot does — so a backend that cannot
9
+ * answer is refused rather than emulated.
10
+ *
11
+ * PORTABLE and exit-code-free by design, the exact mirror of `CaptureUnsupportedError`. The DECISION
12
+ * to refuse is the library's and lives in `deriveAgentWait`, the one place that sees the adapter — how
13
+ * the refusal SURFACES (the exit code, the fix hint, the exact sentence) is the CLI's, which catches
14
+ * this and re-raises its own `backend-unsupported` error. `backend` names the backend so the caller
15
+ * composes the message without re-deriving it; the terse `message` is a factual log line.
16
+ */
17
+ declare class AgentLifecycleUnsupportedError extends Error {
18
+ readonly backend: string;
19
+ constructor(backend: string);
20
+ }
21
+ /**
22
+ * Wait for the target pane's agent to reach one of `opts.until` (or the backend's default set) through
23
+ * the adapter — the surface-independent orchestrator `agent wait` drives, and the single home of the
24
+ * agent-wait refusal. The optional `agentLifecycle` seam is where a backend says whether it has a
25
+ * native wait at all; a backend without it is refused HERE (`AgentLifecycleUnsupportedError`), BEFORE
26
+ * any exec, because `waitForState` never sees the adapter and so cannot make that call. A backend that
27
+ * HAS the capability delegates to it unchanged.
28
+ *
29
+ * Mirrors `deriveRegionCapture` (`template-capture.ts`) exactly: the orchestrator is the one place that
30
+ * sees the adapter, so it is the one place the emulate-or-refuse decision can be made.
31
+ */
32
+ declare function deriveAgentWait(adapter: MuxAdapter, exec: Exec, target: MuxTarget, opts: AgentWaitOptions): AgentStatus;
33
+ /**
34
+ * The `agent` subpath facade with its `Exec` and backend BOUND — the exec-bound parallel of
35
+ * `worktreeApi`/`templateApi`. `agentApi(env, deps?)` resolves the backend adapter from `env` ONCE
36
+ * (`resolveMuxAdapter`, defaulting `exec` to `nodeExec`) and exposes `supported`/`status`/`wait` with
37
+ * the seams already threaded, so a caller never re-plumbs an adapter or a runner into them.
38
+ *
39
+ * It ADDS no logic of its own: `supported` reads the very capability presence `deriveAgentWait` gates
40
+ * on, `status` reads the same `LivePane.agentStatus` the listing already carries (for one pane rather
41
+ * than redefining it), and `wait` routes THROUGH `deriveAgentWait` — so the emulate-or-refuse decision
42
+ * stays specified once and enforced once, with no second refusal path here that could drift from it.
43
+ */
44
+ interface AgentApi {
45
+ /** Whether this backend reports agent-lifecycle state at all (herdr yes; tmux/wezterm/zellij no). */
46
+ supported(): boolean;
47
+ /** A pane's current agent state, or `undefined` when the backend has no feed (absent-not-false). */
48
+ status(target: MuxTarget): AgentStatus | undefined;
49
+ /**
50
+ * Block until the pane's agent reaches one of `opts.until` (or the backend's default set); throws
51
+ * `AgentLifecycleUnsupportedError` naming the backend on one without the capability, via
52
+ * `deriveAgentWait`. A bare `wait(target)` takes herdr's own defaults (`opts ?? {}`).
53
+ */
54
+ wait(target: MuxTarget, opts?: AgentWaitOptions | undefined): AgentStatus;
55
+ }
56
+ /**
57
+ * Bind the agent-lifecycle capability to an environment and runner once, returning an `AgentApi` whose
58
+ * methods no longer take an `Exec`. `deps.exec` defaults to `nodeExec`; `env` is bound like
59
+ * `resolveMux(env)` because it is what the probe resolves the backend from.
60
+ */
61
+ declare function agentApi(env: NodeJS.ProcessEnv, deps?: {
62
+ exec?: Exec | undefined;
63
+ } | undefined): AgentApi;
64
+ //#endregion
65
+ export { AgentApi, type AgentLifecycle, AgentLifecycleUnsupportedError, type AgentStatus, type AgentWaitOptions, agentApi, deriveAgentWait };
66
+ //# sourceMappingURL=agent.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.mts","names":[],"sources":["../src/agent.ts"],"mappings":";;;;;;;;;;;;;;;;cA4Ba,uCAAuC;WACvC;EAAZ,YAAY;;;;;;;;;;;;;iBAiBG,gBACf,SAAS,YACT,MAAM,MACN,QAAQ,WACR,MAAM,mBACJ;;;;;;;;;;;;UAiBc;;EAEhB;;EAEA,OAAO,QAAQ,YAAY;;;;;;EAM3B,KAAK,QAAQ,WAAW,OAAO,+BAA+B;;;;;;;iBAQ/C,SAAS,KAAK,OAAO,YAAY;EAAS,OAAO;gBAAiC"}
package/dist/agent.mjs ADDED
@@ -0,0 +1,58 @@
1
+ import { m as nodeExec } from "./worktree-hHuFZkpW.mjs";
2
+ import { r as resolveMuxAdapter } from "./backend-Cif257ji.mjs";
3
+ //#region src/agent.ts
4
+ /**
5
+ * An agent-lifecycle wait asked of a backend that has no native agent-state primitive (`agent wait`
6
+ * on tmux, wezterm or zellij). An absent `agentLifecycle` seam member is a REFUSAL, never a guess: a
7
+ * wait built from `read()` polling would silently disagree with herdr's own state derivation on the
8
+ * same question, and a wait has no truthful degrade the way a snapshot does — so a backend that cannot
9
+ * answer is refused rather than emulated.
10
+ *
11
+ * PORTABLE and exit-code-free by design, the exact mirror of `CaptureUnsupportedError`. The DECISION
12
+ * to refuse is the library's and lives in `deriveAgentWait`, the one place that sees the adapter — how
13
+ * the refusal SURFACES (the exit code, the fix hint, the exact sentence) is the CLI's, which catches
14
+ * this and re-raises its own `backend-unsupported` error. `backend` names the backend so the caller
15
+ * composes the message without re-deriving it; the terse `message` is a factual log line.
16
+ */
17
+ var AgentLifecycleUnsupportedError = class extends Error {
18
+ backend;
19
+ constructor(backend) {
20
+ super(`${backend} cannot report agent-lifecycle state`);
21
+ this.backend = backend;
22
+ this.name = "AgentLifecycleUnsupportedError";
23
+ }
24
+ };
25
+ /**
26
+ * Wait for the target pane's agent to reach one of `opts.until` (or the backend's default set) through
27
+ * the adapter — the surface-independent orchestrator `agent wait` drives, and the single home of the
28
+ * agent-wait refusal. The optional `agentLifecycle` seam is where a backend says whether it has a
29
+ * native wait at all; a backend without it is refused HERE (`AgentLifecycleUnsupportedError`), BEFORE
30
+ * any exec, because `waitForState` never sees the adapter and so cannot make that call. A backend that
31
+ * HAS the capability delegates to it unchanged.
32
+ *
33
+ * Mirrors `deriveRegionCapture` (`template-capture.ts`) exactly: the orchestrator is the one place that
34
+ * sees the adapter, so it is the one place the emulate-or-refuse decision can be made.
35
+ */
36
+ function deriveAgentWait(adapter, exec, target, opts) {
37
+ const agentLifecycle = adapter.agentLifecycle;
38
+ if (!agentLifecycle) throw new AgentLifecycleUnsupportedError(adapter.name);
39
+ return agentLifecycle.waitForState(exec, target, opts);
40
+ }
41
+ /**
42
+ * Bind the agent-lifecycle capability to an environment and runner once, returning an `AgentApi` whose
43
+ * methods no longer take an `Exec`. `deps.exec` defaults to `nodeExec`; `env` is bound like
44
+ * `resolveMux(env)` because it is what the probe resolves the backend from.
45
+ */
46
+ function agentApi(env, deps) {
47
+ const exec = deps?.exec ?? nodeExec;
48
+ const adapter = resolveMuxAdapter(env, exec);
49
+ return {
50
+ supported: () => adapter.agentLifecycle !== void 0,
51
+ status: (target) => adapter.listPanes(exec).find((p) => p.id === target.id)?.agentStatus,
52
+ wait: (target, opts) => deriveAgentWait(adapter, exec, target, opts ?? {})
53
+ };
54
+ }
55
+ //#endregion
56
+ export { AgentLifecycleUnsupportedError, agentApi, deriveAgentWait };
57
+
58
+ //# sourceMappingURL=agent.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.mjs","names":[],"sources":["../src/agent.ts"],"sourcesContent":["import { resolveMuxAdapter } from './backend.ts'\nimport { type Exec, nodeExec } from './exec.ts'\nimport type { AgentStatus, AgentWaitOptions, MuxAdapter, MuxTarget } from './mux.ts'\n\n/**\n * The `cyber-mux/agent` subpath — the agent-lifecycle capability's orchestrator and its refusal.\n *\n * The `AgentStatus` type rides out on the `.` barrel (it is part of `LivePane`, and `mux.ts` is\n * re-exported there); the WAIT capability — the `AgentLifecycle` seam plus the emulate-or-refuse\n * decision below — is this subpath alone, kept off the core barrel exactly as `template`'s apply\n * engine is: a capability nobody has to import to drive a pane is not on the surface everybody gets.\n */\n\nexport type { AgentLifecycle, AgentStatus, AgentWaitOptions } from './mux.ts'\n\n/**\n * An agent-lifecycle wait asked of a backend that has no native agent-state primitive (`agent wait`\n * on tmux, wezterm or zellij). An absent `agentLifecycle` seam member is a REFUSAL, never a guess: a\n * wait built from `read()` polling would silently disagree with herdr's own state derivation on the\n * same question, and a wait has no truthful degrade the way a snapshot does — so a backend that cannot\n * answer is refused rather than emulated.\n *\n * PORTABLE and exit-code-free by design, the exact mirror of `CaptureUnsupportedError`. The DECISION\n * to refuse is the library's and lives in `deriveAgentWait`, the one place that sees the adapter — how\n * the refusal SURFACES (the exit code, the fix hint, the exact sentence) is the CLI's, which catches\n * this and re-raises its own `backend-unsupported` error. `backend` names the backend so the caller\n * composes the message without re-deriving it; the terse `message` is a factual log line.\n */\nexport class AgentLifecycleUnsupportedError extends Error {\n\tconstructor(readonly backend: string) {\n\t\tsuper(`${backend} cannot report agent-lifecycle state`)\n\t\tthis.name = 'AgentLifecycleUnsupportedError'\n\t}\n}\n\n/**\n * Wait for the target pane's agent to reach one of `opts.until` (or the backend's default set) through\n * the adapter — the surface-independent orchestrator `agent wait` drives, and the single home of the\n * agent-wait refusal. The optional `agentLifecycle` seam is where a backend says whether it has a\n * native wait at all; a backend without it is refused HERE (`AgentLifecycleUnsupportedError`), BEFORE\n * any exec, because `waitForState` never sees the adapter and so cannot make that call. A backend that\n * HAS the capability delegates to it unchanged.\n *\n * Mirrors `deriveRegionCapture` (`template-capture.ts`) exactly: the orchestrator is the one place that\n * sees the adapter, so it is the one place the emulate-or-refuse decision can be made.\n */\nexport function deriveAgentWait(\n\tadapter: MuxAdapter,\n\texec: Exec,\n\ttarget: MuxTarget,\n\topts: AgentWaitOptions,\n): AgentStatus {\n\tconst agentLifecycle = adapter.agentLifecycle\n\tif (!agentLifecycle) throw new AgentLifecycleUnsupportedError(adapter.name)\n\treturn agentLifecycle.waitForState(exec, target, opts)\n}\n\n/**\n * The `agent` subpath facade with its `Exec` and backend BOUND — the exec-bound parallel of\n * `worktreeApi`/`templateApi`. `agentApi(env, deps?)` resolves the backend adapter from `env` ONCE\n * (`resolveMuxAdapter`, defaulting `exec` to `nodeExec`) and exposes `supported`/`status`/`wait` with\n * the seams already threaded, so a caller never re-plumbs an adapter or a runner into them.\n *\n * It ADDS no logic of its own: `supported` reads the very capability presence `deriveAgentWait` gates\n * on, `status` reads the same `LivePane.agentStatus` the listing already carries (for one pane rather\n * than redefining it), and `wait` routes THROUGH `deriveAgentWait` — so the emulate-or-refuse decision\n * stays specified once and enforced once, with no second refusal path here that could drift from it.\n */\nexport interface AgentApi {\n\t/** Whether this backend reports agent-lifecycle state at all (herdr yes; tmux/wezterm/zellij no). */\n\tsupported(): boolean\n\t/** A pane's current agent state, or `undefined` when the backend has no feed (absent-not-false). */\n\tstatus(target: MuxTarget): AgentStatus | undefined\n\t/**\n\t * Block until the pane's agent reaches one of `opts.until` (or the backend's default set); throws\n\t * `AgentLifecycleUnsupportedError` naming the backend on one without the capability, via\n\t * `deriveAgentWait`. A bare `wait(target)` takes herdr's own defaults (`opts ?? {}`).\n\t */\n\twait(target: MuxTarget, opts?: AgentWaitOptions | undefined): AgentStatus\n}\n\n/**\n * Bind the agent-lifecycle capability to an environment and runner once, returning an `AgentApi` whose\n * methods no longer take an `Exec`. `deps.exec` defaults to `nodeExec`; `env` is bound like\n * `resolveMux(env)` because it is what the probe resolves the backend from.\n */\nexport function agentApi(env: NodeJS.ProcessEnv, deps?: { exec?: Exec | undefined } | undefined): AgentApi {\n\tconst exec = deps?.exec ?? nodeExec\n\tconst adapter = resolveMuxAdapter(env, exec)\n\treturn {\n\t\tsupported: () => adapter.agentLifecycle !== undefined,\n\t\tstatus: (target) => adapter.listPanes(exec).find((p) => p.id === target.id)?.agentStatus,\n\t\twait: (target, opts) => deriveAgentWait(adapter, exec, target, opts ?? {}),\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA4BA,IAAa,iCAAb,cAAoD,MAAM;CACpC;CAArB,YAAY,SAA0B;EACrC,MAAM,GAAG,QAAQ,qCAAqC;EADlC,KAAA,UAAA;EAEpB,KAAK,OAAO;CACb;AACD;;;;;;;;;;;;AAaA,SAAgB,gBACf,SACA,MACA,QACA,MACc;CACd,MAAM,iBAAiB,QAAQ;CAC/B,IAAI,CAAC,gBAAgB,MAAM,IAAI,+BAA+B,QAAQ,IAAI;CAC1E,OAAO,eAAe,aAAa,MAAM,QAAQ,IAAI;AACtD;;;;;;AA+BA,SAAgB,SAAS,KAAwB,MAA0D;CAC1G,MAAM,OAAO,MAAM,QAAQ;CAC3B,MAAM,UAAU,kBAAkB,KAAK,IAAI;CAC3C,OAAO;EACN,iBAAiB,QAAQ,mBAAmB,KAAA;EAC5C,SAAS,WAAW,QAAQ,UAAU,IAAI,CAAC,CAAC,MAAM,MAAM,EAAE,OAAO,OAAO,EAAE,CAAC,EAAE;EAC7E,OAAO,QAAQ,SAAS,gBAAgB,SAAS,MAAM,QAAQ,QAAQ,CAAC,CAAC;CAC1E;AACD"}
@@ -1,4 +1,4 @@
1
- import { m as withReason, p as nodeExec, s as normalizeWorktreePath } from "./worktree-CoQdRgv1.mjs";
1
+ import { h as withReason, m as nodeExec, s as normalizeWorktreePath } from "./worktree-hHuFZkpW.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
4
  //#region src/env-fallback.ts
@@ -50,6 +50,130 @@ function envFallback(env, command) {
50
50
  };
51
51
  }
52
52
  //#endregion
53
+ //#region src/ratio.ts
54
+ /**
55
+ * The seam's own precondition on `MuxOpenOptions.ratio`: a fraction STRICTLY between 0 and 1.
56
+ *
57
+ * `ratio` is the fraction kept by the ORIGINAL pane. Outside `0 < ratio < 1` there is no split it can
58
+ * name: `1 - ratio` goes negative above 1 (tmux `-l -50%` / wezterm `--percent -50`), and 0 or 1 hands
59
+ * one side the whole region and the other nothing — a mistake, never an intent worth honoring. Left
60
+ * unrendered these produce a silently broken split, not an error, which is the exact silent-wrong
61
+ * output this seam's loud-over-quiet preference exists to refuse.
62
+ *
63
+ * Enforced HERE, at the seam, rather than left to each caller, because the invariant is a universal
64
+ * property of what a ratio IS — true on every backend — not a per-caller policy. (The DEGRADE policy —
65
+ * what a caller does when a backend cannot size a split at all — genuinely stays the caller's, unchanged;
66
+ * range validity and degrade policy are different questions.) A caller cannot reach an adapter with an
67
+ * out-of-range ratio and have it silently rendered; `template`'s schema still refuses one earlier, per
68
+ * node, with a path-qualified message, so the two layers do different jobs and the seam is the backstop.
69
+ *
70
+ * The guard lives WITH the rendering: it is called by each backend's size render helper, so a backend
71
+ * that cannot size a split (zellij) renders no ratio and so never reaches this guard — a dropped value
72
+ * is never checked, valid or not, which is the same as the even-default degrade its callers already take.
73
+ */
74
+ function assertRatioInRange(ratio) {
75
+ if (!Number.isFinite(ratio) || ratio <= 0 || ratio >= 1) throw new Error(`ratio must be strictly between 0 and 1 — got ${ratio}`);
76
+ }
77
+ //#endregion
78
+ //#region src/wait-output.ts
79
+ /** How long a polling backend sleeps between reads when the caller names no cadence. */
80
+ const DEFAULT_WAIT_POLL_MS = 150;
81
+ /**
82
+ * The seam's own precondition on a wait pattern: EXACTLY ONE of `match`/`regex`, and a `regex` that
83
+ * compiles.
84
+ *
85
+ * Enforced here, at the seam, rather than per adapter, for `assertRatioInRange`'s reason — it is a
86
+ * universal property of what a wait pattern IS, true on every backend, not a per-backend policy. Both
87
+ * halves matter for portability in different ways: the one-of rule is refusable by herdr's CLI and by
88
+ * nothing at all on a polling backend, so leaving it to the backend would make the same call fail on
89
+ * one and silently pick a winner on another; and compiling the source turns a MALFORMED pattern into
90
+ * the same loud failure everywhere, instead of a herdr refusal on one backend and a poll that throws
91
+ * on its first read somewhere else.
92
+ *
93
+ * What it deliberately does NOT check is dialect: a pattern using ECMAScript-only syntax compiles here
94
+ * and is then herdr's own to accept or refuse (see `MuxWaitOptions.regex`). Validating against the
95
+ * intersection of two regex engines would mean shipping a third one.
96
+ */
97
+ function assertWaitPattern(opts) {
98
+ const hasMatch = opts.match != null;
99
+ const hasRegex = opts.regex != null;
100
+ if (hasMatch && hasRegex) throw new Error("wait pattern must be one of match or regex — got both");
101
+ if (!hasMatch && !hasRegex) throw new Error("wait pattern must be one of match or regex — got neither");
102
+ if (opts.match != null && opts.match === "") throw new Error("wait pattern match must not be empty");
103
+ if (opts.regex != null) try {
104
+ new RegExp(opts.regex);
105
+ } catch (err) {
106
+ throw new Error(`wait pattern regex is not a valid expression: ${opts.regex} — ${err.message}`);
107
+ }
108
+ }
109
+ /**
110
+ * Whether `output` satisfies the pattern, and the single line to point at when it does.
111
+ *
112
+ * The match runs against the WHOLE snapshot, not line by line, so a regex that spans a newline still
113
+ * hits — that is why `matchedLine` is derived separately and left absent when no single line carries
114
+ * the match on its own. Pure, so the tricky half is testable with no multiplexer at all, exactly as
115
+ * `template-capture`'s geometry derivation is.
116
+ */
117
+ function matchWaitPattern(output, opts) {
118
+ assertWaitPattern(opts);
119
+ const hit = (text) => opts.match != null ? text.includes(opts.match) : new RegExp(opts.regex).test(text);
120
+ if (!hit(output)) return {
121
+ matched: false,
122
+ output
123
+ };
124
+ const line = output.split("\n").find(hit);
125
+ return {
126
+ matched: true,
127
+ output,
128
+ ...line != null ? { matchedLine: line } : {}
129
+ };
130
+ }
131
+ /**
132
+ * `waitForOutput` for a backend with NO native wait — poll its own `read` until the pattern matches or
133
+ * the deadline passes. tmux, WezTerm and Zellij all route their seam method straight through here, so
134
+ * the three share one cadence, one deadline rule and one liveness rule rather than three copies that
135
+ * can drift; herdr overrides it with its native primitive.
136
+ *
137
+ * **Reads first, sleeps second.** The snapshot on screen when the call arrives is searched before any
138
+ * sleeping, so a pattern already printed returns immediately — the seam's stated "existing output
139
+ * counts" rule, and the same order herdr's native wait documents for itself.
140
+ *
141
+ * **A gone pane throws instead of timing out**, which is `nudge`'s rule for the same reason: a dead
142
+ * pane and a quiet one both read back empty, so without the liveness probe every dead peer would be
143
+ * reported as a timeout — a shape the caller reads as "still working" — and the real cause would be
144
+ * buried. Probed BEFORE the first read (so a pane that was already gone fails at once rather than
145
+ * after the full timeout) and again after the deadline (so a pane that died mid-wait is not reported as
146
+ * one that merely stayed quiet). Never probed per poll: that would double every backend's query load
147
+ * for a fact that only changes the verdict at the end.
148
+ */
149
+ async function pollForOutput(adapter, exec, target, opts) {
150
+ assertWaitPattern(opts);
151
+ const pollMs = opts.pollMs ?? 150;
152
+ const now = opts.now ?? (() => Date.now());
153
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
154
+ const readOpts = opts.lines != null ? { lines: opts.lines } : void 0;
155
+ assertPaneLive(adapter, exec, target);
156
+ const deadline = now() + opts.timeoutMs;
157
+ let output = "";
158
+ for (;;) {
159
+ output = adapter.read(exec, target, readOpts);
160
+ const result = matchWaitPattern(output, opts);
161
+ if (result.matched) return result;
162
+ if (now() >= deadline) break;
163
+ await sleep(pollMs);
164
+ }
165
+ assertPaneLive(adapter, exec, target);
166
+ return {
167
+ matched: false,
168
+ output
169
+ };
170
+ }
171
+ /** The liveness probe both ends of a poll share, throwing `nudge`'s named failure rather than letting a
172
+ * dead pane be reported as a quiet one. */
173
+ function assertPaneLive(adapter, exec, target) {
174
+ if (!adapter.paneExists(exec, target)) throw new Error(`wait failed: pane ${target.id} no longer exists — the pane is gone, not quiet.`);
175
+ }
176
+ //#endregion
53
177
  //#region src/mux.herdr.ts
54
178
  /**
55
179
  * herdr backend — detected via `$HERDR_ENV`. herdr (https://herdr.dev) is an agent-aware terminal
@@ -97,7 +221,7 @@ const herdrMuxAdapter = {
97
221
  } else {
98
222
  const direction = at === "pane:down" ? "down" : "right";
99
223
  const from = opts.from ? [opts.from.id] : ["--current"];
100
- const size = opts.ratio != null ? ["--ratio", String(opts.ratio)] : [];
224
+ const size = opts.ratio != null ? ["--ratio", toHerdrRatio(opts.ratio)] : [];
101
225
  const out = exec("herdr", [
102
226
  "pane",
103
227
  "split",
@@ -170,6 +294,61 @@ const herdrMuxAdapter = {
170
294
  if (opts?.lines != null) args.push("--lines", String(opts.lines));
171
295
  return exec("herdr", args) ?? "";
172
296
  },
297
+ /**
298
+ * The one backend with a NATIVE wait: `pane wait-output` blocks in herdr itself (0.7.5), so no poll
299
+ * loop is run here and no snapshot is pulled across the CLI boundary on every tick.
300
+ *
301
+ * `--source visible` is pinned rather than left to herdr's own default (`recent_unwrapped`, verified
302
+ * against 0.7.5 — the help says `recent`). The seam's rule is that a wait searches exactly what
303
+ * `read` returns, and `read` pins `visible` here; taking the default would make the same wait mean a
304
+ * different snapshot on this backend than on every polling one.
305
+ *
306
+ * Telling a TIMEOUT (an answer) from a broken wait (a failure) is the whole difficulty, because herdr
307
+ * spells both the same way: exit 1 with an error envelope on stderr, so `Exec` yields `null` for
308
+ * either. Two tiers answer it, in order:
309
+ *
310
+ * 1. **The envelope's `code`**, when the runner captured stderr into `lastError` (verified against
311
+ * 0.7.5: `{"error":{"code":"timeout",…}}` vs `{"error":{"code":"pane_not_found",…}}`). Exact.
312
+ * 2. **A live pane that actually consumed the deadline**, when it did not. `Exec.lastError` is
313
+ * specified as a diagnostic and NEVER a control-flow signal — a runner that discards stderr must
314
+ * still work — so the code cannot be the only answer. Liveness alone is not enough either, and the
315
+ * reason is a whole released version of the backend: herdr 0.7.4 has no `pane wait-output` at all,
316
+ * so it answers with clap's usage text (not an envelope) INSTANTLY, and a liveness-only rule reads
317
+ * that as "timed out" — a silently wrong answer for a wait that never ran. Elapsed time is the fact
318
+ * that separates them and needs no stderr: a wait that returns in a fraction of its own timeout did
319
+ * not wait. Both must hold — the pane is live AND the deadline was spent — or this throws.
320
+ *
321
+ * A timeout costs ONE extra `read`, because herdr's timeout envelope carries no snapshot and the seam
322
+ * promises the caller the evidence its verdict was reached on. It is taken at the deadline, so it is
323
+ * the same "last look at the pane" a polling backend returns, one poll interval later.
324
+ */
325
+ async waitForOutput(exec, target, opts) {
326
+ assertWaitPattern(opts);
327
+ const now = opts.now ?? (() => Date.now());
328
+ const pattern = opts.match != null ? ["--match", opts.match] : ["--regex", opts.regex];
329
+ const args = [
330
+ "pane",
331
+ "wait-output",
332
+ target.id,
333
+ "--source",
334
+ "visible",
335
+ "--timeout",
336
+ String(opts.timeoutMs)
337
+ ];
338
+ args.push(...pattern);
339
+ if (opts.lines != null) args.push("--lines", String(opts.lines));
340
+ const started = now();
341
+ const out = exec("herdr", args);
342
+ if (out == null) {
343
+ if (!isHerdrWaitTimeout(exec, target, opts.timeoutMs, now() - started)) throw new Error(withReason(exec, `herdr pane wait-output failed for pane ${target.id}`));
344
+ const readOpts = opts.lines != null ? { lines: opts.lines } : void 0;
345
+ return {
346
+ matched: false,
347
+ output: herdrMuxAdapter.read(exec, target, readOpts)
348
+ };
349
+ }
350
+ return parseWaitOutput(out);
351
+ },
173
352
  focus(exec, target) {
174
353
  const { workspaceId, tabId } = parsePaneLocation$1(exec("herdr", [
175
354
  "pane",
@@ -230,10 +409,12 @@ const herdrMuxAdapter = {
230
409
  return panes.filter((p) => typeof p?.pane_id === "string").map((p) => {
231
410
  const harness = p.agent || void 0;
232
411
  const label = p.label || void 0;
412
+ const agentStatus = toAgentStatus(p.agent_status);
233
413
  return {
234
414
  id: p.pane_id,
235
415
  mux: "herdr",
236
416
  ...harness !== void 0 ? { harness } : {},
417
+ ...agentStatus !== void 0 ? { agentStatus } : {},
237
418
  ...p.cwd !== void 0 ? { cwd: p.cwd } : {},
238
419
  ...label !== void 0 ? { label } : {}
239
420
  };
@@ -298,9 +479,54 @@ const herdrMuxAdapter = {
298
479
  if (tabs.length === 0) throw new Error(`herdr reported no usable tabs in workspace ${workspaceId}: ${out.slice(0, 200)}`);
299
480
  return tabs;
300
481
  }
301
- }
482
+ },
483
+ agentLifecycle: { waitForState(exec, target, opts) {
484
+ const until = opts.until ?? [];
485
+ const status = parseReachedAgentStatus(exec("herdr", [
486
+ "agent",
487
+ "wait",
488
+ target.id,
489
+ ...until.flatMap((state) => ["--until", state]),
490
+ ...opts.timeoutMs != null ? ["--timeout", String(opts.timeoutMs)] : []
491
+ ]));
492
+ if (!status) throw new Error(withReason(exec, `herdr agent wait reported no reached agent_status for pane ${target.id}`));
493
+ return status;
494
+ } }
302
495
  };
303
496
  /**
497
+ * The set of `agent_status` values herdr 0.7.5 reports — the runtime witness of the `AgentStatus`
498
+ * type, so a string read off a herdr envelope can be NARROWED to it rather than cast. A value outside
499
+ * this set is treated as absent (the feed said something this build does not model), never forced into
500
+ * the type.
501
+ */
502
+ const AGENT_STATUSES = [
503
+ "idle",
504
+ "working",
505
+ "blocked",
506
+ "done",
507
+ "unknown"
508
+ ];
509
+ /** A value narrowed to `AgentStatus`, or `undefined` for anything else (a non-string, an empty string,
510
+ * or a status this build does not model) — the normalization both the listing and the wait share. */
511
+ function toAgentStatus(value) {
512
+ return typeof value === "string" && AGENT_STATUSES.includes(value) ? value : void 0;
513
+ }
514
+ /**
515
+ * The `AgentStatus` a `herdr agent wait` run reached, read defensively from its JSON envelope —
516
+ * `{"result":{"agent":{…,"agent_status":"idle",…},"type":"agent_info"}}` (verified against 0.7.5), so
517
+ * the reached status lives at `.result.agent.agent_status`. Every unresolvable shape — `out` is null
518
+ * (an Exec failure), the JSON does not parse, or the field is missing/empty/unmodeled — folds to
519
+ * `undefined`, exactly as `parsePaneRecord`/`isPaneFocused` fold, so the caller states its own failure.
520
+ */
521
+ function parseReachedAgentStatus(out) {
522
+ if (out == null) return void 0;
523
+ try {
524
+ return toAgentStatus(JSON.parse(out)?.result?.agent?.agent_status);
525
+ } catch {
526
+ return;
527
+ }
528
+ }
529
+ /**
304
530
  * The rects of the region `paneId` sits in, joined with the cwd/label half.
305
531
  *
306
532
  * Two sources, because herdr splits the answer across two verbs: `pane layout` reports the region's
@@ -391,6 +617,16 @@ function envFlags(env) {
391
617
  return env ? Object.entries(env).flatMap(([k, v]) => ["--env", `${k}=${v}`]) : [];
392
618
  }
393
619
  /**
620
+ * `--ratio` takes the seam's number VERBATIM — herdr sizes the ORIGINAL pane, so no inversion, unlike
621
+ * tmux's `-l` and wezterm's `--percent`. The guard is the same one those two render helpers call: the
622
+ * seam refuses an out-of-range ratio here rather than pass `--ratio 5` (or `0`) through to a split herdr
623
+ * would then size wrong.
624
+ */
625
+ function toHerdrRatio(ratio) {
626
+ assertRatioInRange(ratio);
627
+ return String(ratio);
628
+ }
629
+ /**
394
630
  * Launch a command in a worktree's root pane, carrying env the worktree verb could not set at birth.
395
631
  * The prefix-or-warn rule is the seam's (`env-fallback.ts`); this is the one route that invokes it,
396
632
  * because it is the one route that loses env. With a command, env rides in as a prefix; with none and
@@ -437,6 +673,65 @@ function nonEmpty(value) {
437
673
  return typeof value === "string" && value !== "" ? value : void 0;
438
674
  }
439
675
  /**
676
+ * The `code` of a herdr error envelope, when the runner captured one — how a wait's TIMEOUT (an answer)
677
+ * is told from any other failure (a throw). Read from `Exec.lastError` because that is where herdr's
678
+ * envelope lands: it is written to stderr with exit 1, so stdout is `null` for every failure alike and
679
+ * the code is the only thing that separates them. Defensive throughout — no reason, unparseable JSON, or
680
+ * a missing/non-string code all answer `undefined`, which routes to the throw rather than to a silent
681
+ * "timed out" the backend never said.
682
+ */
683
+ /**
684
+ * How much of its own timeout a wait must actually spend before a failure is believed to BE that
685
+ * timeout. A fraction rather than the whole, because process start-up and clock granularity make an
686
+ * exact-or-greater comparison flaky on a real runner; wide enough that the case it exists to catch — a
687
+ * herdr with no `wait-output` subcommand, which returns in milliseconds — is nowhere near it.
688
+ */
689
+ const HERDR_WAIT_ELAPSED_RATIO = .9;
690
+ /**
691
+ * Whether a failed `pane wait-output` was the DEADLINE passing rather than the wait breaking — the
692
+ * two-tier rule `waitForOutput` documents, kept out of the method so the tiers read as one decision.
693
+ *
694
+ * The envelope's code answers when the runner captured one. Otherwise the answer needs two facts, and
695
+ * neither alone is enough: the pane must be LIVE (a gone pane is a failure, `pollForOutput`'s rule) and
696
+ * the call must have SPENT the deadline (a wait that returned instantly never ran — herdr 0.7.4, whose
697
+ * usage text for an unknown subcommand is not an envelope to read a code from).
698
+ */
699
+ function isHerdrWaitTimeout(exec, target, timeoutMs, elapsedMs) {
700
+ const code = herdrErrorCode(exec.lastError);
701
+ if (code != null) return code === "timeout";
702
+ if (elapsedMs < timeoutMs * HERDR_WAIT_ELAPSED_RATIO) return false;
703
+ return herdrMuxAdapter.paneExists(exec, target);
704
+ }
705
+ function herdrErrorCode(reason) {
706
+ if (!reason) return void 0;
707
+ try {
708
+ return nonEmpty(JSON.parse(reason)?.error?.code);
709
+ } catch {
710
+ return;
711
+ }
712
+ }
713
+ /**
714
+ * A successful `pane wait-output` envelope: the snapshot it matched in, and the line it matched on.
715
+ *
716
+ * `matched` is `true` by construction — herdr exits 0 only on a match, so reaching here IS the match;
717
+ * the parse only fills in the evidence. Defensive for the same reason `parsePaneRecord` is: a herdr
718
+ * build that reshapes the envelope degrades to a match with no snapshot, never to a failed wait.
719
+ */
720
+ function parseWaitOutput(out) {
721
+ let text;
722
+ let line;
723
+ try {
724
+ const result = JSON.parse(out)?.result;
725
+ text = nonEmpty(result?.read?.text);
726
+ line = nonEmpty(result?.matched_line);
727
+ } catch {}
728
+ return {
729
+ matched: true,
730
+ output: text ?? "",
731
+ ...line != null ? { matchedLine: line } : {}
732
+ };
733
+ }
734
+ /**
440
735
  * The pane's workspace and tab, or a throw — so `focus` never issues a workspace/tab switch against a
441
736
  * pane it couldn't actually resolve.
442
737
  */
@@ -778,6 +1073,9 @@ const tmuxMuxAdapter = {
778
1073
  if (opts?.lines != null) args.push("-S", `-${opts.lines}`);
779
1074
  return exec("tmux", args) ?? "";
780
1075
  },
1076
+ waitForOutput(exec, target, opts) {
1077
+ return pollForOutput(tmuxMuxAdapter, exec, target, opts);
1078
+ },
781
1079
  focus(exec, target) {
782
1080
  const { sessionName, windowId } = parsePaneLocation(exec("tmux", [
783
1081
  "list-panes",
@@ -1018,6 +1316,7 @@ function splitOpenReport(out, command) {
1018
1316
  * the same thing without first querying the region's size.
1019
1317
  */
1020
1318
  function toTmuxSize(ratio) {
1319
+ assertRatioInRange(ratio);
1021
1320
  return `${Math.round((1 - ratio) * 100)}%`;
1022
1321
  }
1023
1322
  /**
@@ -1211,6 +1510,9 @@ function createWeztermAdapter(deps) {
1211
1510
  if (opts?.lines != null) args.push("--start-line", String(-opts.lines));
1212
1511
  return exec("wezterm", args) ?? "";
1213
1512
  },
1513
+ waitForOutput(exec, target, opts) {
1514
+ return pollForOutput(adapter, exec, target, opts);
1515
+ },
1214
1516
  focus(exec, target) {
1215
1517
  exec("wezterm", [
1216
1518
  "cli",
@@ -1326,6 +1628,7 @@ function weztermCwd(cwd) {
1326
1628
  * probe note, #47) — the same inversion tmux's `-l` needs, unlike herdr's pass-through.
1327
1629
  */
1328
1630
  function toWeztermSize(ratio) {
1631
+ assertRatioInRange(ratio);
1329
1632
  return String(Math.round((1 - ratio) * 100));
1330
1633
  }
1331
1634
  /**
@@ -1535,6 +1838,9 @@ function createZellijAdapter(deps) {
1535
1838
  target.id
1536
1839
  ]) ?? "";
1537
1840
  },
1841
+ waitForOutput(exec, target, opts) {
1842
+ return pollForOutput(adapter, exec, target, opts);
1843
+ },
1538
1844
  focus(exec, target) {
1539
1845
  exec("zellij", [
1540
1846
  "action",
@@ -1891,6 +2197,15 @@ function isStaged(visible, message) {
1891
2197
  * A pane that no longer exists is rejected up front rather than retried: a gone pane and a booting
1892
2198
  * one both read back empty, so without the liveness probe the retry loop reports a dead peer as
1893
2199
  * "never took the turn" — a boot-race shape — and buries the real cause.
2200
+ *
2201
+ * **Not built on `waitForOutput`, deliberately.** The two look alike and wait on opposite conditions:
2202
+ * `waitForOutput` returns when a pattern APPEARS anywhere in the snapshot, while nudge returns when the
2203
+ * message DISAPPEARS from the input box at the bottom (`isStaged`) — a negative, position-sensitive
2204
+ * condition the wait primitive cannot express, and one that must not be satisfied by the same text
2205
+ * sitting up in the transcript, which is exactly where a submitted message ends up. Nor is the loop body
2206
+ * the same: nudge does not merely observe between polls, it re-submits, so its "poll" is a corrective
2207
+ * action with its own attempt budget rather than a read. What the two DO share is the liveness rule —
2208
+ * a gone pane throws instead of being reported as a quiet one — and `pollForOutput` adopts it from here.
1894
2209
  */
1895
2210
  async function nudge(adapter, exec, target, message, opts = {}) {
1896
2211
  const attempts = opts.attempts ?? DEFAULT_ATTEMPTS;
@@ -1978,6 +2293,7 @@ function resolveMux(env, deps) {
1978
2293
  sendKeys: (target, keys, d) => raw.sendKeys(pick(d), target, keys),
1979
2294
  submit: (target, text, d) => raw.submit(pick(d), target, text),
1980
2295
  read: (target, opts, d) => raw.read(pick(d), target, opts),
2296
+ waitForOutput: (target, opts, d) => raw.waitForOutput(pick(d), target, opts),
1981
2297
  focus: (target, d) => raw.focus(pick(d), target),
1982
2298
  teardown: (target, d) => raw.teardown(pick(d), target),
1983
2299
  paneExists: (target, d) => raw.paneExists(pick(d), target),
@@ -1985,9 +2301,13 @@ function resolveMux(env, deps) {
1985
2301
  listPanes: (d) => raw.listPanes(pick(d)),
1986
2302
  nudge: (target, message, opts, d) => nudge(raw, pick(d), target, message, opts),
1987
2303
  ...raw.worktree ? { worktree: bindWorktree(raw.worktree, pick) } : {},
1988
- ...raw.regions ? { regions: bindRegions(raw.regions, pick) } : {}
2304
+ ...raw.regions ? { regions: bindRegions(raw.regions, pick) } : {},
2305
+ ...raw.agentLifecycle ? { agentLifecycle: bindAgentLifecycle(raw.agentLifecycle, pick) } : {}
1989
2306
  };
1990
2307
  }
2308
+ function bindAgentLifecycle(agent, pick) {
2309
+ return { waitForState: (target, opts, d) => agent.waitForState(pick(d), target, opts) };
2310
+ }
1991
2311
  function bindWorktree(wt, pick) {
1992
2312
  return {
1993
2313
  createInWorkspace: (opts, d) => wt.createInWorkspace(pick(d), opts),
@@ -2020,6 +2340,6 @@ function callerPane(adapter, env) {
2020
2340
  return self && self.mux === adapter.name ? { id: self.pane } : void 0;
2021
2341
  }
2022
2342
  //#endregion
2023
- export { envFallback as _, nudge as a, createZellijAdapter as c, weztermMuxAdapter as d, nodeNewId as f, herdrMuxAdapter as g, tmuxMuxAdapter as h, isStaged as i, zellijMuxAdapter as l, TMUX_WORKSPACE_GROUP_OPTION as m, resolveMux as n, currentPane as o, TMUX_TAB_NAME_OPTION as p, resolveMuxAdapter as r, probeMultiplexer as s, callerPane as t, createWeztermAdapter as u };
2343
+ export { DEFAULT_WAIT_POLL_MS as _, nudge as a, pollForOutput as b, createZellijAdapter as c, weztermMuxAdapter as d, nodeNewId as f, herdrMuxAdapter as g, tmuxMuxAdapter as h, isStaged as i, zellijMuxAdapter as l, TMUX_WORKSPACE_GROUP_OPTION as m, resolveMux as n, currentPane as o, TMUX_TAB_NAME_OPTION as p, resolveMuxAdapter as r, probeMultiplexer as s, callerPane as t, createWeztermAdapter as u, assertWaitPattern as v, envFallback as x, matchWaitPattern as y };
2024
2344
 
2025
- //# sourceMappingURL=backend-DnrbmL6N.mjs.map
2345
+ //# sourceMappingURL=backend-Cif257ji.mjs.map