@with-logic/elwood 0.1.1 → 0.1.3

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 (156) hide show
  1. package/CHANGELOG.md +30 -3
  2. package/CONTRIBUTING.md +104 -0
  3. package/README.md +43 -31
  4. package/SECURITY.md +42 -0
  5. package/dist/claude/model-picker.d.ts.map +1 -1
  6. package/dist/claude/model-picker.js +4 -3
  7. package/dist/claude/model-picker.js.map +1 -1
  8. package/dist/claude/model-switch-confirmation.js +9 -1
  9. package/dist/claude/model-switch-confirmation.js.map +1 -1
  10. package/dist/claude/screen-table.js +1 -1
  11. package/dist/claude/screen-table.js.map +1 -1
  12. package/dist/claude/session/build.d.ts.map +1 -1
  13. package/dist/claude/session/build.js +3 -0
  14. package/dist/claude/session/build.js.map +1 -1
  15. package/dist/claude/session/start-or-resume.js +1 -0
  16. package/dist/claude/session/start-or-resume.js.map +1 -1
  17. package/dist/cli/help.d.ts +1 -1
  18. package/dist/cli/help.d.ts.map +1 -1
  19. package/dist/cli/help.js +13 -10
  20. package/dist/cli/help.js.map +1 -1
  21. package/dist/cli/interactive/spawn.d.ts.map +1 -1
  22. package/dist/cli/interactive/spawn.js +30 -9
  23. package/dist/cli/interactive/spawn.js.map +1 -1
  24. package/dist/cli/main.d.ts.map +1 -1
  25. package/dist/cli/main.js +9 -0
  26. package/dist/cli/main.js.map +1 -1
  27. package/dist/cli/models/combined.d.ts +6 -0
  28. package/dist/cli/models/combined.d.ts.map +1 -0
  29. package/dist/cli/models/combined.js +92 -0
  30. package/dist/cli/models/combined.js.map +1 -0
  31. package/dist/cli/models/index.d.ts +2 -0
  32. package/dist/cli/models/index.d.ts.map +1 -1
  33. package/dist/cli/models/index.js +21 -7
  34. package/dist/cli/models/index.js.map +1 -1
  35. package/dist/cli/models/render.d.ts +4 -0
  36. package/dist/cli/models/render.d.ts.map +1 -1
  37. package/dist/cli/models/render.js +28 -1
  38. package/dist/cli/models/render.js.map +1 -1
  39. package/dist/cli/models/result.d.ts +13 -0
  40. package/dist/cli/models/result.d.ts.map +1 -0
  41. package/dist/cli/models/result.js +2 -0
  42. package/dist/cli/models/result.js.map +1 -0
  43. package/dist/cli/models/settings.d.ts +61 -0
  44. package/dist/cli/models/settings.d.ts.map +1 -0
  45. package/dist/cli/models/settings.js +24 -0
  46. package/dist/cli/models/settings.js.map +1 -0
  47. package/dist/cli/models/signals.d.ts +11 -0
  48. package/dist/cli/models/signals.d.ts.map +1 -0
  49. package/dist/cli/models/signals.js +21 -0
  50. package/dist/cli/models/signals.js.map +1 -0
  51. package/dist/cli/request/index.js +1 -1
  52. package/dist/cli/request/index.js.map +1 -1
  53. package/dist/cli/request/layers.d.ts +2 -0
  54. package/dist/cli/request/layers.d.ts.map +1 -1
  55. package/dist/cli/request/layers.js +9 -5
  56. package/dist/cli/request/layers.js.map +1 -1
  57. package/dist/cli/request/validation.d.ts.map +1 -1
  58. package/dist/cli/request/validation.js +0 -4
  59. package/dist/cli/request/validation.js.map +1 -1
  60. package/dist/cli/run/output.d.ts.map +1 -1
  61. package/dist/cli/run/output.js +3 -6
  62. package/dist/cli/run/output.js.map +1 -1
  63. package/dist/cli/sessions/index.d.ts +9 -2
  64. package/dist/cli/sessions/index.d.ts.map +1 -1
  65. package/dist/cli/sessions/index.js +2 -6
  66. package/dist/cli/sessions/index.js.map +1 -1
  67. package/dist/cli/sessions/settings.d.ts +11 -0
  68. package/dist/cli/sessions/settings.d.ts.map +1 -0
  69. package/dist/cli/sessions/settings.js +26 -0
  70. package/dist/cli/sessions/settings.js.map +1 -0
  71. package/dist/codex/model-picker.d.ts.map +1 -1
  72. package/dist/codex/model-picker.js +3 -2
  73. package/dist/codex/model-picker.js.map +1 -1
  74. package/dist/codex/session/build.d.ts.map +1 -1
  75. package/dist/codex/session/build.js +5 -7
  76. package/dist/codex/session/build.js.map +1 -1
  77. package/dist/codex/session/start-or-resume.js +1 -0
  78. package/dist/codex/session/start-or-resume.js.map +1 -1
  79. package/dist/core/input/index.d.ts.map +1 -1
  80. package/dist/core/input/index.js +15 -3
  81. package/dist/core/input/index.js.map +1 -1
  82. package/dist/core/models/input.d.ts +3 -0
  83. package/dist/core/models/input.d.ts.map +1 -0
  84. package/dist/core/models/input.js +9 -0
  85. package/dist/core/models/input.js.map +1 -0
  86. package/dist/core/models/picker.d.ts +2 -1
  87. package/dist/core/models/picker.d.ts.map +1 -1
  88. package/dist/core/models/picker.js +6 -5
  89. package/dist/core/models/picker.js.map +1 -1
  90. package/dist/core/models/tui-screen.d.ts +1 -1
  91. package/dist/core/models/tui-screen.d.ts.map +1 -1
  92. package/dist/core/models/tui-screen.js +26 -3
  93. package/dist/core/models/tui-screen.js.map +1 -1
  94. package/dist/runtime/session/base.d.ts.map +1 -1
  95. package/dist/runtime/session/base.js +4 -1
  96. package/dist/runtime/session/base.js.map +1 -1
  97. package/dist/runtime/session/commands.d.ts +2 -1
  98. package/dist/runtime/session/commands.d.ts.map +1 -1
  99. package/dist/runtime/session/commands.js +10 -4
  100. package/dist/runtime/session/commands.js.map +1 -1
  101. package/dist/runtime/session/lifecycle.d.ts +3 -4
  102. package/dist/runtime/session/lifecycle.d.ts.map +1 -1
  103. package/dist/runtime/session/lifecycle.js +6 -5
  104. package/dist/runtime/session/lifecycle.js.map +1 -1
  105. package/dist/state/directories.d.ts +4 -0
  106. package/dist/state/directories.d.ts.map +1 -0
  107. package/dist/state/directories.js +41 -0
  108. package/dist/state/directories.js.map +1 -0
  109. package/dist/state/files.d.ts.map +1 -1
  110. package/dist/state/files.js +25 -11
  111. package/dist/state/files.js.map +1 -1
  112. package/dist/state/private-session.d.ts.map +1 -1
  113. package/dist/state/private-session.js +2 -0
  114. package/dist/state/private-session.js.map +1 -1
  115. package/dist/state/store.d.ts.map +1 -1
  116. package/dist/state/store.js +7 -1
  117. package/dist/state/store.js.map +1 -1
  118. package/docs/cli-behavior.md +1 -1
  119. package/docs/cli-output.md +40 -10
  120. package/docs/cli.md +3 -2
  121. package/docs/codex/cli.md +57 -0
  122. package/docs/codex/config.md +47 -0
  123. package/docs/codex/hooks.md +70 -0
  124. package/examples/README.md +56 -0
  125. package/examples/full.ts +113 -0
  126. package/examples/minimal.ts +15 -0
  127. package/examples/stream.ts +39 -0
  128. package/examples/support.ts +71 -0
  129. package/package.json +10 -5
  130. package/prd/01-overview.md +21 -0
  131. package/prd/02-goals.md +47 -0
  132. package/prd/03-terminology.md +32 -0
  133. package/prd/04-architecture.md +121 -0
  134. package/prd/05-api/5.1-starting-claude.md +108 -0
  135. package/prd/05-api/5.2-resuming-claude.md +86 -0
  136. package/prd/05-api/5.3-claude-session-api.md +658 -0
  137. package/prd/05-api/5.4-events.md +145 -0
  138. package/prd/05-api/5.5-starting-codex.md +106 -0
  139. package/prd/05-api/5.6-resuming-codex.md +48 -0
  140. package/prd/05-api/5.7-codex-session-api.md +425 -0
  141. package/prd/05-api/5.8-session-api.md +122 -0
  142. package/prd/05-api/5.9-loops.md +170 -0
  143. package/prd/05-api/README.md +19 -0
  144. package/prd/06-claude-hook-bridge.md +164 -0
  145. package/prd/07-claude-policy.md +15 -0
  146. package/prd/07a-codex-hook-bridge.md +76 -0
  147. package/prd/08-state.md +127 -0
  148. package/prd/09-lifecycle.md +222 -0
  149. package/prd/10-errors.md +55 -0
  150. package/prd/11-test-app.md +53 -0
  151. package/prd/12-e2e.md +23 -0
  152. package/prd/12a-cli.md +402 -0
  153. package/prd/13-latitude.md +12 -0
  154. package/prd/14-conformance.md +333 -0
  155. package/prd/15-open-notes.md +8 -0
  156. package/prd/README.md +45 -0
@@ -0,0 +1,658 @@
1
+ ## 5.3 ClaudeSessionApi (the raw live session)
2
+
3
+ ```ts
4
+ interface ClaudeSessionApi {
5
+ readonly elwoodSessionId: string;
6
+ readonly cwd: string;
7
+ readonly status: ElwoodSessionStatus;
8
+ readonly terminal: ElwoodTerminal;
9
+
10
+ statusDecisions(): readonly ElwoodStatusDecision[];
11
+ waitForStatus(
12
+ match: (status: ElwoodSessionStatus) => boolean,
13
+ timeoutMs?: number,
14
+ ): Promise<ElwoodSessionStatus>;
15
+ waitForActivity(
16
+ match: (event: ElwoodActivityEvent) => boolean,
17
+ timeoutMs?: number,
18
+ ): Promise<ElwoodActivityEvent>;
19
+
20
+ on<E extends ClaudeEventName>(event: E, handler: ClaudeEventHandler<E>): Unsubscribe;
21
+ off<E extends ClaudeEventName>(event: E, handler: ClaudeEventHandler<E>): void;
22
+
23
+ sendPrompt(prompt: string, options?: SendOptions): Promise<void>;
24
+ sendMessage(message: string, options?: SendOptions): Promise<void>;
25
+ sendGuidance(message: string, options?: SendOptions): Promise<void>;
26
+ sendKeys(input: string | Uint8Array): Promise<void>;
27
+ resize(size: TerminalSize): Promise<void>;
28
+ interrupt(options?: { readonly timeoutMs?: number }): Promise<void>;
29
+ compact(options?: { readonly timeoutMs?: number }): Promise<void>;
30
+ listModels(options?: { readonly timeoutMs?: number }): Promise<readonly AgentModelOption[]>;
31
+ setModel(id: string, options?: { readonly timeoutMs?: number }): Promise<void>;
32
+ createLoop(request: ElwoodLoopRequest): Promise<ElwoodLoopSnapshot>;
33
+ listLoops(): Promise<readonly ElwoodLoopSnapshot[]>;
34
+ cancelLoop(loopId: string): Promise<void>;
35
+ // Claude-only re-authentication recovery; NOT part of CodexSession or the
36
+ // common ElwoodAgentSession surface (C-API-43).
37
+ login(options: ClaudeLoginOptions): Promise<void>;
38
+
39
+ stop(): Promise<void>;
40
+ kill(): Promise<void>;
41
+ teardown(): Promise<void>;
42
+ }
43
+
44
+ interface SendOptions {
45
+ // Images to attach to this submission. Attached content: the submission is the
46
+ // text plus this ordered list of images (images have no position within the
47
+ // text, but array order determines attachment/chip order), attached by the
48
+ // adapter's native path before the text is submitted (C-API-44).
49
+ readonly images?: readonly ImageInput[];
50
+ }
51
+
52
+ // An image to attach, given either as a filesystem path or as in-memory bytes
53
+ // with an explicit format (never both — the alternatives are mutually exclusive).
54
+ // Bytes are written to a short-lived temp file so the adapter attach paths (which
55
+ // are file- or clipboard-based) have something to read; the temp file is removed
56
+ // (best-effort) after the submission is attached.
57
+ type ImageInput =
58
+ | { readonly path: string }
59
+ | { readonly data: Uint8Array; readonly format: "png" | "jpeg" | "gif" | "webp" };
60
+
61
+ type ClaudeLoginMethod = "claudeai" | "console" | "third_party";
62
+
63
+ interface ClaudeLoginOptions {
64
+ // Which method to pick from the "Select login method" list if it renders;
65
+ // defaults to "claudeai". Skipped when the list never appears.
66
+ readonly method?: ClaudeLoginMethod;
67
+ // Called with the (validated https, approved-host) browser authorization URL
68
+ // once it is scraped off screen, so the caller can open/show it. Optional.
69
+ readonly onAuthUrl?: (url: string) => void;
70
+ // REQUIRED for the default account flow: called when the CLI reaches its
71
+ // "paste code here" prompt; the returned code is validated then submitted.
72
+ readonly provideCode: () => string | Promise<string>;
73
+ readonly timeoutMs?: number; // default 300000
74
+ }
75
+ ```
76
+
77
+ `login` is Claude-only and drives the interactive `/login` flow to recover a
78
+ session whose login lapsed (C-CLAUDE-18); it is NOT on `CodexSession` or the
79
+ common `ElwoodAgentSession`. It runs as an EXCLUSIVE control-queue transaction:
80
+ it serializes behind and ahead of all other queued operations and no message,
81
+ prompt, model-picker command, or second `login` may interleave with its picker
82
+ keys, authorization code, or Enters. It writes exactly one library-controlled
83
+ Enter after a submitted code, never sends raw keystrokes into a blocking dialog,
84
+ and validates both untrusted inputs — the scraped URL (rejected unless `https:`
85
+ on an approved Anthropic host with no embedded credentials) and the human code
86
+ (rejected as `login_failed` if empty, oversized, or control-bearing). Screen
87
+ stages are recognized only when they NEWLY appear after this `/login` submission,
88
+ so stale on-screen text cannot settle the wrong attempt or disclose the code. It
89
+ resolves only after the CLI reports success AND a fresh `ready` transition proves
90
+ the session is usable again; it rejects with `login_failed` on an explicit
91
+ failure/invalid code or a failing `provideCode` callback, `login_timeout` after
92
+ `timeoutMs`, and `session_not_running` if the session terminates first or is
93
+ already terminal. A flow that self-completes without a code prompt never calls
94
+ `provideCode`.
95
+
96
+ `login` defends against untrusted on-screen text with layered checks — exclusive
97
+ queue ownership, approved-host/`https:` URL validation, active-tail + baseline
98
+ scoping of stage markers, blocking-dialog write holds, and a re-check that the
99
+ paste prompt is still current immediately before the code is written. Two
100
+ residual limitations are accepted by design: a single frame that renders BOTH a
101
+ genuine approved-host OAuth URL and a paste-code prompt in the active region can
102
+ still advance the flow (coherent per-frame dialog identity on a text-only TUI is
103
+ not attempted — callers should invoke `login` only when the session is genuinely
104
+ at `/login`, not while untrusted output streams), and the version-coupled login
105
+ screen matchers are covered by unit tests against real captured CLI strings
106
+ rather than a live-CLI e2e (which would mutate real auth and cannot run in CI).
107
+
108
+ `sendPrompt` submits a user prompt through the terminal input path, as if a
109
+ human typed or pasted it into Claude and pressed Enter. It MUST support
110
+ multi-line prompts. The implementation should use bracketed paste or an
111
+ equivalent robust terminal-input strategy so newline characters are entered as
112
+ prompt content rather than premature submissions.
113
+
114
+ `sendMessage` is the adapter-neutral API for the common chat-loop operation:
115
+ send a user message when the wrapped agent can accept one. Parent apps that only
116
+ need to send a user message and wait for activity/status should prefer
117
+ `sendMessage`. If the session is ready, Elwood submits the message immediately
118
+ through the same PTY input path as `sendPrompt`. If the session is alive but not
119
+ ready, Elwood queues the message and submits it when the adapter reports semantic
120
+ readiness through adapter-specific lifecycle signals. Initial readiness MUST be
121
+ hook-backed whenever the CLI emits a pre-input readiness event: Claude's
122
+ `InstructionsLoaded` and Codex's `SessionStart` both fire only once the CLI is
123
+ initialized and accepting input, so Elwood submits the first queued message on
124
+ that hook. The rendered terminal is NOT a reliable pre-input signal on Codex:
125
+ the input composer marker (a line-leading `›`) is painted as a boot-time
126
+ placeholder roughly a second before the TUI accepts input, and it is
127
+ byte-for-byte identical to the ready composer, so a message submitted on the
128
+ composer marker alone races Codex's input loop and is silently swallowed
129
+ (observed: the queued persona never reaches `SessionStart`/`UserPromptSubmit`).
130
+ Elwood therefore never treats the rendered composer as a readiness signal on a
131
+ COLD START. On RESUME the contract differs: the CLI is reattaching to an
132
+ already-established conversation, so it does not re-fire its pre-input readiness
133
+ hook (`SessionStart` for Codex; observed), and its input loop is live the moment
134
+ the composer paints — a message submitted when the composer marker first appears
135
+ on resume is accepted, not swallowed (verified against the real CLI). So on a
136
+ resumed session Elwood treats the first rendered composer marker as the readiness
137
+ signal, submitting the first queued message on it rather than waiting out the
138
+ deadline; this brings a resumed session to readiness in roughly a second instead
139
+ of the full deadline. (A resumed adapter whose readiness hook DOES re-fire —
140
+ Claude's `InstructionsLoaded` re-fires on resume — still marks ready on that hook,
141
+ whichever comes first.) The bounded deadline remains the ultimate fallback for
142
+ BOTH start and resume: a continuously animating screen region (for example a
143
+ failing MCP server's retry spinner) or a session whose readiness hook never
144
+ arrives (a missing or failed hook bridge) must not starve readiness forever, so
145
+ initial readiness fires no later than a fixed deadline after the first rendered
146
+ frame even when neither a hook nor (on cold start) any composer signal arrives.
147
+ Subsequent turn readiness comes from
148
+ completion signals such as an unblocked `Stop` hook. Queued messages are
149
+ submitted in FIFO order, one message per ready transition, so back-to-back calls
150
+ do not accidentally paste multiple user turns into one active agent prompt. A
151
+ queued `sendMessage` promise resolves when that message has been written to the
152
+ PTY. If the session terminates before a queued message is written, the promise
153
+ rejects with `session_not_running`.
154
+
155
+ The ergonomic `stream`/`send` turn owner additionally requires positive turn
156
+ acceptance before an idle `ready` transition may complete a turn. A matching
157
+ `UserPromptSubmit`, turn content, or `Stop` hook is positive acceptance. This
158
+ closes the cold-start deadline race: under load, Codex may repaint a boot spinner
159
+ and composer after the deadline-released paste, swallow that paste, and otherwise
160
+ look exactly like a completed empty turn. When `ready` returns without positive
161
+ acceptance, Elwood waits the normal no-oracle quiet window and replays the same
162
+ submission at the now-idle composer, at most twice. A still-unaccepted turn fails
163
+ with `wait_timeout`; it never reports a false empty success. A positive acceptance
164
+ arriving during the quiet window cancels recovery and lets the observed `ready`
165
+ settle normally.
166
+
167
+ `sendGuidance` is the adapter-neutral intervention API. Before the session has
168
+ ever reached semantic readiness, and while a blocking dialog is visible, it has
169
+ the same readiness-safe behavior as `sendMessage`. During a real running turn
170
+ after initial readiness, it bypasses turn readiness and enters the live TUI
171
+ immediately, like `sendPrompt`. Guidance may overtake readiness-waiting messages,
172
+ but queue-backed prompt, message, and command submissions remain serialized so
173
+ their bracketed pastes and delayed Enter keystrokes never interleave. Its promise
174
+ resolves only after the first submitting Enter is dispatched.
175
+
176
+ Prompt submission MUST NOT race the TUI's paste ingestion. The CLIs ingest a
177
+ bracketed paste asynchronously, and an Enter concatenated into the same PTY
178
+ write can be consumed by the paste boundary — leaving the prompt staged in
179
+ the composer, never submitted (long multi-line prompts such as personas hit
180
+ this reliably). Elwood therefore writes the bracketed paste and the
181
+ submitting Enter as separate PTY writes, with the Enter following after a
182
+ short settle delay, and then confirms submission against the rendered
183
+ screen: while the composer still shows staged content (Claude's
184
+ "[Pasted text" chip; for Codex the paste's final line still in the
185
+ composer), Elwood re-sends Enter a bounded number of times. A surplus Enter
186
+ on an empty composer is a no-op on both CLIs, so the recovery cannot
187
+ double-submit.
188
+
189
+ Message text is DATA, never terminal control. `sendPrompt`, `sendMessage`, and
190
+ `sendGuidance` submit caller- or model-controlled text as the content of a
191
+ bracketed paste, so that text MUST NOT be able to escape paste mode or inject
192
+ live keystrokes. Before framing the paste, Elwood sanitizes the text: it removes
193
+ the bracketed-paste sentinels (`ESC [ 200~` and `ESC [ 201~`) so an embedded end
194
+ sentinel cannot terminate the paste early and turn following bytes — an Enter, an
195
+ Escape, a permission-dialog confirmation — into live input, and it strips other
196
+ C0/C1 control characters except the horizontal tab, newline, and carriage return
197
+ that are legitimate multi-line text. This sanitization applies only to the
198
+ queued text-submission APIs; `sendKeys` is the explicit raw-input escape hatch
199
+ and is never sanitized. Newlines inside a sanitized paste remain paste content
200
+ (the whole point of bracketed paste) rather than premature submissions.
201
+
202
+ `sendPrompt`, `sendMessage`, and `sendGuidance` accept an optional `images`
203
+ list (file paths or in-memory bytes with an explicit format). A submission is
204
+ then the text plus that list of attached images: images have no position WITHIN
205
+ the text (an attached-content model, not interleaving), but the list is ORDERED
206
+ — array order determines attachment order and the CLI's `[Image #N]` chip
207
+ numbering. Elwood attaches every image through the SAME native ingestion path a
208
+ human would use in that CLI, as part of the SAME serialized queue operation that
209
+ submits the text, BEFORE the text's submitting Enter, so a single call produces
210
+ a single agent turn carrying both. An image given as bytes is written to a
211
+ short-lived temp file (the adapter attach paths are file- or clipboard-based);
212
+ Elwood attempts to remove every such temp file after the submission is attached,
213
+ on success or failure (best-effort — see the residual limitations below).
214
+
215
+ Inputs are validated and bounded BEFORE any temp file is written or any input
216
+ reaches the composer, and a validation failure rejects the whole call so nothing
217
+ is partially attached. The limits are: at most 16 images per submission; each
218
+ byte input at most 25 MiB and each path input's file at most 25 MiB; at most 50
219
+ MiB total across a submission. A `format` that is not one of `png`/`jpeg`/`gif`/
220
+ `webp`, empty bytes, a `path` that is not a readable regular file (its readability
221
+ is checked, not merely its existence), or any count/size past those limits
222
+ reject with `invalid_image`. Malformed inputs from JavaScript callers (a non-object
223
+ entry, or one lacking a valid `path`/`data`) also reject with `invalid_image`
224
+ rather than a raw type error.
225
+
226
+ Image attachment is CONFIRMED, never assumed. For each image Elwood waits for the
227
+ CLI's `[Image #N]` chip to appear in the composer region; if the chip is not
228
+ observed within the per-image confirmation timeout (10 s), or the OS clipboard
229
+ cannot be read or written, the call rejects with `image_attach_failed` and Elwood
230
+ submits NO text and attaches no further images. A confirmation timeout therefore
231
+ surfaces (e.g. on TUI-wording drift) rather than silently degrading to a text-only
232
+ turn. A session that TERMINATES mid-attach rejects the call with
233
+ `session_not_running`, like any other queued operation. Chip confirmation is
234
+ scoped to the composer region so unrelated on-screen text containing a literal
235
+ `[Image #N]` cannot spoof it.
236
+
237
+ The attach path is adapter-specific because the two CLIs ingest images
238
+ differently, and this is a version-coupled TUI behavior verified against real
239
+ CLIs (Claude 2.1.215, codex-cli 0.144.6):
240
+
241
+ - Claude reads a pasted ABSOLUTE image path itself: Elwood bracketed-pastes each
242
+ image's absolute path into the composer (the same delivery a terminal produces
243
+ on drag-and-drop), Claude reads and encodes the file, and the composer shows an
244
+ `[Image #N]` chip. This is pure PTY text and works on every platform.
245
+ - Codex ingests an interactive image ONLY from the OS clipboard via Ctrl+V (it
246
+ reads `public.tiff` off the macOS `NSPasteboard`); a pasted path is treated as
247
+ pasted TEXT, not an image. Elwood snapshots the user's clipboard ONCE before
248
+ the attachment, then for each image in order writes it onto the clipboard as a
249
+ native image, sends Ctrl+V, and waits for the `[Image #N]` chip; after all
250
+ images it restores the single snapshotted clipboard text. The whole
251
+ snapshot/set/paste/confirm/restore sequence holds a process-wide clipboard lock
252
+ so two concurrent Codex sessions cannot cross-attach each other's images. If
253
+ the initial clipboard snapshot itself fails, the attach rejects with
254
+ `image_attach_failed` before mutating the clipboard. Because this drives the
255
+ macOS clipboard, Codex image attachment is macOS-only; a `sendMessage`/
256
+ `sendPrompt`/`sendGuidance` with `images` on a non-macOS Codex session rejects
257
+ with `unsupported_platform` and submits nothing. Restoring the clipboard is
258
+ best-effort and covers text contents; a brief window where the injected image
259
+ is the clipboard contents is accepted by design. When the restore itself fails,
260
+ the attach result is unchanged (still success) but Elwood surfaces a content-free
261
+ `clipboard_restore_failed` warning (no clipboard contents) so the possible loss
262
+ of the user's prior clipboard is diagnosable rather than silent.
263
+
264
+ Image attachment carries several accepted residual limitations. First, temp-file
265
+ removal after an attach is best-effort: the primary attach/materialize error is
266
+ always preserved (a cleanup failure never masks it), but a failed removal is not
267
+ retried and leaves the file in the OS temp directory for the OS to reap rather
268
+ than surfacing a distinct error. Second, the
269
+ Codex clipboard lock serializes attachments within a SINGLE Elwood process only;
270
+ two Elwood processes sharing one OS clipboard are not coordinated, so a
271
+ cross-process cross-attach remains possible (callers running concurrent
272
+ Elwood processes that attach Codex images should serialize that themselves).
273
+ Third, session close during an in-flight attach rejects the operation and blocks
274
+ the text write, but does not join the attach's temp-cleanup and clipboard-restore
275
+ finalizers; on an abrupt teardown those may not complete before process exit
276
+ (clipboard restore is already best-effort, and temp files fall to OS reaping).
277
+ Fourth, composer isolation on a failed attach is best-effort: when an attach
278
+ fails after staging began, Elwood clears the composer draft (kill-to-line-start
279
+ plus kill-to-line-end) so the rejected images do not leak into a later caller's
280
+ turn, but it does not run an adapter-specific transactional confirm-and-restore
281
+ of the exact prior composer baseline; a partially-staged draft that those keys do
282
+ not fully clear could still carry over (mitigated because a caller does not
283
+ interleave submissions on one session). Fifth, chip confirmation identifies the
284
+ composer by the adapter's prompt-glyph line (`›`/`❯`) rather than adapter cursor
285
+ metadata: this defeats stray transcript/assistant `[Image #N]` text on
286
+ non-composer lines, but a running-turn render that puts a prompt-glyph line
287
+ on-screen at the confirmation moment, or a chip on a wrapped composer
288
+ continuation row, is a known edge the glyph heuristic does not perfectly resolve.
289
+
290
+ Turn boundaries MUST be observable even when no completion hook fires. Claude
291
+ does not fire a `Stop` hook when a running turn is interrupted (Escape), so
292
+ hook-driven readiness alone leaves the session in `running` forever after an
293
+ interrupt. Elwood therefore also derives turn state from the rendered TUI:
294
+ each adapter has a documented working indicator (a footer token rendered only
295
+ while a turn is running) and an idle indicator (the input composer). When the
296
+ working indicator appears after initial readiness, the session transitions to
297
+ `running`; when it disappears and the composer is visible again, the session
298
+ transitions to `ready` — emitting the standard `status` lifecycle activities
299
+ on every real turn boundary regardless of how the turn ended (completion,
300
+ interrupt, or otherwise). A screen that shows neither indicator (for example
301
+ a modal permission dialog) holds the current state so queued messages are
302
+ never submitted into a dialog. Detection MUST be robust to terminal width:
303
+ Claude elides the footer working token below roughly 66 columns, so each
304
+ adapter also has a documented interrupt end banner (Claude renders
305
+ "⎿ Interrupted", Codex renders "Conversation interrupted") whose appearance
306
+ fires the turn-end transition even when the working token was never visible.
307
+ Together the sources cover every width. Turn start is a rendered-state signal:
308
+ the on-screen working token where it fits, plus the OSC window-title braille
309
+ spinner (C-TURN-05) which is width-independent and reveals a running turn even
310
+ when the footer token is elided; a caller `sendMessage`/`sendPrompt` also moves
311
+ the session to `running`. Normal completion comes from the `Stop` hook, and
312
+ interrupts from the working-token edge on wide screens or the end banner
313
+ anywhere. Hook-driven readiness (`Stop`) remains a redundant turn-end source;
314
+ transitions are idempotent, and rendered-state watching only activates after
315
+ initial readiness so startup spinners that borrow the same wording cannot
316
+ fabricate turns. On a RESUME the same guard extends to the transcript replay: a
317
+ resumed session that reached readiness on its first composer frame then repaints
318
+ prior turns, whose footer lines carry the working token — so rendered turn edges
319
+ are suppressed until the composer has stayed quiet and non-blocking (composer
320
+ visible, no working token, no blocking dialog) for a SUSTAINED run of consecutive
321
+ frames, not merely one. The replay repaints those footers in bursts separated by
322
+ brief quiet gaps, so a single quiet frame does not prove the replay finished; any
323
+ working or blocking frame during suppression resets the run. Once the quiet run is
324
+ reached, detection behaves exactly as it does post-readiness on a cold start.
325
+ Explicit running evidence (a caller submission,
326
+ a hook) releases that suppression IMMEDIATELY — a real turn has begun, so the replay
327
+ is over by definition — and synchronizes the watcher into the running state without
328
+ re-announcing a start; the CURRENT frame is still evaluated, so if the turn ended
329
+ exactly as the evidence arrived (the first post-release frame is already the quiet
330
+ composer or the interrupt banner) that frame's end edge is retained, not discarded.
331
+ This ensures a resume never fabricates a phantom rendered turn from replayed history
332
+ and never swallows a real turn's only end edge. These
333
+ indicators (working token, title spinner, composer markers, interrupt banners, and
334
+ blocking-dialog shapes) are version-coupled TUI wording and MUST live in the
335
+ documented per-adapter screen-fact tables, each recording the CLI version it was
336
+ verified against.
337
+
338
+ Elwood MUST NOT require callers or examples to inspect the terminal screen or
339
+ match adapter-specific prompt text before calling `sendMessage`. Adapter-specific
340
+ terminal behavior belongs inside Elwood. Because terminal prompts are a rendered
341
+ UI rather than a stable machine protocol, Elwood should prefer lifecycle hooks
342
+ and startup health checks over fragile prompt-text regular expressions.
343
+
344
+ `sendPrompt` does not gate on readiness. If a caller sends a prompt while Claude
345
+ is busy, Elwood does NOT wait for `ready`: the prompt bypasses readiness and
346
+ dispatches as soon as no other submission is in flight, matching human terminal
347
+ behavior. Like guidance, it may overtake readiness-waiting messages, but it never
348
+ interleaves with an in-flight submission — any paste/Enter sequence already
349
+ underway completes first (C-API-07, queue ordering above).
350
+ This readiness guarantee applies while the agent process is alive, including
351
+ `starting`, `running`, and `ready` states. It does not require Elwood to write to
352
+ terminated sessions; calls made after `stopped`, `exited`, `killed`, or
353
+ `torn_down` should fail with `session_not_running`. Promise-returning session
354
+ methods MUST report that failure by rejecting the returned promise; they never
355
+ throw synchronously, so `promise.catch` alone is a complete error-handling
356
+ strategy for callers.
357
+
358
+ `sendKeys` is the low-level escape hatch for raw terminal input. String input
359
+ flows through the headless xterm input path so terminal semantics match visual
360
+ input. `Uint8Array` input is written to the PTY as bytes instead of being decoded
361
+ as UTF-8, because callers use this overload for raw escape/binary input. It
362
+ bypasses the control queue intentionally so interrupts remain immediate, and
363
+ may therefore interleave with a prompt's paste and delayed Enter.
364
+
365
+ `interrupt` cancels the in-flight turn: the programmatic equivalent of a human
366
+ pressing Escape in the adapter's TUI. An interrupt exists to stop work already
367
+ in flight, so like `sendKeys` it bypasses the control queue and writes Escape
368
+ to the terminal immediately; it never waits behind queued submissions. Once the
369
+ session has reached initial readiness at least once and is `running` or
370
+ `blocked`, Elwood sends a single Escape and the returned promise resolves when
371
+ the session next reaches `ready`, driven by the same turn-end and
372
+ dialog-dismissal signals as a human interrupt (turn-state watching and blocked
373
+ detection above). When no turn is in flight, `interrupt` resolves immediately
374
+ and MUST NOT write anything: Escape at an idle composer is not neutral (Claude
375
+ clears staged composer text; Codex arms its edit-previous-message affordance),
376
+ so an idle interrupt is a safe no-op. "No turn in flight" covers both an idle
377
+ `ready` session AND the pre-readiness startup window, where the session is
378
+ `running` only because startup is usable, not because a user turn has begun —
379
+ Escape into a booting TUI MUST NOT fire. Concurrent `interrupt` calls coalesce:
380
+ while one interrupt is in flight, a second joins it rather than writing a second
381
+ Escape, which could otherwise land on the composer the first interrupt just made
382
+ idle. If the session does not reach `ready` within `timeoutMs` (default 10000
383
+ ms), the promise rejects with `interrupt_failed`; if the session terminates
384
+ first, it rejects with `session_not_running` — as does an `interrupt` on an
385
+ already-terminated session.
386
+
387
+ `listModels` and `setModel` drive the adapter's own `/model` picker UI through
388
+ the headless terminal, because neither CLI exposes a stable machine protocol
389
+ for session model selection. The row model is:
390
+
391
+ ```ts
392
+ type AgentModelOption = {
393
+ readonly id: string;
394
+ readonly label: string;
395
+ readonly description?: string;
396
+ readonly isCurrent: boolean;
397
+ readonly isDefault: boolean;
398
+ readonly raw: string;
399
+ };
400
+ ```
401
+
402
+ `id` is a stable lowercase selector (the Claude option label, such as `haiku`,
403
+ or the Codex model slug, such as `gpt-5.4`). `isCurrent` reflects the model the
404
+ session is using now; `isDefault` reflects the CLI's configured default. When
405
+ Codex renders only a `(current)` marker, that row is also the default.
406
+
407
+ `listModels` opens the picker, parses the rendered rows, cancels the picker
408
+ with Escape, and MUST leave the session's model unchanged. `setModel` selects
409
+ the requested row through cursor navigation. Elwood itself MUST NOT persist a
410
+ new default model into the user's configuration (§4.5): for Claude it applies
411
+ with the session-only key rather than Enter, and it never selects rows via
412
+ number keys because Claude's number shortcut immediately saves the choice as
413
+ the user's default. Claude may next show its built-in `Switch model?` or
414
+ `Change effort level?` cache warning before applying the session-only change.
415
+ When the dialog carries Claude's standard cache-warning copy, Elwood MUST find
416
+ the rendered affirmative row across numbered and unnumbered layouts, navigate
417
+ to it from the rendered cursor position, confirm it, and resolve `setModel`
418
+ only after the dialog has cleared and the idle composer has returned. Recognition
419
+ MUST use one bottom-most, contiguous dialog region and revalidate that live
420
+ region before confirmation; transcript text elsewhere in the viewport cannot
421
+ supply cache-warning copy, action rows, or a composer signal. A confirmation
422
+ requested by a `PreModelSwitch` hook is NOT the built-in cache warning: Elwood
423
+ MUST leave that decision to the human and keep it classified as a blocking
424
+ prompt. For Codex, the Codex CLI itself persists the confirmed
425
+ picker selection (model and reasoning effort) into the user's `config.toml` —
426
+ there is no session-only affordance — so Elwood compensates: it snapshots
427
+ `config.toml` before opening the picker and, after the switch is confirmed,
428
+ restores the prior contents if and only if the file differs solely in the
429
+ root-level `model` and `model_reasoning_effort` keys the picker writes
430
+ (compare-and-swap on the untouched remainder). The restore never affects the
431
+ live session, which keeps the switched model because Codex reads its config
432
+ at launch. When the compare fails — for example the user edited `config.toml`
433
+ concurrently — Elwood MUST NOT clobber the file; it leaves the CLI's write in
434
+ place and emits the `codex_default_model_persisted` warning so the deviation
435
+ is surfaced rather than silent. Elwood confirms the CLI's own default
436
+ reasoning level for the chosen Codex model. The whole snapshot/apply/restore
437
+ transaction holds a process-wide lock so two concurrent `setModel` switches in
438
+ the SAME Elwood process cannot interleave and leave the wrong default. This lock
439
+ is process-local: two SEPARATE Elwood processes sharing one `config.toml` can
440
+ still interleave their transactions. That cross-process race is an accepted
441
+ current limitation (the compare-and-swap still refuses to clobber an unrelated
442
+ edit; the residual risk is a transiently-persisted picker model surviving as the
443
+ default). Closing it requires an inter-process lock on the resolved config path
444
+ and is scoped as separate work.
445
+ `listModels` and `setModel` are transient TUI control: they open and drive the
446
+ adapter's own picker overlay and MUST dispatch even while a turn is in flight
447
+ (for example an MCP-server boot spinner at startup), rather than waiting for
448
+ the session to reach `ready`. Waiting would stall picker automation behind a
449
+ turn that has not completed. Failures to recognize or drive the rendered
450
+ picker reject with `model_automation_failed`; an unknown `id` rejects with the
451
+ same error and includes the available ids in the error details.
452
+
453
+ Elwood also exposes the model list WITHOUT an active session, for callers that
454
+ only want to enumerate available models (for example to populate a UI selector):
455
+
456
+ ```ts
457
+ type ListModelsOptions = {
458
+ readonly cwd: string;
459
+ readonly stateDir?: string;
460
+ readonly autoupdate?: boolean;
461
+ readonly hookTimeoutMs?: number;
462
+ readonly strictVersionCheck?: boolean;
463
+ readonly timeoutMs?: number;
464
+ };
465
+
466
+ declare function listClaudeModels(
467
+ options: ListModelsOptions,
468
+ ): Promise<readonly AgentModelOption[]>;
469
+ declare function listCodexModels(
470
+ options: ListModelsOptions,
471
+ ): Promise<readonly AgentModelOption[]>;
472
+ ```
473
+
474
+ Each starts a throwaway session and, as a documented and deliberate exception to
475
+ the opt-in workspace-trust default (§5.1), implies `autotrust: true` so a
476
+ workspace-trust prompt cannot silently stall the probe at `blocked`. This
477
+ exception is scoped: the probe never runs a user turn — it only opens and cancels
478
+ the `/model` picker, then tears the session down — so a trusted workspace's
479
+ integrations are loaded but never exercised. A caller that must NOT auto-trust an
480
+ untrusted workspace should probe from a directory it already trusts. Elwood then
481
+ waits for the session to reach first readiness, calls `listModels`, and ALWAYS
482
+ tears the session down — including when readiness or the picker fails, AND when
483
+ adapter startup itself fails after allocating a state directory — so no probe
484
+ session, PTY process tree, or state directory is ever leaked. The returned
485
+ options are exactly the `listModels` rows, so
486
+ `isCurrent` reflects the throwaway session's launch model and `isDefault`
487
+ reflects the user's configured default. Because the probe opens and cancels the
488
+ picker only (it never selects a row), it leaves the user's saved model default
489
+ untouched, exactly as `listModels` does on a live session (Codex's own boot-time
490
+ config bookkeeping is outside Elwood's control and is not a model change). A
491
+ start failure surfaces the adapter's normal typed start error
492
+ (`claude_not_found`, `codex_start_failed`, and so on); a picker failure rejects
493
+ with `model_automation_failed`. The `timeoutMs` bounds the picker automation as
494
+ it does on a live session; readiness uses the normal startup deadline.
495
+
496
+ Command submissions (`compact`, `listModels`, `setModel`) MUST NOT consume the
497
+ session's ready transition: typing a slash command does not start a user turn,
498
+ no completion hook will re-arm readiness afterwards, and consuming readiness
499
+ would deadlock every later queued submission.
500
+
501
+ Queue ordering is FIFO by default, with two documented overtaking exceptions.
502
+ An operation is *readiness-waiting* (a `sendMessage`, a persona message, or a
503
+ `compact`) when it may only reach the terminal once the session is ready; the
504
+ rest are *immediate* (a `sendPrompt`; a `listModels`/`setModel` picker command;
505
+ and a `sendGuidance` that was submitted during an already-ready running turn —
506
+ see §5.3 guidance). When the head of the queue is readiness-waiting and the
507
+ session is not ready, it holds every OTHER readiness-waiting operation behind it
508
+ in FIFO order, but an immediate operation queued behind it overtakes the head
509
+ and reaches the terminal first. Among operations of the same class, and once the
510
+ session is ready, order is strict FIFO. Overtaking never interleaves two
511
+ submissions: the bracketed paste and delayed Enter of one operation always
512
+ complete before the next operation writes. Guidance's eligibility to overtake is
513
+ fixed when it is submitted (§5.3), so guidance queued before first readiness or
514
+ while blocked stays readiness-waiting for its whole lifetime and never reclassifies
515
+ into a later turn.
516
+
517
+ `compact` asks the wrapped agent to compact its conversation. The compact
518
+ command submission goes through the same readiness queue as `sendMessage`, so
519
+ a compact requested mid-turn runs after the active turn completes. Elwood
520
+ types the adapter's `/compact` command through terminal input and resolves the
521
+ returned promise when the adapter reports completion through its `PostCompact`
522
+ hook. If no `PostCompact` hook arrives within `timeoutMs` (default 120000 ms),
523
+ the promise rejects with `compact_failed`. If the session terminates first,
524
+ the promise rejects with `session_not_running`. Hook and activity events for
525
+ `PreCompact`/`PostCompact` flow to the parent app exactly as for any other
526
+ hook.
527
+
528
+ `ElwoodSessionStatus` values are `starting`, `running`, `ready`, `blocked`,
529
+ `stopped`, `exited`, `killed`, and `torn_down`.
530
+
531
+ After startup health checks pass, a newly started or resumed session may enter
532
+ `ready` only when that adapter's first-input readiness signal has fired.
533
+ Otherwise it remains `running` until an adapter lifecycle hook or terminal
534
+ lifecycle signal proves the interactive prompt is ready. Sending a message
535
+ through `sendMessage` or a prompt through `sendPrompt` keeps or moves the session
536
+ in `running`. Adapter readiness signals move the session back to `ready`.
537
+
538
+ `blocked` reports that the wrapped agent is waiting on a human decision it
539
+ cannot answer itself: a tool permission or approval dialog, or a workspace
540
+ trust prompt Elwood was not authorized to answer (`autotrust: false`).
541
+ Blocked detection derives from the rendered screen via the adapter's
542
+ documented screen-fact rules. A session enters `blocked` from any live status
543
+ (`running` or `ready`) when a blocking dialog appears — an adapter may finish
544
+ its working indicator before the dialog settles, so the block can follow a
545
+ `ready` beat — and settles to `ready` when the dialog resolves, since the
546
+ composer is then waiting for input again. Prompts Elwood answers automatically
547
+ (startup prompt automation, or workspace trust with `autotrust: true`) MUST
548
+ NOT produce a `blocked` transition. Terminal statuses absorb blocking
549
+ evidence. On the transition into `blocked`, Elwood also emits an `activity`
550
+ event with `source: "terminal"`, `kind: "attention"`, a label naming the
551
+ matched screen-fact rule ids, and text describing that the agent is waiting on
552
+ a human decision.
553
+
554
+ `statusDecisions()` returns a live-only log of at most the 50 most recent
555
+ status decisions, oldest first. Each `ElwoodStatusDecision` records the evidence that
556
+ arrived, the status it moved `from`, the status it moved `to` (undefined when
557
+ the evidence was ignored rather than applied), and a human-readable `reason`.
558
+ It explains why lifecycle transitions did or did not fire — including which
559
+ evidence was ignored — for diagnostics and the local dev app. The log is never
560
+ persisted and never contains prompt, terminal, or hook payload content.
561
+
562
+ `waitForStatus(match, timeoutMs?)` and `waitForActivity(match, timeoutMs?)` are
563
+ typed helpers over the session's own `status` and `activity` event streams.
564
+ `waitForStatus` resolves with the current status if `match` already accepts it,
565
+ otherwise on the first matching `status` event. `waitForActivity` resolves on
566
+ the first `activity` event `match` accepts. Both reject with `wait_timeout`
567
+ after `timeoutMs` (default 60000 ms), and reject with `session_not_running` if
568
+ the session reaches a terminal status the caller was not waiting for before the
569
+ condition is met (for `waitForActivity`, this includes a session that is
570
+ already terminal when the call is made). The helpers unsubscribe on settle and
571
+ add no persistence; they are convenience wrappers over events parent apps could
572
+ observe directly.
573
+
574
+ If `initialSize` is omitted, Elwood MUST default the terminal to 189 columns by
575
+ 48 rows. Parent apps with a visible terminal should still pass and maintain the
576
+ visual xterm size explicitly.
577
+
578
+ Claude sessions requested below 100 columns MUST bootstrap the PTY and headless
579
+ terminal at 100 columns because Claude Code can silently discard input when it
580
+ initializes below that width. Elwood holds only the PHYSICAL pty/terminal resize
581
+ during this bootstrap; a caller `resize` in this window is recorded in memory as
582
+ the latest requested size (terminal size is never persisted, §8.2). Elwood
583
+ restores that latest requested size at the one-shot initial-ready transition,
584
+ before the queued persona or caller message is submitted. Later resizes retain
585
+ their ordinary exact behavior. Codex sessions do not apply this bootstrap.
586
+
587
+ If restoring the latest requested size at the initial-ready transition fails with
588
+ a real (non-closed) native PTY resize error, Elwood MUST NOT report that resize as
589
+ applied: the session stays at its safe bootstrap width, the failure is surfaced as
590
+ a typed `resize_restore_failed` warning, and the queued persona or caller message
591
+ is still released so a failed restore never starves input. A closed-fd resize at
592
+ this transition is the ordinary benign race and is a no-op, not a warning.
593
+
594
+ `terminal` exposes the session's headless xterm.js terminal handle. The handle
595
+ MUST expose the underlying headless xterm instance, current size, the latest
596
+ OSC window title, and a snapshot API with visible lines, joined visible text,
597
+ and cursor position. Parent apps may
598
+ use this handle for diagnostics or to bridge Elwood's terminal state into their
599
+ own renderer, while `terminal:data` remains the live raw stream for browser
600
+ renderers. The handle also exposes terminal-control methods used internally
601
+ (`sendInput`, `writeOutput`, `resize`, `settled`, and `dispose`); callers should
602
+ prefer the session-level `sendPrompt`, `sendMessage`, `sendKeys`, `resize`,
603
+ `stop`, `kill`, and `teardown` APIs unless they are building advanced terminal
604
+ integrations. Consumers that need diagnostics may await `terminal.settled()` and
605
+ read `terminal.snapshot()`, but visual terminal renderers SHOULD render the raw
606
+ `terminal:data` byte stream instead of repainting from snapshots.
607
+ `sendInput` resolves after input has been forwarded to the PTY and rejects when
608
+ that forwarding fails.
609
+ Because parent apps often attach listeners after `startClaude` or `startCodex`
610
+ resolves, sessions MUST maintain a bounded live-only replay buffer of raw PTY
611
+ output emitted during the current process lifetime (128 KB by default). New
612
+ `terminal:data` subscribers receive that buffer immediately, then receive future
613
+ live chunks. When the bound trims the oldest retained output, it trims on a UTF-8
614
+ code-point boundary so the replayed data never begins with a split multibyte
615
+ sequence (U+FFFD).
616
+ This replay buffer MUST NOT be written to Elwood session state. Parent apps that
617
+ embed their own visual xterm.js instance should feed it the replayed and live
618
+ `terminal:data` chunks; Elwood does not require sharing the same JavaScript
619
+ terminal object between Node and the browser.
620
+
621
+ `stop` attempts graceful process termination while preserving Elwood metadata and
622
+ loop definitions for resume. It should wait for process exit for a bounded grace period, then escalate
623
+ to force termination so callers do not get a terminal status while the process is
624
+ still indefinitely alive. `kill` force-terminates the process and permanently
625
+ clears its loop definitions. `teardown` removes
626
+ Elwood-owned state for the session and must not remove Claude-owned transcripts,
627
+ auth, or project/user settings that Elwood did not create.
628
+
629
+ `stop`, `kill`, and `teardown` MUST reap the full PTY process tree, not only the
630
+ PTY leader. The adapter CLI spawns helper subprocesses that are descendants of
631
+ the PTY leader — notably the per-session hook bridge (`node hook-bridge.mjs`) the
632
+ CLI runs on each hook — and signaling only the leader can orphan them, leaking a
633
+ process tree that accumulates across launch/quit cycles. Elwood MUST reap these
634
+ by SIGKILLing the leader's process group: node-pty starts the leader as its own
635
+ session leader, so every descendant inherits its process group, and a group
636
+ SIGKILL reaches them even after a descendant has been reparented to PID 1 by the
637
+ leader's exit — which a parent-pid walk cannot. The reap MUST run for `stop()`,
638
+ `kill()`, and `teardown()`, including when the leader has already exited on its
639
+ own (an already-terminal session still reaps the group). A dead or empty group
640
+ is a no-op. A `teardown()` that rejects in-flight control-queue operations MUST
641
+ still complete: rejecting a pending `sendMessage`/`compact`/model operation does
642
+ not skip the group reap or the rest of teardown.
643
+
644
+ ### Automated input and pending decisions
645
+
646
+ All queued text, including slash-command text and its delayed Enter, MUST wait
647
+ while a human-decision dialog is visible. Permission menus remain blocking
648
+ regardless of the highlighted option or wrapped question. Model commands may
649
+ bypass startup readiness but never this dialog guard. Their timeout includes a
650
+ held submission and cancels it, so no expired command or retry executes later.
651
+ Input blocking follows live rendered facts even before startup usability or
652
+ semantic readiness. Timeout cleanup MUST NOT write control keys into a blocking
653
+ dialog; text already staged may remain for the caller to inspect. Picker navigation
654
+ and apply keystrokes MUST revalidate their expected current screen immediately
655
+ before writing; only the existing first-party cache-confirmation policy may own
656
+ its specific blocking screen. Compaction recovery Enter must also respect the dialog guard. Explicit caller
657
+ `sendKeys` and narrowly scoped login/picker confirmation automation retain their
658
+ existing contracts. C-API-23/24/37 and C-ATTN-01.