@wastedcode/claudemux 0.2.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.
Files changed (249) hide show
  1. package/CHANGELOG.md +257 -0
  2. package/LICENSE +21 -0
  3. package/README.md +493 -0
  4. package/bin/claudemux +6 -0
  5. package/dist/agents/claude.d.ts +3 -0
  6. package/dist/agents/claude.d.ts.map +1 -0
  7. package/dist/agents/claude.js +585 -0
  8. package/dist/agents/claude.js.map +1 -0
  9. package/dist/agents/index.d.ts +2 -0
  10. package/dist/agents/index.d.ts.map +1 -0
  11. package/dist/agents/index.js +2 -0
  12. package/dist/agents/index.js.map +1 -0
  13. package/dist/agents/types.d.ts +252 -0
  14. package/dist/agents/types.d.ts.map +1 -0
  15. package/dist/agents/types.js +2 -0
  16. package/dist/agents/types.js.map +1 -0
  17. package/dist/backends/tmux/capture.d.ts +25 -0
  18. package/dist/backends/tmux/capture.d.ts.map +1 -0
  19. package/dist/backends/tmux/capture.js +35 -0
  20. package/dist/backends/tmux/capture.js.map +1 -0
  21. package/dist/backends/tmux/exec.d.ts +105 -0
  22. package/dist/backends/tmux/exec.d.ts.map +1 -0
  23. package/dist/backends/tmux/exec.js +226 -0
  24. package/dist/backends/tmux/exec.js.map +1 -0
  25. package/dist/backends/tmux/index.d.ts +22 -0
  26. package/dist/backends/tmux/index.d.ts.map +1 -0
  27. package/dist/backends/tmux/index.js +108 -0
  28. package/dist/backends/tmux/index.js.map +1 -0
  29. package/dist/backends/tmux/keys.d.ts +38 -0
  30. package/dist/backends/tmux/keys.d.ts.map +1 -0
  31. package/dist/backends/tmux/keys.js +63 -0
  32. package/dist/backends/tmux/keys.js.map +1 -0
  33. package/dist/backends/tmux/options.d.ts +24 -0
  34. package/dist/backends/tmux/options.d.ts.map +1 -0
  35. package/dist/backends/tmux/options.js +84 -0
  36. package/dist/backends/tmux/options.js.map +1 -0
  37. package/dist/backends/tmux/sessions.d.ts +70 -0
  38. package/dist/backends/tmux/sessions.d.ts.map +1 -0
  39. package/dist/backends/tmux/sessions.js +156 -0
  40. package/dist/backends/tmux/sessions.js.map +1 -0
  41. package/dist/backends/tmux/socket.d.ts +26 -0
  42. package/dist/backends/tmux/socket.d.ts.map +1 -0
  43. package/dist/backends/tmux/socket.js +31 -0
  44. package/dist/backends/tmux/socket.js.map +1 -0
  45. package/dist/backends/types.d.ts +110 -0
  46. package/dist/backends/types.d.ts.map +1 -0
  47. package/dist/backends/types.js +24 -0
  48. package/dist/backends/types.js.map +1 -0
  49. package/dist/cli/ask.d.ts +11 -0
  50. package/dist/cli/ask.d.ts.map +1 -0
  51. package/dist/cli/ask.js +17 -0
  52. package/dist/cli/ask.js.map +1 -0
  53. package/dist/cli/capture.d.ts +8 -0
  54. package/dist/cli/capture.d.ts.map +1 -0
  55. package/dist/cli/capture.js +15 -0
  56. package/dist/cli/capture.js.map +1 -0
  57. package/dist/cli/context.d.ts +71 -0
  58. package/dist/cli/context.d.ts.map +1 -0
  59. package/dist/cli/context.js +82 -0
  60. package/dist/cli/context.js.map +1 -0
  61. package/dist/cli/exists.d.ts +7 -0
  62. package/dist/cli/exists.d.ts.map +1 -0
  63. package/dist/cli/exists.js +16 -0
  64. package/dist/cli/exists.js.map +1 -0
  65. package/dist/cli/interrupt.d.ts +10 -0
  66. package/dist/cli/interrupt.d.ts.map +1 -0
  67. package/dist/cli/interrupt.js +13 -0
  68. package/dist/cli/interrupt.js.map +1 -0
  69. package/dist/cli/kill.d.ts +7 -0
  70. package/dist/cli/kill.d.ts.map +1 -0
  71. package/dist/cli/kill.js +14 -0
  72. package/dist/cli/kill.js.map +1 -0
  73. package/dist/cli/list.d.ts +10 -0
  74. package/dist/cli/list.d.ts.map +1 -0
  75. package/dist/cli/list.js +19 -0
  76. package/dist/cli/list.js.map +1 -0
  77. package/dist/cli/main.d.ts +13 -0
  78. package/dist/cli/main.d.ts.map +1 -0
  79. package/dist/cli/main.js +143 -0
  80. package/dist/cli/main.js.map +1 -0
  81. package/dist/cli/messages.d.ts +9 -0
  82. package/dist/cli/messages.d.ts.map +1 -0
  83. package/dist/cli/messages.js +13 -0
  84. package/dist/cli/messages.js.map +1 -0
  85. package/dist/cli/respond.d.ts +10 -0
  86. package/dist/cli/respond.d.ts.map +1 -0
  87. package/dist/cli/respond.js +23 -0
  88. package/dist/cli/respond.js.map +1 -0
  89. package/dist/cli/resume.d.ts +12 -0
  90. package/dist/cli/resume.d.ts.map +1 -0
  91. package/dist/cli/resume.js +21 -0
  92. package/dist/cli/resume.js.map +1 -0
  93. package/dist/cli/send.d.ts +9 -0
  94. package/dist/cli/send.d.ts.map +1 -0
  95. package/dist/cli/send.js +16 -0
  96. package/dist/cli/send.js.map +1 -0
  97. package/dist/cli/spawn.d.ts +14 -0
  98. package/dist/cli/spawn.d.ts.map +1 -0
  99. package/dist/cli/spawn.js +21 -0
  100. package/dist/cli/spawn.js.map +1 -0
  101. package/dist/cli/state.d.ts +7 -0
  102. package/dist/cli/state.d.ts.map +1 -0
  103. package/dist/cli/state.js +11 -0
  104. package/dist/cli/state.js.map +1 -0
  105. package/dist/cli/turn-complete.d.ts +8 -0
  106. package/dist/cli/turn-complete.d.ts.map +1 -0
  107. package/dist/cli/turn-complete.js +14 -0
  108. package/dist/cli/turn-complete.js.map +1 -0
  109. package/dist/cli/wait.d.ts +13 -0
  110. package/dist/cli/wait.d.ts.map +1 -0
  111. package/dist/cli/wait.js +17 -0
  112. package/dist/cli/wait.js.map +1 -0
  113. package/dist/compose.d.ts +81 -0
  114. package/dist/compose.d.ts.map +1 -0
  115. package/dist/compose.js +64 -0
  116. package/dist/compose.js.map +1 -0
  117. package/dist/errors.d.ts +250 -0
  118. package/dist/errors.d.ts.map +1 -0
  119. package/dist/errors.js +300 -0
  120. package/dist/errors.js.map +1 -0
  121. package/dist/index.d.ts +22 -0
  122. package/dist/index.d.ts.map +1 -0
  123. package/dist/index.js +17 -0
  124. package/dist/index.js.map +1 -0
  125. package/dist/io/baseline.d.ts +53 -0
  126. package/dist/io/baseline.d.ts.map +1 -0
  127. package/dist/io/baseline.js +97 -0
  128. package/dist/io/baseline.js.map +1 -0
  129. package/dist/io/capture.d.ts +15 -0
  130. package/dist/io/capture.d.ts.map +1 -0
  131. package/dist/io/capture.js +13 -0
  132. package/dist/io/capture.js.map +1 -0
  133. package/dist/io/interrupt.d.ts +46 -0
  134. package/dist/io/interrupt.d.ts.map +1 -0
  135. package/dist/io/interrupt.js +60 -0
  136. package/dist/io/interrupt.js.map +1 -0
  137. package/dist/io/respond.d.ts +28 -0
  138. package/dist/io/respond.d.ts.map +1 -0
  139. package/dist/io/respond.js +33 -0
  140. package/dist/io/respond.js.map +1 -0
  141. package/dist/io/send.d.ts +44 -0
  142. package/dist/io/send.d.ts.map +1 -0
  143. package/dist/io/send.js +66 -0
  144. package/dist/io/send.js.map +1 -0
  145. package/dist/io/stabilize.d.ts +28 -0
  146. package/dist/io/stabilize.d.ts.map +1 -0
  147. package/dist/io/stabilize.js +20 -0
  148. package/dist/io/stabilize.js.map +1 -0
  149. package/dist/io/wait.d.ts +47 -0
  150. package/dist/io/wait.d.ts.map +1 -0
  151. package/dist/io/wait.js +117 -0
  152. package/dist/io/wait.js.map +1 -0
  153. package/dist/observe/incremental.d.ts +28 -0
  154. package/dist/observe/incremental.d.ts.map +1 -0
  155. package/dist/observe/incremental.js +57 -0
  156. package/dist/observe/incremental.js.map +1 -0
  157. package/dist/observe/observer.d.ts +86 -0
  158. package/dist/observe/observer.d.ts.map +1 -0
  159. package/dist/observe/observer.js +167 -0
  160. package/dist/observe/observer.js.map +1 -0
  161. package/dist/observe/session-observer.d.ts +49 -0
  162. package/dist/observe/session-observer.d.ts.map +1 -0
  163. package/dist/observe/session-observer.js +123 -0
  164. package/dist/observe/session-observer.js.map +1 -0
  165. package/dist/session/adopt.d.ts +52 -0
  166. package/dist/session/adopt.d.ts.map +1 -0
  167. package/dist/session/adopt.js +57 -0
  168. package/dist/session/adopt.js.map +1 -0
  169. package/dist/session/boot.d.ts +66 -0
  170. package/dist/session/boot.d.ts.map +1 -0
  171. package/dist/session/boot.js +216 -0
  172. package/dist/session/boot.js.map +1 -0
  173. package/dist/session/constants.d.ts +57 -0
  174. package/dist/session/constants.d.ts.map +1 -0
  175. package/dist/session/constants.js +54 -0
  176. package/dist/session/constants.js.map +1 -0
  177. package/dist/session/create.d.ts +88 -0
  178. package/dist/session/create.d.ts.map +1 -0
  179. package/dist/session/create.js +66 -0
  180. package/dist/session/create.js.map +1 -0
  181. package/dist/session/default-backend.d.ts +27 -0
  182. package/dist/session/default-backend.d.ts.map +1 -0
  183. package/dist/session/default-backend.js +58 -0
  184. package/dist/session/default-backend.js.map +1 -0
  185. package/dist/session/handle.d.ts +63 -0
  186. package/dist/session/handle.d.ts.map +1 -0
  187. package/dist/session/handle.js +284 -0
  188. package/dist/session/handle.js.map +1 -0
  189. package/dist/session/hooks.d.ts +37 -0
  190. package/dist/session/hooks.d.ts.map +1 -0
  191. package/dist/session/hooks.js +42 -0
  192. package/dist/session/hooks.js.map +1 -0
  193. package/dist/session/mutex.d.ts +15 -0
  194. package/dist/session/mutex.d.ts.map +1 -0
  195. package/dist/session/mutex.js +29 -0
  196. package/dist/session/mutex.js.map +1 -0
  197. package/dist/session/recover.d.ts +43 -0
  198. package/dist/session/recover.d.ts.map +1 -0
  199. package/dist/session/recover.js +45 -0
  200. package/dist/session/recover.js.map +1 -0
  201. package/dist/session/ref.d.ts +2 -0
  202. package/dist/session/ref.d.ts.map +1 -0
  203. package/dist/session/ref.js +5 -0
  204. package/dist/session/ref.js.map +1 -0
  205. package/dist/session/registry.d.ts +31 -0
  206. package/dist/session/registry.d.ts.map +1 -0
  207. package/dist/session/registry.js +32 -0
  208. package/dist/session/registry.js.map +1 -0
  209. package/dist/session/resolve.d.ts +30 -0
  210. package/dist/session/resolve.d.ts.map +1 -0
  211. package/dist/session/resolve.js +24 -0
  212. package/dist/session/resolve.js.map +1 -0
  213. package/dist/session/resume.d.ts +68 -0
  214. package/dist/session/resume.d.ts.map +1 -0
  215. package/dist/session/resume.js +54 -0
  216. package/dist/session/resume.js.map +1 -0
  217. package/dist/session/spawn-boot.d.ts +44 -0
  218. package/dist/session/spawn-boot.d.ts.map +1 -0
  219. package/dist/session/spawn-boot.js +87 -0
  220. package/dist/session/spawn-boot.js.map +1 -0
  221. package/dist/session/validate.d.ts +10 -0
  222. package/dist/session/validate.d.ts.map +1 -0
  223. package/dist/session/validate.js +0 -0
  224. package/dist/session/validate.js.map +1 -0
  225. package/dist/state/classifier.d.ts +29 -0
  226. package/dist/state/classifier.d.ts.map +1 -0
  227. package/dist/state/classifier.js +37 -0
  228. package/dist/state/classifier.js.map +1 -0
  229. package/dist/state/types.d.ts +32 -0
  230. package/dist/state/types.d.ts.map +1 -0
  231. package/dist/state/types.js +2 -0
  232. package/dist/state/types.js.map +1 -0
  233. package/dist/types.d.ts +401 -0
  234. package/dist/types.d.ts.map +1 -0
  235. package/dist/types.js +9 -0
  236. package/dist/types.js.map +1 -0
  237. package/dist/util/ansi.d.ts +14 -0
  238. package/dist/util/ansi.d.ts.map +1 -0
  239. package/dist/util/ansi.js +18 -0
  240. package/dist/util/ansi.js.map +1 -0
  241. package/dist/util/emitter.d.ts +17 -0
  242. package/dist/util/emitter.d.ts.map +1 -0
  243. package/dist/util/emitter.js +33 -0
  244. package/dist/util/emitter.js.map +1 -0
  245. package/dist/util/sleep.d.ts +8 -0
  246. package/dist/util/sleep.d.ts.map +1 -0
  247. package/dist/util/sleep.js +10 -0
  248. package/dist/util/sleep.js.map +1 -0
  249. package/package.json +50 -0
package/dist/errors.js ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Typed errors thrown by the claudemux substrate. Every error carries the
3
+ * session name in its message so a consumer logging an unknown failure
4
+ * still has the context they need.
5
+ *
6
+ * No bare `Error` is ever thrown from the library. The classes here are
7
+ * exhaustive for the current public surface.
8
+ */
9
+ /** Base class — consumers can `catch (e: ClaudemuxError)` for the union. */
10
+ export class ClaudemuxError extends Error {
11
+ /** The session this error pertains to. */
12
+ sessionName;
13
+ constructor(message, sessionName) {
14
+ super(`[claudemux:${sessionName}] ${message}`);
15
+ this.name = new.target.name;
16
+ this.sessionName = sessionName;
17
+ }
18
+ }
19
+ /**
20
+ * Thrown by {@link create} when a session with the requested name already
21
+ * exists. The substrate never silently adopts an existing session — reconnect
22
+ * to the live session with {@link adopt} instead.
23
+ */
24
+ export class SessionExists extends ClaudemuxError {
25
+ constructor(sessionName) {
26
+ super("session already exists; refusing to silently adopt — use adopt() to reconnect to a live session", sessionName);
27
+ }
28
+ }
29
+ /**
30
+ * Thrown by {@link create} when a caller-supplied `agentSessionId` is not a
31
+ * well-formed v4 UUID. Thrown *before spawn*, at the substrate boundary.
32
+ *
33
+ * @remarks
34
+ * This is a **security boundary**, not just input hygiene. The id flows into
35
+ * the agent's argv next to its identity flag, and into the backend's per-session
36
+ * store and command grammar — where, in some backends, a bare `;` element can be
37
+ * a command separator. A v4 UUID is hex + hyphens only, so it can never be `;`,
38
+ * a path, or any token a backend would re-interpret; rejecting non-UUIDs here is
39
+ * what keeps the always-two-argv-elements injection safe. Do not "simplify" this
40
+ * to a loose check.
41
+ */
42
+ export class InvalidAgentSessionId extends ClaudemuxError {
43
+ /** The malformed value the caller passed. */
44
+ value;
45
+ constructor(value) {
46
+ super(`invalid agentSessionId ${JSON.stringify(value)}: must be a v4 UUID (e.g. the value crypto.randomUUID() produces)`,
47
+ // No session was created — mirror InvalidSessionName's placeholder.
48
+ "<invalid-agentSessionId>");
49
+ this.value = value;
50
+ }
51
+ }
52
+ /**
53
+ * Thrown by {@link create} when the caller passes an explicit
54
+ * `agentSessionId` **and** an identity flag in `extraArgs` that also selects a
55
+ * conversation id (claude: `--session-id`, `-r`/`--resume`, `--fork-session`).
56
+ * The two would fight over which id the agent runs under, so the substrate
57
+ * fails fast *before spawn* rather than silently dropping one. Pass the id one
58
+ * way or the other, not both.
59
+ *
60
+ * @remarks
61
+ * Which `extraArgs` flags count as identity flags is agent-specific knowledge,
62
+ * so the conflict is detected inside the agent's `buildArgv` (claude owns the
63
+ * flag vocabulary per the layering grep) and surfaced as this neutral error.
64
+ */
65
+ export class AgentSessionIdConflict extends ClaudemuxError {
66
+ constructor(sessionName) {
67
+ super("explicit agentSessionId conflicts with an identity flag in extraArgs " +
68
+ "(e.g. --session-id / --resume / --fork-session); pass the conversation id one way, not both", sessionName);
69
+ }
70
+ }
71
+ /**
72
+ * Thrown by the boot orchestrator when a recognized dialog matches but its
73
+ * response did not advance the pane within the dialog timeout.
74
+ */
75
+ export class DialogStuck extends ClaudemuxError {
76
+ /** The matched dialog's id (e.g. `"theme-picker"`). */
77
+ dialogId;
78
+ constructor(sessionName, dialogId) {
79
+ super(`dialog "${dialogId}" matched but did not advance after response`, sessionName);
80
+ this.dialogId = dialogId;
81
+ }
82
+ }
83
+ /**
84
+ * Thrown by **boot** ({@link create}/{@link resume}/{@link adopt}) when the REPL
85
+ * did not reach a stable ready state within `bootTimeoutMs`. NOT thrown by
86
+ * `wait()` — turn patience is the consumer's, so a turn that outlasts your budget
87
+ * is a returned `budget-exceeded` {@link TurnOutcome}, never an exception.
88
+ */
89
+ export class ReplTimeout extends ClaudemuxError {
90
+ /** The timeout budget that elapsed, in milliseconds. */
91
+ timeoutMs;
92
+ constructor(sessionName, timeoutMs) {
93
+ super(`REPL did not settle within ${timeoutMs}ms`, sessionName);
94
+ this.timeoutMs = timeoutMs;
95
+ }
96
+ }
97
+ /**
98
+ * Thrown by boot when the login-method dialog fires — claudemux assumes the
99
+ * founder is already authenticated, so this dialog firing under claudemux is
100
+ * a setup error, not an auto-answerable dialog.
101
+ */
102
+ export class LoginRequired extends ClaudemuxError {
103
+ constructor(sessionName) {
104
+ super("claude is not authenticated; the login-method dialog appeared. " +
105
+ "Run `claude` interactively once to sign in, then retry", sessionName);
106
+ }
107
+ }
108
+ /**
109
+ * Thrown by boot when the agent presents a workspace-trust dialog for a
110
+ * `cwd` the substrate has not been told to trust. Trusting a folder is an
111
+ * **authority grant** (the agent gains read/edit/execute on those files),
112
+ * so the substrate fails closed: it does **not** auto-answer the dialog —
113
+ * it throws this *before sending any keystroke* and leaves the decision to
114
+ * the consumer. Pass `trustWorkspace: true` to `create` (or `--trust-workspace`
115
+ * on the CLI) to opt in.
116
+ *
117
+ * @remarks
118
+ * Opting in writes a **persistent, global, per-cwd** trust flag to the
119
+ * agent's config (`~/.claude.json` → `projects[<abs-cwd>]`), NOT a
120
+ * session-scoped one: it outlives the claudemux process and applies to
121
+ * every future `claude` run in that path, including the user's own
122
+ * interactive sessions. Trust is sticky per `(HOME × cwd-path)` — fail-closed
123
+ * only protects the *first* run in an untrusted path; a reused checkout path
124
+ * a prior run (or the user) already trusted inherits trust silently. For
125
+ * untrusted-fork workloads (PR bots / CI), use an ephemeral unique checkout
126
+ * path or an ephemeral HOME per run.
127
+ */
128
+ export class WorkspaceUntrusted extends ClaudemuxError {
129
+ /** The cwd the agent asked to trust. */
130
+ cwd;
131
+ constructor(sessionName, cwd) {
132
+ super(`workspace at ${JSON.stringify(cwd)} is not trusted; the agent asked to trust it. Pass trustWorkspace:true (or --trust-workspace) to grant the agent read/edit/execute on this folder — note this writes a persistent, per-folder trust flag to the agent's config`, sessionName);
133
+ this.cwd = cwd;
134
+ }
135
+ }
136
+ /**
137
+ * Thrown by {@link SessionHandle.respond} when the agent declares no
138
+ * permission-prompt handling — there is no menu mapping to translate a
139
+ * {@link PromptChoice} into a keystroke, so the substrate refuses to guess a
140
+ * digit (a wrong guess could pick the broadest "allow all" option). An agent
141
+ * grows prompt handling by adding the mapping to its `AgentDef`; until then a
142
+ * consumer that hits an `awaiting{permission-prompt}` must answer the agent
143
+ * out-of-band (or run it in a non-interactive permission mode).
144
+ */
145
+ export class PromptResponseUnsupported extends ClaudemuxError {
146
+ /** The agent that has no permission-prompt mapping (e.g. a future codex def). */
147
+ agentName;
148
+ constructor(sessionName, agentName) {
149
+ super(`agent "${agentName}" declares no permission-prompt handling; respond() has no menu mapping to answer the prompt`, sessionName);
150
+ this.agentName = agentName;
151
+ }
152
+ }
153
+ /**
154
+ * Thrown by {@link SessionHandle.messagesSince} / {@link SessionHandle.turnComplete}
155
+ * when the session's transcript **cannot be located at all** — there is no
156
+ * recoverable `agentSessionId` to locate it by AND no hook edge has reported its
157
+ * path (an {@link adopt} whose recovery cache missed, a non-claudemux session, or
158
+ * a fork before its first hook edge). Reads are *blind*, not "nothing new."
159
+ *
160
+ * This is a **loud** failure on purpose: an empty read silently conflated with
161
+ * "no reply yet" sits exactly in the crash-recovery re-send path, where the wrong
162
+ * answer double-runs work. Throwing forces the consumer to handle "I can't see
163
+ * this conversation" distinctly. A genuinely empty (but *locatable*) transcript,
164
+ * or an unresolvable cursor (a sentinel/garbage value), still returns empty —
165
+ * only true unlocatability throws.
166
+ */
167
+ export class TranscriptUnlocatable extends ClaudemuxError {
168
+ constructor(sessionName) {
169
+ super("transcript cannot be located (no recoverable agentSessionId and no hook-reported path); reads are blind, not empty — persist the agentSessionId, or check `agentSessionId !== undefined` before reading", sessionName);
170
+ }
171
+ }
172
+ /**
173
+ * Thrown when the backend cannot find the named session — it has been reaped
174
+ * (a crash, a `kill`, or the box lost its backend server). The canonical "this
175
+ * session is gone" for **every** per-session op (read or write); `kill()` never
176
+ * throws it (killing a gone session is success).
177
+ */
178
+ export class SessionGone extends ClaudemuxError {
179
+ constructor(sessionName) {
180
+ super("session is gone", sessionName);
181
+ }
182
+ }
183
+ /**
184
+ * Thrown by {@link create} when the agent process **exits before the session
185
+ * becomes ready** — claudemux spawned it, but it was gone (its backend session
186
+ * reaped) before boot could reach an interactive prompt.
187
+ *
188
+ * @remarks
189
+ * **The most common cause is an `agentSessionId` collision:** claude refuses to
190
+ * silently resume or clobber an in-use conversation id — it prints
191
+ * "Session ID … is already in use" and exits. But a malformed `extraArgs` flag,
192
+ * an auth edge, or any startup crash produce the *identical* shape, and
193
+ * claudemux **cannot read which** — the substrate runs panes with
194
+ * `remain-on-exit off`, so claude's stderr is reaped before boot can capture
195
+ * it. That is the same deliberate property that lets {@link adopt} hand back a
196
+ * clean {@link SessionGone} for a crashed agent instead of a corpse handle, so
197
+ * we do not flip it to recover a diagnostic string.
198
+ *
199
+ * This is a distinct class on purpose (errors.ts reuses before adding):
200
+ * {@link SessionGone} reads as *external* reaping of a session that should
201
+ * exist, which does not carry the meaning the create path needs — *"the agent I
202
+ * just spawned rejected its own launch"* (self-inflicted exit vs external
203
+ * interference) — which is exactly what the consumer must act on.
204
+ *
205
+ * When the spawn used a caller-chosen id, it is carried on
206
+ * {@link agentSessionId} so the collision case stays actionable as structured
207
+ * data (pick another id, or resume that conversation) without scraping text.
208
+ *
209
+ * **Deferred precision:** once a `transcriptPath` helper lands (owning claude's
210
+ * cwd-slug rule in `claude.ts` per the layering grep), a pre-spawn transcript
211
+ * probe can upgrade the collision case to a precise `AgentSessionInUse` and
212
+ * fall back to this error for the other death causes — a non-breaking addition.
213
+ */
214
+ export class AgentExitedDuringBoot extends ClaudemuxError {
215
+ /** The caller-chosen id the spawn used, if any — the likely collision culprit. */
216
+ agentSessionId;
217
+ constructor(sessionName, agentSessionId) {
218
+ const withId = agentSessionId !== undefined
219
+ ? ` (spawned with agentSessionId ${agentSessionId}, which is most likely already in use)`
220
+ : "";
221
+ super(`the agent exited before the session became ready${withId}; the most common cause is an agentSessionId collision (the agent refuses to resume silently and exits)`, sessionName);
222
+ if (agentSessionId !== undefined)
223
+ this.agentSessionId = agentSessionId;
224
+ }
225
+ }
226
+ /**
227
+ * Thrown when the underlying backend (the agent's I/O substrate) is
228
+ * unreachable — the backend process failed to spawn (`spawn-failed`), its
229
+ * server was not running on the requested connection (`no-server`), or a
230
+ * backend invocation hung past its budget (`timeout`).
231
+ */
232
+ export class BackendUnreachable extends ClaudemuxError {
233
+ /** Why the backend was unreachable — see {@link BackendUnreachableKind}. */
234
+ kind;
235
+ /** The underlying spawn / connection error, if available. */
236
+ underlying;
237
+ constructor(sessionName, kind, underlying) {
238
+ super(`backend unreachable [${kind}]${underlying ? ` (${underlying.message})` : ""}`, sessionName);
239
+ this.kind = kind;
240
+ if (underlying !== undefined) {
241
+ this.underlying = underlying;
242
+ }
243
+ }
244
+ }
245
+ /**
246
+ * Thrown by `create` / `spawn` when the session name or namespace contains
247
+ * characters that the substrate cannot encode safely for the backend's
248
+ * target grammar. The substrate fails fast at the boundary instead of
249
+ * silently renaming and producing an un-addressable handle.
250
+ *
251
+ * @remarks
252
+ * Reserved set: `.`, `:`, `*`, `?`, leading `-`, any whitespace, `/`,
253
+ * `\n`, `\r`, `\\`, and the empty string.
254
+ */
255
+ export class InvalidSessionName extends ClaudemuxError {
256
+ /** Which field was invalid (`"name"` or `"namespace"`). */
257
+ field;
258
+ /** The actual value the caller passed. */
259
+ value;
260
+ /** The reason the value is rejected. */
261
+ reason;
262
+ constructor(field, value, reason) {
263
+ super(`invalid ${field} ${JSON.stringify(value)}: ${reason}`,
264
+ // Use the offending value itself so the error has *some* identifier.
265
+ // The session was never created; there is no real name to use.
266
+ `<invalid-${field}>`);
267
+ this.field = field;
268
+ this.value = value;
269
+ this.reason = reason;
270
+ }
271
+ }
272
+ /**
273
+ * Wrapper for an unexpected backend failure that isn't one of the typed
274
+ * cases above (non-zero exit + unrecognized stderr).
275
+ *
276
+ * @remarks
277
+ * The `.message` deliberately **excludes the backend argv** — the argv is
278
+ * pure backend vocabulary (the backend's own subcommand names) and leaking
279
+ * it into the user-facing message violates the substrate's "zero references
280
+ * to the backend in error messages" promise. The argv lives on `.argv` (and
281
+ * flows through `onBackendCommand`) for programmatic diagnosis; the
282
+ * human-readable message carries the exit code and the backend's own stderr
283
+ * text, which is the diagnostic value without the command vocabulary.
284
+ *
285
+ * This is the structural backstop behind the backend classifier's
286
+ * routine-case promotion: even a backend failure shape we haven't classified
287
+ * yet cannot leak the backend's command names into user-facing text.
288
+ */
289
+ export class BackendError extends ClaudemuxError {
290
+ argv;
291
+ exitCode;
292
+ stderr;
293
+ constructor(sessionName, argv, exitCode, stderr) {
294
+ super(`backend command failed (exit ${exitCode}): ${stderr.trim() || "<empty stderr>"}`, sessionName);
295
+ this.argv = argv;
296
+ this.exitCode = exitCode;
297
+ this.stderr = stderr;
298
+ }
299
+ }
300
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * claudemux — drive long-lived Claude Code sessions from Node.
3
+ *
4
+ * Public re-exports only; the file carries no logic. Internal seams
5
+ * (`Backend`, `BackendEvent`, `SendPayload`, `ClassifierRules`) stay
6
+ * internal so the public API survives a backend swap without consumer
7
+ * rewrites.
8
+ */
9
+ export { claude } from "./agents/index.js";
10
+ export type { AgentDef, BootDialog, HookEdge } from "./agents/types.js";
11
+ export { AgentExitedDuringBoot, AgentSessionIdConflict, BackendError, BackendUnreachable, ClaudemuxError, DialogStuck, InvalidAgentSessionId, InvalidSessionName, LoginRequired, PromptResponseUnsupported, ReplTimeout, SessionExists, SessionGone, TranscriptUnlocatable, WorkspaceUntrusted, } from "./errors.js";
12
+ export { ask, recover } from "./compose.js";
13
+ export type { AskResult, RecoverResult, RecoverStatus } from "./compose.js";
14
+ export { DELIVERED_QUEUED, DELIVERY_UNCONFIRMED } from "./session/handle.js";
15
+ export { adopt } from "./session/adopt.js";
16
+ export type { AdoptOptions } from "./session/adopt.js";
17
+ export { create } from "./session/create.js";
18
+ export { exists, kill, list } from "./session/registry.js";
19
+ export { resume } from "./session/resume.js";
20
+ export type { ResumeOptions } from "./session/resume.js";
21
+ export type { BackendCommandEvent, IdleState, Message, MessagePart, Progress, PromptChoice, ReadyOpts, SessionHandle, State, TurnOutcome, } from "./types.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,yBAAyB,EACzB,WAAW,EACX,aAAa,EACb,WAAW,EACX,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,YAAY,EACV,mBAAmB,EACnB,SAAS,EACT,OAAO,EACP,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,aAAa,EACb,KAAK,EACL,WAAW,GACZ,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * claudemux — drive long-lived Claude Code sessions from Node.
3
+ *
4
+ * Public re-exports only; the file carries no logic. Internal seams
5
+ * (`Backend`, `BackendEvent`, `SendPayload`, `ClassifierRules`) stay
6
+ * internal so the public API survives a backend swap without consumer
7
+ * rewrites.
8
+ */
9
+ export { claude } from "./agents/index.js";
10
+ export { AgentExitedDuringBoot, AgentSessionIdConflict, BackendError, BackendUnreachable, ClaudemuxError, DialogStuck, InvalidAgentSessionId, InvalidSessionName, LoginRequired, PromptResponseUnsupported, ReplTimeout, SessionExists, SessionGone, TranscriptUnlocatable, WorkspaceUntrusted, } from "./errors.js";
11
+ export { ask, recover } from "./compose.js";
12
+ export { DELIVERED_QUEUED, DELIVERY_UNCONFIRMED } from "./session/handle.js";
13
+ export { adopt } from "./session/adopt.js";
14
+ export { create } from "./session/create.js";
15
+ export { exists, kill, list } from "./session/registry.js";
16
+ export { resume } from "./session/resume.js";
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,yBAAyB,EACzB,WAAW,EACX,aAAa,EACb,WAAW,EACX,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,53 @@
1
+ import type { AgentDef } from "../agents/types.js";
2
+ import type { Backend, SessionRef } from "../backends/types.js";
3
+ /**
4
+ * The send→wait *baseline* — how a stateless `wait()` (the CLI reattaches in a
5
+ * fresh process each invocation) tells a turn that already completed from the
6
+ * previous turn's idle prompt.
7
+ *
8
+ * `wait()` is transition-aware: it will not accept `idle` as "turn complete"
9
+ * until it has seen the pane *leave* idle. In-process that signal is an
10
+ * observed `working` frame. But the CLI `send` and `wait` are separate
11
+ * processes; for a fast turn the agent can be back to idle before the `wait`
12
+ * process takes its first capture, so `wait` never observes `working` and
13
+ * hangs to `ReplTimeout` (bug 8a500a52).
14
+ *
15
+ * The fix: `send` records a fingerprint of the **post-submit** pane — the
16
+ * frame after the input box has cleared but *before* the agent's answer lands
17
+ * — under a session-scoped key. A later `wait` arms when the live pane
18
+ * *diverges* from that fingerprint. Capturing the post-submit frame (not the
19
+ * pre-send one) is what keeps the previous turn's idle from counting as a
20
+ * divergence: during the post-submit window the live pane *equals* the
21
+ * baseline, so `wait` correctly keeps polling instead of returning early.
22
+ */
23
+ export declare const SEND_BASELINE_KEY = "send-baseline";
24
+ /** Stable fingerprint of a bottom-N pane capture (sha256 hex — newline-safe). */
25
+ export declare function paneFingerprint(text: string): string;
26
+ /**
27
+ * After a `send`, capture the post-submit pane fingerprint: the first frame
28
+ * that (a) differs from the pre-send pane `pre` and (b) is a settled idle box
29
+ * or a working frame — i.e. the submit was accepted and the box cleared (or
30
+ * the agent already started working), but *before* the answer completes.
31
+ *
32
+ * Returns `undefined` when no clean frame appears within budget, or when the
33
+ * pre-send pane is unknown (without `pre` we cannot exclude the previous idle,
34
+ * and a wrong baseline would re-introduce the premature-return race). In that
35
+ * case `wait` falls back to arming on an observed `working` frame.
36
+ */
37
+ export declare function captureSendBaseline(backend: Backend, agent: AgentDef, ref: SessionRef, pre: string | undefined): Promise<string | undefined>;
38
+ /**
39
+ * Read the persisted post-submit baseline fingerprint, if any. Best-effort:
40
+ * returns `undefined` when unset or unreadable, so `wait` degrades to
41
+ * observed-working arming rather than failing.
42
+ */
43
+ export declare function readSendBaseline(backend: Backend, ref: SessionRef): Promise<string | undefined>;
44
+ /**
45
+ * Persist the post-submit baseline fingerprint for a later `wait`. An empty
46
+ * `fingerprint` clears the baseline (so `readSendBaseline` reports "unset") —
47
+ * `send` uses that to drop a prior turn's stale fingerprint when it couldn't
48
+ * establish a fresh one. Best-effort — a metadata write failure must never
49
+ * fail the `send` (whose contract is byte delivery); `wait` then falls back to
50
+ * observed-working arming.
51
+ */
52
+ export declare function writeSendBaseline(backend: Backend, ref: SessionRef, fingerprint: string): Promise<void>;
53
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/io/baseline.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIhE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAOjD,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,QAAQ,EACf,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAoB7B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO7B;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { CLASSIFIER_CAPTURE } from "../session/constants.js";
3
+ import { sleep } from "../util/sleep.js";
4
+ /**
5
+ * The send→wait *baseline* — how a stateless `wait()` (the CLI reattaches in a
6
+ * fresh process each invocation) tells a turn that already completed from the
7
+ * previous turn's idle prompt.
8
+ *
9
+ * `wait()` is transition-aware: it will not accept `idle` as "turn complete"
10
+ * until it has seen the pane *leave* idle. In-process that signal is an
11
+ * observed `working` frame. But the CLI `send` and `wait` are separate
12
+ * processes; for a fast turn the agent can be back to idle before the `wait`
13
+ * process takes its first capture, so `wait` never observes `working` and
14
+ * hangs to `ReplTimeout` (bug 8a500a52).
15
+ *
16
+ * The fix: `send` records a fingerprint of the **post-submit** pane — the
17
+ * frame after the input box has cleared but *before* the agent's answer lands
18
+ * — under a session-scoped key. A later `wait` arms when the live pane
19
+ * *diverges* from that fingerprint. Capturing the post-submit frame (not the
20
+ * pre-send one) is what keeps the previous turn's idle from counting as a
21
+ * divergence: during the post-submit window the live pane *equals* the
22
+ * baseline, so `wait` correctly keeps polling instead of returning early.
23
+ */
24
+ export const SEND_BASELINE_KEY = "send-baseline";
25
+ /** Total budget for `send` to observe the post-submit frame before giving up. */
26
+ const BASELINE_CAPTURE_BUDGET_MS = 2_000;
27
+ /** Capture cadence while watching for the post-submit frame. */
28
+ const BASELINE_POLL_MS = 40;
29
+ /** Stable fingerprint of a bottom-N pane capture (sha256 hex — newline-safe). */
30
+ export function paneFingerprint(text) {
31
+ return createHash("sha256").update(text).digest("hex");
32
+ }
33
+ /**
34
+ * After a `send`, capture the post-submit pane fingerprint: the first frame
35
+ * that (a) differs from the pre-send pane `pre` and (b) is a settled idle box
36
+ * or a working frame — i.e. the submit was accepted and the box cleared (or
37
+ * the agent already started working), but *before* the answer completes.
38
+ *
39
+ * Returns `undefined` when no clean frame appears within budget, or when the
40
+ * pre-send pane is unknown (without `pre` we cannot exclude the previous idle,
41
+ * and a wrong baseline would re-introduce the premature-return race). In that
42
+ * case `wait` falls back to arming on an observed `working` frame.
43
+ */
44
+ export async function captureSendBaseline(backend, agent, ref, pre) {
45
+ if (pre === undefined)
46
+ return undefined;
47
+ const deadline = Date.now() + BASELINE_CAPTURE_BUDGET_MS;
48
+ while (Date.now() < deadline) {
49
+ let text;
50
+ try {
51
+ text = await backend.capture(ref, CLASSIFIER_CAPTURE);
52
+ }
53
+ catch {
54
+ return undefined; // pane unreadable — let wait fall back to working-arm
55
+ }
56
+ // The first frame that has changed from the pre-send pane AND is either an
57
+ // empty input box (the submit cleared it) or a working frame. This is
58
+ // reached before the answer lands, so it never captures the completed-turn
59
+ // pane (which would make a later wait see "no divergence" and hang).
60
+ if (text !== pre && (agent.boot.isReady(text) || agent.rules.working(text))) {
61
+ return paneFingerprint(text);
62
+ }
63
+ await sleep(BASELINE_POLL_MS);
64
+ }
65
+ return undefined;
66
+ }
67
+ /**
68
+ * Read the persisted post-submit baseline fingerprint, if any. Best-effort:
69
+ * returns `undefined` when unset or unreadable, so `wait` degrades to
70
+ * observed-working arming rather than failing.
71
+ */
72
+ export async function readSendBaseline(backend, ref) {
73
+ try {
74
+ const v = await backend.getSessionMeta(ref, SEND_BASELINE_KEY);
75
+ return v && v.length > 0 ? v : undefined;
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
81
+ /**
82
+ * Persist the post-submit baseline fingerprint for a later `wait`. An empty
83
+ * `fingerprint` clears the baseline (so `readSendBaseline` reports "unset") —
84
+ * `send` uses that to drop a prior turn's stale fingerprint when it couldn't
85
+ * establish a fresh one. Best-effort — a metadata write failure must never
86
+ * fail the `send` (whose contract is byte delivery); `wait` then falls back to
87
+ * observed-working arming.
88
+ */
89
+ export async function writeSendBaseline(backend, ref, fingerprint) {
90
+ try {
91
+ await backend.setSessionMeta(ref, SEND_BASELINE_KEY, fingerprint);
92
+ }
93
+ catch {
94
+ /* baseline is an optimization for cross-process wait; ignore */
95
+ }
96
+ }
97
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/io/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD,iFAAiF;AACjF,MAAM,0BAA0B,GAAG,KAAK,CAAC;AACzC,gEAAgE;AAChE,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAgB,EAChB,KAAe,EACf,GAAe,EACf,GAAuB;IAEvB,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,0BAA0B,CAAC;IACzD,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC,CAAC,sDAAsD;QAC1E,CAAC;QACD,2EAA2E;QAC3E,sEAAsE;QACtE,2EAA2E;QAC3E,qEAAqE;QACrE,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC5E,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC;QACD,MAAM,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAgB,EAChB,GAAe;IAEf,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAgB,EAChB,GAAe,EACf,WAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,iBAAiB,EAAE,WAAW,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Backend, SessionRef } from "../backends/types.js";
2
+ /**
3
+ * Pure pass-through to `Backend.capture`. Lives in `src/io/` because the
4
+ * top-level public verbs ARE the io layer; the backend is below it. Future
5
+ * non-tmux backends implement the same `capture` shape, so the wrapper
6
+ * stays trivial.
7
+ *
8
+ * @param opts.ansi preserve escape sequences when `true`.
9
+ * @param opts.lines bottom-N visible lines (default: full visible region).
10
+ */
11
+ export declare function captureOnce(backend: Backend, ref: SessionRef, opts?: {
12
+ ansi?: boolean;
13
+ lines?: number;
14
+ }): Promise<string>;
15
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/io/capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,CAAC,CAEjB"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Pure pass-through to `Backend.capture`. Lives in `src/io/` because the
3
+ * top-level public verbs ARE the io layer; the backend is below it. Future
4
+ * non-tmux backends implement the same `capture` shape, so the wrapper
5
+ * stays trivial.
6
+ *
7
+ * @param opts.ansi preserve escape sequences when `true`.
8
+ * @param opts.lines bottom-N visible lines (default: full visible region).
9
+ */
10
+ export function captureOnce(backend, ref, opts) {
11
+ return backend.capture(ref, opts);
12
+ }
13
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/io/capture.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CACzB,OAAgB,EAChB,GAAe,EACf,IAAyC;IAEzC,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type { Backend, SessionRef } from "../backends/types.js";
2
+ /**
3
+ * Fire a single `Escape` at the pane — claude's own documented interrupt key
4
+ * (the classifier detects `working` by the literal `"esc to interrupt"`
5
+ * affordance, `agents/claude.ts`). ESC is already in `SendPayload`'s key union
6
+ * (`backends/types.ts`) and `sendKey` already sends it, so there is no backend
7
+ * change.
8
+ *
9
+ * ESC is sent **unconditionally** — no state-check guard. A guard would bake
10
+ * policy into the substrate and open a TOCTOU race (state read, then ESC, with
11
+ * the agent free to change state in between). ESC on an idle claude is harmless
12
+ * (it clears the input box). Gating on `state()` is the consumer's call. This
13
+ * is a mechanism, not a policy
14
+ * (`brain/decisions/0013-mechanism-not-policy-substrate-boundary.md`).
15
+ *
16
+ * That consumer-side gate is also not atomic with the ESC: a turn can finish
17
+ * between a `state()===working` read and the ESC landing — most easily across
18
+ * separate CLI processes (a short turn completes in the gap), so the ESC hits
19
+ * an already-idle agent. That is a harmless no-op, not a failure; a consumer
20
+ * that needs the interrupt to catch a turn should read `state()` and call this
21
+ * in one tight in-process sequence, not trust a stale prior-process reading.
22
+ *
23
+ * Blocks on **write delivery** plus a brief fixed settle ({@link
24
+ * INTERRUPT_SETTLE_MS}); it guarantees ESC was delivered, NOT that an in-flight
25
+ * abort has fully completed. This verb does exactly one named action — stop the
26
+ * turn — and nothing more (`brain/decisions/0013`, "a primitive does exactly the
27
+ * keystroke it names").
28
+ *
29
+ * **After interrupt(), claude does NOT return to a clean idle prompt.** It
30
+ * restores the interrupted message back into the composer, and the classifier
31
+ * reads that frame as `unknown` (never `idle`, never `working`). Two
32
+ * consequences the consumer must know:
33
+ * - `wait()` after interrupt() resolves `{ kind: "aborted" }` immediately (the
34
+ * handle records the interrupt authoritatively) — it does NOT hang waiting for
35
+ * an idle that won't come.
36
+ * - Do **not** naively `send()` a replacement after interrupt(): `send`
37
+ * pastes into the *non-empty* composer (the restored message), so the
38
+ * submission is the two texts concatenated. For a clean "interrupt and
39
+ * replace" the composer must first be cleared to empty. claude's only
40
+ * substrate-reachable composer clear is repeated ESC (its "Esc again to
41
+ * clear" ladder), so the recipe is consumer-composed and claude-specific —
42
+ * see the README "Interrupting a working agent" note. It is deliberately
43
+ * NOT bundled into this agent-agnostic verb.
44
+ */
45
+ export declare function interruptOnce(backend: Backend, ref: SessionRef): Promise<void>;
46
+ //# sourceMappingURL=interrupt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interrupt.d.ts","sourceRoot":"","sources":["../../src/io/interrupt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAehE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAGpF"}