@with-logic/elwood 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -3
- package/CONTRIBUTING.md +104 -0
- package/README.md +23 -17
- package/SECURITY.md +42 -0
- package/dist/claude/model-picker.d.ts.map +1 -1
- package/dist/claude/model-picker.js +4 -3
- package/dist/claude/model-picker.js.map +1 -1
- package/dist/claude/model-switch-confirmation.js +9 -1
- package/dist/claude/model-switch-confirmation.js.map +1 -1
- package/dist/claude/screen-table.js +1 -1
- package/dist/claude/screen-table.js.map +1 -1
- package/dist/claude/session/build.d.ts.map +1 -1
- package/dist/claude/session/build.js +3 -0
- package/dist/claude/session/build.js.map +1 -1
- package/dist/claude/session/start-or-resume.js +1 -0
- package/dist/claude/session/start-or-resume.js.map +1 -1
- package/dist/cli/interactive/spawn.d.ts.map +1 -1
- package/dist/cli/interactive/spawn.js +30 -9
- package/dist/cli/interactive/spawn.js.map +1 -1
- package/dist/cli/models/index.d.ts.map +1 -1
- package/dist/cli/models/index.js +6 -3
- package/dist/cli/models/index.js.map +1 -1
- package/dist/cli/request/layers.d.ts +2 -0
- package/dist/cli/request/layers.d.ts.map +1 -1
- package/dist/cli/request/layers.js +9 -5
- package/dist/cli/request/layers.js.map +1 -1
- package/dist/cli/sessions/index.d.ts +9 -2
- package/dist/cli/sessions/index.d.ts.map +1 -1
- package/dist/cli/sessions/index.js +2 -6
- package/dist/cli/sessions/index.js.map +1 -1
- package/dist/cli/sessions/settings.d.ts +11 -0
- package/dist/cli/sessions/settings.d.ts.map +1 -0
- package/dist/cli/sessions/settings.js +26 -0
- package/dist/cli/sessions/settings.js.map +1 -0
- package/dist/codex/model-picker.d.ts.map +1 -1
- package/dist/codex/model-picker.js +3 -2
- package/dist/codex/model-picker.js.map +1 -1
- package/dist/codex/session/build.d.ts.map +1 -1
- package/dist/codex/session/build.js +5 -7
- package/dist/codex/session/build.js.map +1 -1
- package/dist/codex/session/start-or-resume.js +1 -0
- package/dist/codex/session/start-or-resume.js.map +1 -1
- package/dist/core/input/index.d.ts.map +1 -1
- package/dist/core/input/index.js +15 -3
- package/dist/core/input/index.js.map +1 -1
- package/dist/core/models/input.d.ts +3 -0
- package/dist/core/models/input.d.ts.map +1 -0
- package/dist/core/models/input.js +9 -0
- package/dist/core/models/input.js.map +1 -0
- package/dist/core/models/picker.d.ts +2 -1
- package/dist/core/models/picker.d.ts.map +1 -1
- package/dist/core/models/picker.js +6 -5
- package/dist/core/models/picker.js.map +1 -1
- package/dist/core/models/tui-screen.d.ts +1 -1
- package/dist/core/models/tui-screen.d.ts.map +1 -1
- package/dist/core/models/tui-screen.js +26 -3
- package/dist/core/models/tui-screen.js.map +1 -1
- package/dist/runtime/session/base.d.ts.map +1 -1
- package/dist/runtime/session/base.js +4 -1
- package/dist/runtime/session/base.js.map +1 -1
- package/dist/runtime/session/commands.d.ts +2 -1
- package/dist/runtime/session/commands.d.ts.map +1 -1
- package/dist/runtime/session/commands.js +10 -4
- package/dist/runtime/session/commands.js.map +1 -1
- package/dist/runtime/session/lifecycle.d.ts +3 -4
- package/dist/runtime/session/lifecycle.d.ts.map +1 -1
- package/dist/runtime/session/lifecycle.js +6 -5
- package/dist/runtime/session/lifecycle.js.map +1 -1
- package/dist/state/directories.d.ts +4 -0
- package/dist/state/directories.d.ts.map +1 -0
- package/dist/state/directories.js +41 -0
- package/dist/state/directories.js.map +1 -0
- package/dist/state/files.d.ts.map +1 -1
- package/dist/state/files.js +25 -11
- package/dist/state/files.js.map +1 -1
- package/dist/state/private-session.d.ts.map +1 -1
- package/dist/state/private-session.js +2 -0
- package/dist/state/private-session.js.map +1 -1
- package/dist/state/store.d.ts.map +1 -1
- package/dist/state/store.js +7 -1
- package/dist/state/store.js.map +1 -1
- package/docs/cli-behavior.md +1 -1
- package/docs/codex/cli.md +57 -0
- package/docs/codex/config.md +47 -0
- package/docs/codex/hooks.md +70 -0
- package/examples/README.md +56 -0
- package/examples/full.ts +113 -0
- package/examples/minimal.ts +15 -0
- package/examples/stream.ts +39 -0
- package/examples/support.ts +71 -0
- package/package.json +10 -5
- package/prd/01-overview.md +21 -0
- package/prd/02-goals.md +47 -0
- package/prd/03-terminology.md +32 -0
- package/prd/04-architecture.md +121 -0
- package/prd/05-api/5.1-starting-claude.md +108 -0
- package/prd/05-api/5.2-resuming-claude.md +86 -0
- package/prd/05-api/5.3-claude-session-api.md +658 -0
- package/prd/05-api/5.4-events.md +145 -0
- package/prd/05-api/5.5-starting-codex.md +106 -0
- package/prd/05-api/5.6-resuming-codex.md +48 -0
- package/prd/05-api/5.7-codex-session-api.md +425 -0
- package/prd/05-api/5.8-session-api.md +122 -0
- package/prd/05-api/5.9-loops.md +170 -0
- package/prd/05-api/README.md +19 -0
- package/prd/06-claude-hook-bridge.md +164 -0
- package/prd/07-claude-policy.md +15 -0
- package/prd/07a-codex-hook-bridge.md +76 -0
- package/prd/08-state.md +127 -0
- package/prd/09-lifecycle.md +222 -0
- package/prd/10-errors.md +55 -0
- package/prd/11-test-app.md +53 -0
- package/prd/12-e2e.md +23 -0
- package/prd/12a-cli.md +375 -0
- package/prd/13-latitude.md +12 -0
- package/prd/14-conformance.md +333 -0
- package/prd/15-open-notes.md +8 -0
- package/prd/README.md +45 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
## 9. Process Lifecycle
|
|
2
|
+
|
|
3
|
+
### 9.1 Startup
|
|
4
|
+
|
|
5
|
+
`startClaude` performs these observable steps:
|
|
6
|
+
|
|
7
|
+
1. Resolve `cwd` and state directory.
|
|
8
|
+
2. Preflight platform support.
|
|
9
|
+
3. Preflight `claude` availability and version.
|
|
10
|
+
4. Create an Elwood session record.
|
|
11
|
+
5. Generate session-scoped Claude settings.
|
|
12
|
+
6. Start the local hook IPC endpoint.
|
|
13
|
+
7. Spawn the user's shell in a PTY.
|
|
14
|
+
8. Launch `claude` from that shell with generated settings and session env.
|
|
15
|
+
9. Return a `ClaudeSessionApi` object once the process and bridge are ready.
|
|
16
|
+
|
|
17
|
+
`startCodex` performs the analogous startup sequence for Codex:
|
|
18
|
+
|
|
19
|
+
1. Resolve `cwd` and state directory.
|
|
20
|
+
2. Preflight platform support.
|
|
21
|
+
3. Preflight `codex` availability and version.
|
|
22
|
+
4. Create an Elwood session record.
|
|
23
|
+
5. Generate session-scoped hook bridge runtime files.
|
|
24
|
+
6. Start the local hook IPC endpoint.
|
|
25
|
+
7. Spawn the user's shell in a PTY.
|
|
26
|
+
8. Launch `codex` from that shell with generated `--config` hook overrides and
|
|
27
|
+
session env.
|
|
28
|
+
9. Return a `CodexSessionApi` object once the process and bridge are ready.
|
|
29
|
+
|
|
30
|
+
### 9.2 Compatibility checks
|
|
31
|
+
|
|
32
|
+
Elwood MUST check the installed Claude Code version during startup. The minimum
|
|
33
|
+
supported Claude Code version is `2.1.144`.
|
|
34
|
+
|
|
35
|
+
Elwood MUST check the installed Codex CLI version during `startCodex`. The
|
|
36
|
+
minimum supported Codex CLI version is `0.124.0`.
|
|
37
|
+
|
|
38
|
+
If `autoupdate` is true, Elwood first verifies that the CLI exists, then runs
|
|
39
|
+
the adapter's update command, then reads the version again before enforcing the
|
|
40
|
+
minimum version. The update step is BEST-EFFORT maintenance, not a control gate:
|
|
41
|
+
if the update command itself fails (nonzero exit, timeout, or a subprocess
|
|
42
|
+
error), Elwood MUST NOT fail startup on that basis. Instead it re-reads the
|
|
43
|
+
installed version and enforces the minimum as usual — if the installed CLI still
|
|
44
|
+
meets the minimum, Elwood records a typed `agent_update_failed` warning and
|
|
45
|
+
starts the session normally from the installed binary; only if the installed
|
|
46
|
+
version is below the minimum does startup fail (with the same typed
|
|
47
|
+
compatibility error below). A failed update is attempted at most once per parent
|
|
48
|
+
process and MUST NOT be retried per start, nor may one failed shared update
|
|
49
|
+
reject or poison any other otherwise-compatible start in the roster. If the
|
|
50
|
+
installed agent version is below the minimum after that optional update step,
|
|
51
|
+
startup fails with a typed error that names the required version and feature. If
|
|
52
|
+
the version cannot be parsed, Elwood records a typed `version_unparseable`
|
|
53
|
+
warning and continues by default, with an option for callers to make this fatal.
|
|
54
|
+
|
|
55
|
+
The `agent_update_failed` warning carries only safe diagnostics — the adapter,
|
|
56
|
+
the installed version that will be used, the update command's exit status, an
|
|
57
|
+
allowlisted error code (e.g. a timeout/`errno`), and bounded captured stderr —
|
|
58
|
+
never terminal transcripts, prompts, tokens, or environment secrets. Like all
|
|
59
|
+
warnings it is live-only and requires no consumer handling: a caller that does
|
|
60
|
+
not subscribe to the `warning` event is unaffected and the session still reaches
|
|
61
|
+
`ready`.
|
|
62
|
+
|
|
63
|
+
Version checks and optional `claude update` / `codex update` commands must run
|
|
64
|
+
through the user's configured macOS interactive login shell so PATH resolves as
|
|
65
|
+
it would in a normal Terminal.app session (see §4.2). Elwood uses the user's
|
|
66
|
+
configured login shell rather than trusting
|
|
67
|
+
an inherited `SHELL` environment override from the parent process.
|
|
68
|
+
|
|
69
|
+
These probes MUST run asynchronously and MUST NOT block the host process's
|
|
70
|
+
event loop: a parent that spawns a roster of sessions must not serialize their
|
|
71
|
+
preflights on a single thread. Elwood reads each adapter's `--version` at most
|
|
72
|
+
once per parent process — concurrent first reads share a single subprocess, and
|
|
73
|
+
later reads reuse the cached result — mirroring the once-per-process autoupdate
|
|
74
|
+
dedupe (C-LIFE-09). The cached version and adapter-capability reads are invalidated
|
|
75
|
+
after every coordinated update attempt, including a failed or peer-owned attempt,
|
|
76
|
+
because a failed installer may still have partially changed the binary. When
|
|
77
|
+
`autoupdate` is set, concurrent callers share one update and all validate the same
|
|
78
|
+
post-attempt version, so no caller proceeds on a stale pre-update version or races
|
|
79
|
+
a second update. This mutual exclusion also applies across separate Elwood parent
|
|
80
|
+
processes for the same macOS user and adapter: an atomic lease names the adapter in
|
|
81
|
+
a stable per-account cache independent of `TMPDIR`; contenders wait without
|
|
82
|
+
blocking the event loop, then invalidate their local caches and continue without
|
|
83
|
+
running a duplicate update. The lease records the owner's process id and a unique
|
|
84
|
+
generation. Cleanup removes only the generation it owns, a live owner is never
|
|
85
|
+
evicted solely because the stale bound elapsed, and recovery of a dead owner's
|
|
86
|
+
lease is itself serialized before removal, so neither cleanup nor concurrent stale
|
|
87
|
+
recovery can evict a successor. Within one parent process, a FAILED shared update
|
|
88
|
+
is likewise shared once — every concurrent caller observes the same failure,
|
|
89
|
+
re-reads the installed version, and proceeds through the compatibility gate; the
|
|
90
|
+
failure is never cached in a way that rejects those callers or poisons a later
|
|
91
|
+
start. A contender in another parent process does not receive the owner's live
|
|
92
|
+
warning event; after the lease releases it re-reads and validates the installed
|
|
93
|
+
binary without retrying the failed update. More generally, any per-process cached
|
|
94
|
+
probe (version read, capability detection, update) that fails is not retained as a
|
|
95
|
+
rejected result: a later caller re-attempts rather than inheriting the prior
|
|
96
|
+
failure.
|
|
97
|
+
|
|
98
|
+
Each probe is bounded so a broken or hostile CLI on PATH cannot hang or flood
|
|
99
|
+
the host: a probe that does not exit within a default timeout (15 seconds) or
|
|
100
|
+
whose captured output exceeds a per-stream byte cap (1,000,000 bytes) is
|
|
101
|
+
killed, and its captured output is truncated to the cap. Truncation happens on
|
|
102
|
+
a UTF-8 code-point boundary — an incomplete trailing sequence is dropped — so
|
|
103
|
+
the decoded output re-encodes to at most the cap rather than growing via a
|
|
104
|
+
replacement character. This applies to every
|
|
105
|
+
non-PTY probe — `--version`, `--help` capability detection, and
|
|
106
|
+
`claude update` / `codex update`. A `--version`/`--help` failure (bounded or
|
|
107
|
+
otherwise) surfaces through that probe's existing public error name —
|
|
108
|
+
`claude_start_failed` / `codex_start_failed` — with the underlying reason
|
|
109
|
+
preserved in the error `details` as `cause` (and `errno`, e.g. `ETIMEDOUT` on
|
|
110
|
+
timeout or `E2BIG` on overflow) alongside any `stderr`. An UPDATE failure
|
|
111
|
+
(`claude update` / `codex update`), being best-effort maintenance, does NOT
|
|
112
|
+
surface as a start-blocking error; it is contained and surfaces as the live
|
|
113
|
+
`agent_update_failed` warning carrying the same bounded diagnostics (exit status,
|
|
114
|
+
`errno`, `stderr`), while startup continues through the compatibility gate. A
|
|
115
|
+
killed probe is signaled only because Elwood aborted it; a probe that exits or
|
|
116
|
+
fails to spawn on its own is not signaled.
|
|
117
|
+
|
|
118
|
+
After spawning the PTY, Elwood waits briefly for immediate process exits or
|
|
119
|
+
known authentication/startup failure banners before reporting the session as
|
|
120
|
+
running. The default wait must be long enough to catch typical local CLI startup
|
|
121
|
+
failures without turning `startClaude` or `startCodex` into a readiness wait for
|
|
122
|
+
the first agent prompt. Authentication banner matching is best-effort and should
|
|
123
|
+
cover the common `not authenticated`, `login required`, `authentication failed`,
|
|
124
|
+
and non-MCP `not logged in` forms, as well as lapsed-session banners that only
|
|
125
|
+
direct the user to re-run `/login` (`Login expired`, `Session expired`,
|
|
126
|
+
`OAuth token revoked`, or `Not logged in`, each paired with a `run /login`
|
|
127
|
+
recovery hint).
|
|
128
|
+
|
|
129
|
+
Once the live resources exist (hook bridge, PTY, terminal, and transcript
|
|
130
|
+
watcher), every remaining startup step runs behind a single cleanup boundary: the
|
|
131
|
+
flush of any transcript diagnostics buffered before the session sink existed
|
|
132
|
+
(emitted as live warnings), PTY-exit registration, the readiness/authentication
|
|
133
|
+
assertion, and the startup-usable evidence. If ANY of them fails, Elwood tears
|
|
134
|
+
down the now-live PTY, hook bridge, terminal, and transcript watcher before
|
|
135
|
+
rejecting, so a failed `startClaude`/`startCodex` never leaks a live process, IPC
|
|
136
|
+
endpoint, or file watcher. (The one-shot version/preflight warning is NOT part of
|
|
137
|
+
this boundary — it is scheduled for delivery AFTER start resolves, per C-API-14.) Tearing down the PTY routes through the SAME one-shot
|
|
138
|
+
process-group reap the normal exit path uses (§9.4, C-LIFE-10): the PTY is
|
|
139
|
+
signaled, its exit is awaited within a bounded window, and the leader's process
|
|
140
|
+
group is SIGKILLed on every path — even if the PTY signal throws — so a
|
|
141
|
+
CLI-spawned descendant such as a hook-bridge grandchild (which POSIX reparents to
|
|
142
|
+
PID 1 the instant the leader exits) cannot survive or reparent past a failed
|
|
143
|
+
startup. The original startup error is preserved; a cleanup or reap failure is
|
|
144
|
+
secondary and never replaces it.
|
|
145
|
+
|
|
146
|
+
### 9.3 Resume
|
|
147
|
+
|
|
148
|
+
`resumeClaude` loads the Elwood session record and starts a new wrapper around
|
|
149
|
+
the same logical Claude conversation using Claude Code's resume mechanism.
|
|
150
|
+
|
|
151
|
+
Resume must re-establish hook routing, regenerate settings, and re-establish PTY
|
|
152
|
+
control. It does not restore any persisted terminal size (that
|
|
153
|
+
is not persisted, §8.2): the caller passes `initialSize` on resume to set
|
|
154
|
+
geometry, which otherwise falls back to the default. Resume must not require
|
|
155
|
+
callers to know Claude's internal session ID.
|
|
156
|
+
Resume must reject session records owned by another adapter and must fail
|
|
157
|
+
explicitly when the Claude resume id is not available.
|
|
158
|
+
Resume defaults the launch posture (privilege and tool policy) from the persisted
|
|
159
|
+
session record, as specified in §5.2: a bare `resumeClaude` relaunches with the
|
|
160
|
+
same `permissionMode`, `allowedTools`, `disallowedTools`, and `tools` the session
|
|
161
|
+
started with, so tool restrictions cannot silently loosen across a resume.
|
|
162
|
+
Explicit resume options override the persisted posture field by field, and the
|
|
163
|
+
effective posture is re-persisted. Model and caller config overrides remain
|
|
164
|
+
caller-supplied-per-call and are intentionally not persisted yet.
|
|
165
|
+
|
|
166
|
+
`resumeCodex` loads the Elwood session record and starts a new wrapper around the
|
|
167
|
+
same logical Codex conversation using `codex resume <SESSION_ID>` when Elwood has
|
|
168
|
+
observed and persisted Codex's session id. Resume must reject session records
|
|
169
|
+
owned by another adapter and must fail explicitly when the Codex resume id is not
|
|
170
|
+
available.
|
|
171
|
+
Resume defaults the launch posture (sandbox and approval policy) from the
|
|
172
|
+
persisted session record, as specified in §5.2, so a bare `resumeCodex` relaunches
|
|
173
|
+
with the same `sandbox` and `approvalPolicy` the session started with and
|
|
174
|
+
restrictions cannot silently loosen. Explicit resume options override the
|
|
175
|
+
persisted posture field by field, and the effective posture is re-persisted.
|
|
176
|
+
Model and caller config overrides remain caller-supplied-per-call and are
|
|
177
|
+
intentionally not persisted yet.
|
|
178
|
+
|
|
179
|
+
For either adapter, resume validates the loop sidecar before starting the agent,
|
|
180
|
+
silently removes definitions whose seven-day wall-clock expiry passed during
|
|
181
|
+
downtime, and restores surviving definitions with the same IDs and persisted
|
|
182
|
+
jitter. It restores no timer or due state: every surviving cadence starts fresh
|
|
183
|
+
from the resumed session's first readiness, using that launch's current
|
|
184
|
+
permissions and blocking-dialog posture, and no missed run is replayed.
|
|
185
|
+
|
|
186
|
+
### 9.4 Exit
|
|
187
|
+
|
|
188
|
+
When the agent process exits, Elwood emits terminal/process exit events and
|
|
189
|
+
updates the live session status. It keeps the persisted session record and
|
|
190
|
+
generated files, including loop definitions, unless kill or teardown is
|
|
191
|
+
requested. Any unsubmitted due state is discarded. (Session status is live-only and is
|
|
192
|
+
not written to the record, §8.2.)
|
|
193
|
+
|
|
194
|
+
Reaping the leader's process group is unconditional on every exit path
|
|
195
|
+
(C-LIFE-10). On an unsolicited PTY exit the terminal status is submitted first,
|
|
196
|
+
then the group is reaped in a `finally`, so transcript-drain, warning-emission, or
|
|
197
|
+
status-listener failures can never skip the reap; a reap failure there surfaces as
|
|
198
|
+
a live `reap_failed` warning rather than aborting the exit callback. An explicit
|
|
199
|
+
`stop()`/`kill()` on an already-terminal session performs only the one-shot
|
|
200
|
+
survivor reap (it does not re-signal the dead PTY — node-pty does not replay the
|
|
201
|
+
exit event, and the pid may already be recycled) and rejects with a typed
|
|
202
|
+
`termination_failed` error if that reap fails, leaving the reap retryable by a
|
|
203
|
+
later `teardown()`.
|
|
204
|
+
|
|
205
|
+
Overlapping `stop()`, `kill()`, and `teardown()` calls on one session are
|
|
206
|
+
serialized so the PTY is signaled at most once. The first caller owns the in-flight
|
|
207
|
+
shutdown; a concurrent caller of the same or a lower urgency joins that same
|
|
208
|
+
in-flight operation rather than snapshotting status independently and re-signaling
|
|
209
|
+
a PID that may already have exited and been recycled. A more urgent call (a
|
|
210
|
+
`kill()` during an in-flight `stop()`, or a `teardown()`) escalates by running after
|
|
211
|
+
the in-flight shutdown settles, then performs only the one-shot survivor reap and
|
|
212
|
+
its own extra work (e.g. teardown's file removal) and never issues a second PTY
|
|
213
|
+
signal. Signal ownership is tracked INDEPENDENTLY of lifecycle status: once any
|
|
214
|
+
shutdown has signaled the PTY, no later caller re-signals it — not even when the
|
|
215
|
+
predecessor signaled and reaped but failed before submitting a terminal status, so
|
|
216
|
+
the session is still non-terminal. Such a later caller may still reap, clean up,
|
|
217
|
+
and (for teardown) remove files. A settled shutdown failure is not permanently
|
|
218
|
+
sticky: because signal ownership is separate from the in-flight promise, a later
|
|
219
|
+
`stop()`/`kill()`/`teardown()` after a failed one runs a fresh attempt that RETRIES
|
|
220
|
+
the reap, runtime cleanup, and file removal — without re-signaling the PTY — rather
|
|
221
|
+
than returning the earlier rejection forever (C-LIFE-10 retry posture). Each caller
|
|
222
|
+
still observes its own operation's success or typed failure.
|
package/prd/10-errors.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
## 10. Error Model
|
|
2
|
+
|
|
3
|
+
Elwood errors exposed to library callers must be typed and stable. Error names
|
|
4
|
+
should be string-literal discriminants.
|
|
5
|
+
|
|
6
|
+
Initial required error names:
|
|
7
|
+
|
|
8
|
+
| Error name | Meaning |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `unsupported_platform` | The current OS is not supported by the implementation. |
|
|
11
|
+
| `claude_not_found` | `claude` could not be resolved or spawned. |
|
|
12
|
+
| `claude_start_failed` | Claude started but exited or failed before the session was usable. |
|
|
13
|
+
| `claude_update_failed` | `claude update` failed. Best-effort: contained by the autoupdate preflight and surfaced as the `agent_update_failed` warning (never thrown out of `startClaude` when the installed CLI meets the minimum). |
|
|
14
|
+
| `claude_not_authenticated` | Startup output or status indicates Claude is not authenticated. |
|
|
15
|
+
| `claude_version_unsupported` | Installed Claude version lacks required features. |
|
|
16
|
+
| `claude_invalid_reasoning_effort` | `reasoningEffort` was not one of the valid `ClaudeReasoningEffort` values; rejected before spawn. |
|
|
17
|
+
| `claude_high_trust_conflict` | `highTrust: true` was combined with an explicit `permissionMode`; rejected before spawn. |
|
|
18
|
+
| `codex_not_found` | `codex` could not be resolved or spawned. |
|
|
19
|
+
| `codex_start_failed` | Codex started but exited or failed before the session was usable. |
|
|
20
|
+
| `codex_update_failed` | `codex update` failed. Best-effort: contained by the autoupdate preflight and surfaced as the `agent_update_failed` warning (never thrown out of `startCodex` when the installed CLI meets the minimum). |
|
|
21
|
+
| `codex_not_authenticated` | Startup output or status indicates Codex is not authenticated. |
|
|
22
|
+
| `codex_version_unsupported` | Installed Codex version lacks required features. |
|
|
23
|
+
| `codex_invalid_reasoning_effort` | `reasoningEffort` was not one of the valid `CodexReasoningEffort` values; rejected before spawn (Codex would otherwise fail server-side at the first turn). |
|
|
24
|
+
| `codex_high_trust_conflict` | `highTrust: true` was combined with an explicit `sandbox` or `approvalPolicy`; rejected before spawn. |
|
|
25
|
+
| `state_not_found` | A requested Elwood session record does not exist. |
|
|
26
|
+
| `state_corrupt` | A session record exists but cannot be parsed or validated. |
|
|
27
|
+
| `adapter_mismatch` | A resume request targeted a session record owned by another adapter. |
|
|
28
|
+
| `resume_unavailable` | A resume request cannot be completed from available session metadata. |
|
|
29
|
+
| `pty_start_failed` | PTY or shell startup failed. |
|
|
30
|
+
| `hook_bridge_failed` | The hook bridge or IPC endpoint could not be initialized. |
|
|
31
|
+
| `session_not_running` | Operation requires a running process but the session is stopped. |
|
|
32
|
+
| `termination_failed` | A stop/kill request did not observe process exit after escalation, or the PTY exited but its process group could not be confirmed reaped. |
|
|
33
|
+
| `teardown_failed` | Elwood could not remove all owned session files. |
|
|
34
|
+
| `compact_failed` | A requested conversation compaction did not report completion in time. |
|
|
35
|
+
| `interrupt_failed` | A requested turn interrupt did not return the session to ready in time. |
|
|
36
|
+
| `model_automation_failed` | The adapter's model picker could not be recognized or driven to completion. |
|
|
37
|
+
| `login_failed` | The interactive `/login` re-authentication flow reported an explicit failure or an invalid authorization code. |
|
|
38
|
+
| `login_timeout` | The interactive `/login` re-authentication flow did not report success before the timeout. |
|
|
39
|
+
| `invalid_image` | An `images` input is not attachable: an unsupported/absent byte format, empty bytes, a path that is not a readable file, or a total image count/size beyond the documented limits (C-API-44). Raised before any partial input reaches the composer. |
|
|
40
|
+
| `image_attach_failed` | An image could not be confirmed attached: the CLI's `[Image #N]` chip did not appear before the confirmation timeout, or the OS clipboard could not be read/written. No text is submitted afterward. (A session that TERMINATES mid-attach rejects with `session_not_running`, like any queued op.) (C-API-44/45/46) |
|
|
41
|
+
| `wait_timeout` | A `waitForStatus`/`waitForActivity` call did not observe its condition before the timeout. |
|
|
42
|
+
| `invalid_loop` | A loop request or recognized `/loop` command violates the documented shape, interval, or UTF-8 message bounds. |
|
|
43
|
+
| `loop_limit_reached` | Creating a loop would exceed 50 unexpired definitions; existing loops are unchanged. |
|
|
44
|
+
| `loop_not_found` | Cancellation targeted an unknown or already-finished loop ID. |
|
|
45
|
+
| `loop_persistence_failed` | A loop-sidecar write required by creation, cancellation, kill, expiry, or teardown did not complete safely. |
|
|
46
|
+
| `loop_submission_failed` | Live loop scheduling or readiness-safe prompt submission failed; details identify only the loop ID. |
|
|
47
|
+
|
|
48
|
+
Hook handler failures are normally surfaced as `hookError` events, not thrown
|
|
49
|
+
from the hook bridge path.
|
|
50
|
+
|
|
51
|
+
Startup errors MUST NOT swallow their underlying cause: `pty_start_failed`,
|
|
52
|
+
`hook_bridge_failed`, and adapter start failures carry the underlying error
|
|
53
|
+
message in `details.cause`, plus `errno`, `syscall`, and `path` fields when
|
|
54
|
+
the underlying error exposes them, so consumers can diagnose environment
|
|
55
|
+
failures without instrumenting Elwood internals.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
## 11. Local Test App
|
|
2
|
+
|
|
3
|
+
The repository should include a small local developer test app. It is not the
|
|
4
|
+
product's primary surface, but it is required for manual acceptance testing.
|
|
5
|
+
|
|
6
|
+
The browser dev app should be launched with `npm run dev:web`, but its
|
|
7
|
+
PTY-owning process SHOULD run under Node when using `node-pty`, because the
|
|
8
|
+
native PTY binding is a Node dependency. npm remains the package script runner,
|
|
9
|
+
and Vitest is the default test runner.
|
|
10
|
+
|
|
11
|
+
The repository should also include runnable examples for the public library API.
|
|
12
|
+
Examples that own real PTYs SHOULD be exposed through package scripts that run a
|
|
13
|
+
small Node supervisor, rather than asking developers to invoke
|
|
14
|
+
`node --experimental-strip-types` directly. Example output should demonstrate
|
|
15
|
+
Elwood as a headless library by logging structured events, not by mirroring the
|
|
16
|
+
wrapped agent's raw terminal stream into the caller's shell.
|
|
17
|
+
|
|
18
|
+
The test app must:
|
|
19
|
+
|
|
20
|
+
- start a Claude or Codex session for a selected `cwd`;
|
|
21
|
+
- resume a saved Elwood session ID;
|
|
22
|
+
- render the live PTY session with xterm.js or an equivalent terminal renderer;
|
|
23
|
+
- send prompts, including multi-line prompts;
|
|
24
|
+
- send raw key/input sequences;
|
|
25
|
+
- resize the terminal;
|
|
26
|
+
- show current session status;
|
|
27
|
+
- show a chronological live hook/event log with event name, timestamp, payload
|
|
28
|
+
summary, handler result summary, and whether Claude received no decision,
|
|
29
|
+
allow, deny, block, context, or another response;
|
|
30
|
+
- show rich structured debugger entries for hooks, unified activity events,
|
|
31
|
+
warnings, hook errors, lifecycle status, terminal exits, and startup/runtime
|
|
32
|
+
errors;
|
|
33
|
+
- provide an inspector for each debugger entry that exposes the raw structured
|
|
34
|
+
payload as formatted JSON so developers can validate typed fields without
|
|
35
|
+
reading terminal escape output;
|
|
36
|
+
- visually delineate debugger entries by event kind and severity with compact
|
|
37
|
+
labels, color, timestamps, and searchable/filterable categories;
|
|
38
|
+
- support a manual smoke test where a developer can have a conversation with
|
|
39
|
+
Claude or Codex and visibly confirm hook coverage;
|
|
40
|
+
- clean up its HTTP server, WebSocket server, active Elwood session, and child
|
|
41
|
+
process tree on SIGTERM/SIGHUP and job-control signals such as SIGTTIN. On
|
|
42
|
+
SIGINT/Ctrl-C, it MUST immediately SIGKILL descendant processes and itself
|
|
43
|
+
without waiting for graceful cleanup. The `dev:web` package script should run
|
|
44
|
+
the web server under a small process supervisor so terminal Ctrl-C can kill the
|
|
45
|
+
entire spawned process group even if the app-level handler or an agent child is
|
|
46
|
+
wedged. The web dev app must not read from stdin for shutdown handling because
|
|
47
|
+
terminal reads can suspend the job instead of killing it. When a browser
|
|
48
|
+
session starts or resumes, the PTY mirror should render only raw replayed and
|
|
49
|
+
live `terminal:data` chunks in its visual xterm.js instance so startup TUI
|
|
50
|
+
bytes remain visible without clearing or rewriting the visual terminal from a
|
|
51
|
+
headless snapshot.
|
|
52
|
+
|
|
53
|
+
The test app must not become required for library consumers.
|
package/prd/12-e2e.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## 12. End-To-End Testing
|
|
2
|
+
|
|
3
|
+
The repository should provide `npm run test:e2e` as a separate, opt-in quality
|
|
4
|
+
gate for slow and potentially paid real-agent testing. The e2e suite MUST run
|
|
5
|
+
the public TypeScript library API against real local CLI processes and real
|
|
6
|
+
PTYs, minimizing mocks and seams. It should verify the critical library flows
|
|
7
|
+
for each installed/authenticated supported adapter: start, terminal output,
|
|
8
|
+
raw input, prompt/message submission, hook observation, activity observation,
|
|
9
|
+
resize, stop, resume when the adapter reports a resume id, kill/teardown, and
|
|
10
|
+
Elwood-owned state cleanup.
|
|
11
|
+
|
|
12
|
+
`test:e2e` may skip an adapter when its CLI is not installed or cannot pass a
|
|
13
|
+
basic version/authentication preflight, but it should not use fake PTYs, fake
|
|
14
|
+
hook bridges, or fake agent processes for the adapter flows it does run. Because
|
|
15
|
+
the suite depends on local auth, network, model availability, and user-installed
|
|
16
|
+
CLIs, it is intentionally separate from `npm run check`. E2E coverage should be
|
|
17
|
+
reported separately and made as high as practical; unit/conformance tests remain
|
|
18
|
+
responsible for hard-to-force error paths and 100% default coverage.
|
|
19
|
+
|
|
20
|
+
The repository should also provide `npm run check:all`, an opt-in convenience
|
|
21
|
+
gate that runs the default `npm run check` gate and then `npm run test:e2e`.
|
|
22
|
+
`check:all` does not change the composition of `npm run check`; the default gate
|
|
23
|
+
remains free of real-agent dependencies.
|