@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,56 @@
|
|
|
1
|
+
# Elwood Examples
|
|
2
|
+
|
|
3
|
+
## Minimal: ask and answer (`minimal.ts`)
|
|
4
|
+
|
|
5
|
+
The smallest possible use of Elwood. Construct a session, `send` a message, get the
|
|
6
|
+
assistant's reply back as a string, ask a follow-up. The session starts lazily on the
|
|
7
|
+
first `send`, so there is no start ceremony and no manual "wait for ready".
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm run example:minimal
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const session = new CodexSession({ autotrust: true });
|
|
15
|
+
const joke = await session.send("Tell me a joke that involves a dog.");
|
|
16
|
+
const rewrite = await session.send("Now rewrite it to be about a cat.");
|
|
17
|
+
await session.close();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Streaming: watch the work (`stream.ts`)
|
|
21
|
+
|
|
22
|
+
When you want the intermediate steps, `stream` yields typed events (`thinking`,
|
|
23
|
+
`tool_call`, `tool_result`, `text`) as they arrive. `send` is just this generator
|
|
24
|
+
drained for the `text`, so the two share one turn boundary.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm run example:stream
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
for await (const event of session.stream("List the files, then summarize the project.")) {
|
|
32
|
+
if (event.type === "text") process.stdout.write(event.text);
|
|
33
|
+
// event.type is also "thinking" | "tool_call" | "tool_result"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Full: the raw control surface (`full.ts`)
|
|
38
|
+
|
|
39
|
+
`full.ts` drops below the ergonomic facade to the low-level API: `startClaude`/
|
|
40
|
+
`startCodex`, raw `activity`/`warning`/`terminal:exit` events, an explicit
|
|
41
|
+
`waitForSettled` helper (from `support.ts`) that watches `status` and
|
|
42
|
+
`terminal:exit`, and CLI flags for agent/cwd/prompt/timeout. Reach for this when you
|
|
43
|
+
need full control over the session lifecycle; most callers want `send`/`stream` above.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm run example:full
|
|
47
|
+
npm run example:full -- --agent codex --cwd . --prompt "Summarize this repo in one paragraph."
|
|
48
|
+
npm run example:full -- --agent claude --cwd . --prompt "Summarize this repo in one paragraph."
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
All examples use `autotrust: true` so embedded sessions can move past known workspace
|
|
52
|
+
trust prompts. The package scripts run the TypeScript examples under a small Node
|
|
53
|
+
supervisor because `node-pty` owns real PTYs more reliably there and the supervisor can
|
|
54
|
+
kill the example process tree on Ctrl-C. The examples import from `../src/index.ts`
|
|
55
|
+
so they run from a checkout without a build; in your own project import the same
|
|
56
|
+
symbols from `@with-logic/elwood`.
|
package/examples/full.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full runnable Elwood example for starting one agent session, sending one
|
|
3
|
+
* message, logging rich events, and cleaning up the process.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { startClaude, startCodex } from "../src/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
type Agent,
|
|
10
|
+
cleanupSession,
|
|
11
|
+
type ExampleSession,
|
|
12
|
+
errorMessage,
|
|
13
|
+
isTerminalStatus,
|
|
14
|
+
textSuffix,
|
|
15
|
+
waitForSettled,
|
|
16
|
+
} from "./support.ts";
|
|
17
|
+
|
|
18
|
+
type Options = {
|
|
19
|
+
readonly agent: Agent;
|
|
20
|
+
readonly cwd: string;
|
|
21
|
+
readonly keep: boolean;
|
|
22
|
+
readonly prompt: string;
|
|
23
|
+
readonly timeoutMs: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const defaultPrompt = "Say hello from Elwood in one short sentence, then stop.";
|
|
27
|
+
const options = parseArgs(process.argv.slice(2));
|
|
28
|
+
let session: ExampleSession | undefined;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
session = await startSession(options);
|
|
32
|
+
wireLogs(session);
|
|
33
|
+
process.stdout.write(`started ${options.agent} session ${session.elwoodSessionId}\n`);
|
|
34
|
+
await session.sendMessage(options.prompt);
|
|
35
|
+
if (!(await waitForSettled(session, options.timeoutMs))) {
|
|
36
|
+
throw new Error(`Timed out after ${options.timeoutMs}ms waiting for the agent to finish.`);
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
process.stderr.write(`${errorMessage(error)}\n`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
} finally {
|
|
42
|
+
if (session && !options.keep && !isTerminalStatus(session.status)) await cleanupSession(session);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function startSession(options: Options): Promise<ExampleSession> {
|
|
46
|
+
const common = { cwd: options.cwd, autotrust: true, initialSize: { cols: 120, rows: 40 } };
|
|
47
|
+
return options.agent === "claude" ? await startClaude(common) : await startCodex(common);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function wireLogs(session: ExampleSession): void {
|
|
51
|
+
session.on("activity", (event) => {
|
|
52
|
+
process.stdout.write(
|
|
53
|
+
`[activity] ${event.agent}:${event.kind}:${event.label}${textSuffix(event.text)}\n`,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
session.on("warning", (event) => {
|
|
57
|
+
process.stdout.write(`[warning] ${event.agent}:${event.code} ${event.message}\n`);
|
|
58
|
+
});
|
|
59
|
+
session.on("hookError", (event) => {
|
|
60
|
+
process.stdout.write(`[hookError] ${event.hookEventName}:${event.category} ${event.message}\n`);
|
|
61
|
+
});
|
|
62
|
+
session.on("terminal:exit", (event) => {
|
|
63
|
+
process.stdout.write(`[terminal:exit] code=${event.exitCode}\n`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseArgs(args: readonly string[]): Options {
|
|
68
|
+
let agent: Agent = "codex";
|
|
69
|
+
let cwd = process.cwd();
|
|
70
|
+
let keep = false;
|
|
71
|
+
let prompt = defaultPrompt;
|
|
72
|
+
let timeoutMs = 120_000;
|
|
73
|
+
let index = 0;
|
|
74
|
+
while (index < args.length) {
|
|
75
|
+
const arg = args[index];
|
|
76
|
+
if (arg === "--help") return printUsageAndExit();
|
|
77
|
+
if (arg === "--agent")
|
|
78
|
+
[agent, index] = [parseAgent(requireValue(args, index + 1, arg)), index + 2];
|
|
79
|
+
else if (arg === "--cwd")
|
|
80
|
+
[cwd, index] = [resolve(requireValue(args, index + 1, arg)), index + 2];
|
|
81
|
+
else if (arg === "--prompt") [prompt, index] = [requireValue(args, index + 1, arg), index + 2];
|
|
82
|
+
else if (arg === "--timeout-ms")
|
|
83
|
+
[timeoutMs, index] = [Number(requireValue(args, index + 1, arg)), index + 2];
|
|
84
|
+
else if (arg === "--keep") [keep, index] = [true, index + 1];
|
|
85
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
86
|
+
}
|
|
87
|
+
if (!prompt) throw new Error("--prompt must not be empty.");
|
|
88
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0)
|
|
89
|
+
throw new Error("--timeout-ms must be positive.");
|
|
90
|
+
return { agent, cwd: resolve(cwd), keep, prompt, timeoutMs };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function requireValue(args: readonly string[], index: number, flag: string): string {
|
|
94
|
+
const value = args[index];
|
|
95
|
+
if (!value) throw new Error(`Missing value for ${flag}.`);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseAgent(value: string): Agent {
|
|
100
|
+
if (value === "claude" || value === "codex") return value;
|
|
101
|
+
throw new Error("--agent must be claude or codex.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function printUsageAndExit(): never {
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
[
|
|
107
|
+
"Usage: npm run example:full",
|
|
108
|
+
' npm run example:full -- --agent codex --cwd . --prompt "Summarize this repo."',
|
|
109
|
+
"",
|
|
110
|
+
].join("\n"),
|
|
111
|
+
);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest Elwood example: ask a question, get the answer, ask a follow-up.
|
|
3
|
+
* The ergonomic facade starts Codex lazily on the first `send` and gives back the
|
|
4
|
+
* assistant's text as a string (PRD §5.8).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CodexSession } from "../src/index.ts";
|
|
8
|
+
|
|
9
|
+
const session = new CodexSession({ autotrust: true });
|
|
10
|
+
try {
|
|
11
|
+
process.stdout.write(`${await session.send("Tell me a joke that involves a dog.")}\n`);
|
|
12
|
+
process.stdout.write(`${await session.send("Now rewrite it to be about a cat.")}\n`);
|
|
13
|
+
} finally {
|
|
14
|
+
await session.close();
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ergonomic streaming: watch a turn's typed events — thinking, tool calls/results, and
|
|
3
|
+
* assistant text — as they arrive, instead of waiting for the final string (PRD §5.8).
|
|
4
|
+
* `send` is just this generator drained for text; `stream` is the same turn, live.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ClaudeSession } from "../src/index.ts";
|
|
8
|
+
|
|
9
|
+
/** Compile-time exhaustiveness guard: reaching this with a real value means a case is missing. */
|
|
10
|
+
function assertNever(value: never): never {
|
|
11
|
+
throw new Error(`unhandled turn event: ${JSON.stringify(value)}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const session = new ClaudeSession({ autotrust: true });
|
|
15
|
+
try {
|
|
16
|
+
for await (const event of session.stream("List the files here, then summarize the project.")) {
|
|
17
|
+
switch (event.type) {
|
|
18
|
+
case "thinking":
|
|
19
|
+
process.stderr.write(` (thinking) ${event.text}\n`);
|
|
20
|
+
break;
|
|
21
|
+
case "tool_call":
|
|
22
|
+
process.stdout.write(`→ ${event.name}${event.input ? ` ${event.input}` : ""}\n`);
|
|
23
|
+
break;
|
|
24
|
+
case "tool_result":
|
|
25
|
+
process.stdout.write(`← ${event.output ?? ""}\n`);
|
|
26
|
+
break;
|
|
27
|
+
case "text":
|
|
28
|
+
process.stdout.write(event.text);
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
// Exhaustiveness: adding a new TurnEvent variant makes this fail to compile, so the
|
|
32
|
+
// canonical example can never silently ignore a newly-surfaced event kind.
|
|
33
|
+
assertNever(event);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write("\n");
|
|
37
|
+
} finally {
|
|
38
|
+
await session.close();
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for runnable Elwood examples.
|
|
3
|
+
* Implements PRD §11 example behavior.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ElwoodActivityEvent,
|
|
8
|
+
ElwoodSessionStatus,
|
|
9
|
+
ElwoodWarningEvent,
|
|
10
|
+
HookErrorEvent,
|
|
11
|
+
Unsubscribe,
|
|
12
|
+
} from "../src/index.ts";
|
|
13
|
+
|
|
14
|
+
export type Agent = "claude" | "codex";
|
|
15
|
+
export type ExampleSession = {
|
|
16
|
+
readonly elwoodSessionId: string;
|
|
17
|
+
readonly status: ElwoodSessionStatus;
|
|
18
|
+
on(event: "activity", handler: (event: ElwoodActivityEvent) => void): Unsubscribe;
|
|
19
|
+
on(event: "warning", handler: (event: ElwoodWarningEvent) => void): Unsubscribe;
|
|
20
|
+
on(event: "hookError", handler: (event: HookErrorEvent) => void): Unsubscribe;
|
|
21
|
+
on(
|
|
22
|
+
event: "status",
|
|
23
|
+
handler: (event: { readonly status: ElwoodSessionStatus }) => void,
|
|
24
|
+
): Unsubscribe;
|
|
25
|
+
on(event: "terminal:exit", handler: (event: { readonly exitCode: number }) => void): Unsubscribe;
|
|
26
|
+
sendMessage(message: string): Promise<void>;
|
|
27
|
+
stop(): Promise<void>;
|
|
28
|
+
kill(): Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function waitForSettled(session: ExampleSession, timeoutMs: number): Promise<boolean> {
|
|
32
|
+
if (session.status === "ready" || isTerminalStatus(session.status)) return Promise.resolve(true);
|
|
33
|
+
return new Promise((resolveWait) => {
|
|
34
|
+
const done = (result: boolean) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
for (const unsubscribe of unsubs) unsubscribe();
|
|
37
|
+
resolveWait(result);
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
40
|
+
const unsubs: Unsubscribe[] = [
|
|
41
|
+
session.on("status", (event) => {
|
|
42
|
+
if (event.status === "ready" || isTerminalStatus(event.status)) done(true);
|
|
43
|
+
}),
|
|
44
|
+
session.on("terminal:exit", () => done(true)),
|
|
45
|
+
];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function cleanupSession(session: ExampleSession): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
await session.stop();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
process.stderr.write(`stop failed, killing session: ${errorMessage(error)}\n`);
|
|
54
|
+
await session.kill();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isTerminalStatus(status: ElwoodSessionStatus): boolean {
|
|
59
|
+
return (
|
|
60
|
+
status === "stopped" || status === "exited" || status === "killed" || status === "torn_down"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function textSuffix(text: string | undefined): string {
|
|
65
|
+
if (!text) return "";
|
|
66
|
+
return ` ${text.replace(/\s+/g, " ").slice(0, 120)}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function errorMessage(error: unknown): string {
|
|
70
|
+
return error instanceof Error ? error.message : String(error);
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@with-logic/elwood",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Control interactive agentic coding CLIs (Claude Code, Codex) through real terminal sessions: typed hooks, a unified activity stream, resumable sessions, and a headless CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Logic App, Inc.",
|
|
@@ -26,7 +26,12 @@
|
|
|
26
26
|
"type": "module",
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
|
-
"docs
|
|
29
|
+
"docs/**/*.md",
|
|
30
|
+
"prd/**/*.md",
|
|
31
|
+
"examples/*.ts",
|
|
32
|
+
"examples/README.md",
|
|
33
|
+
"CONTRIBUTING.md",
|
|
34
|
+
"SECURITY.md",
|
|
30
35
|
"CHANGELOG.md",
|
|
31
36
|
"README.md"
|
|
32
37
|
],
|
|
@@ -60,9 +65,9 @@
|
|
|
60
65
|
"check:lines": "node --no-warnings scripts/check-lines.ts",
|
|
61
66
|
"check": "npm run build && npm run typecheck && npm run lint && npm run check:lines && npm test",
|
|
62
67
|
"check:all": "npm run check && npm run test:e2e",
|
|
63
|
-
"test:e2e": "exec node --no-warnings --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-exclude='
|
|
64
|
-
"dev:app": "exec node --no-warnings
|
|
65
|
-
"dev:web": "exec node --no-warnings scripts/supervise.ts
|
|
68
|
+
"test:e2e": "exec node --no-warnings --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-exclude='tests/**' --test-timeout=240000 --test-concurrency=1 --test tests/e2e/*.e2e.ts",
|
|
69
|
+
"dev:app": "exec node --no-warnings dev/dev.ts",
|
|
70
|
+
"dev:web": "exec node --no-warnings scripts/supervise.ts dev/web/dev.ts",
|
|
66
71
|
"example:minimal": "exec node --no-warnings scripts/supervise.ts examples/minimal.ts",
|
|
67
72
|
"example:stream": "exec node --no-warnings scripts/supervise.ts examples/stream.ts",
|
|
68
73
|
"example:full": "exec node --no-warnings scripts/supervise.ts examples/full.ts"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## 1. Overview
|
|
2
|
+
|
|
3
|
+
Elwood lets an application control an agentic coding CLI that is running in its
|
|
4
|
+
native interactive terminal mode, without forcing a human to operate the TUI.
|
|
5
|
+
|
|
6
|
+
The first supported tool is Claude Code. The second supported tool is OpenAI
|
|
7
|
+
Codex CLI. Elwood starts the selected agent inside a real pseudoterminal,
|
|
8
|
+
configured so the agent sees the same kind of interactive shell session it would
|
|
9
|
+
see if the user opened Terminal.app and ran the CLI manually. Elwood then
|
|
10
|
+
controls the session by writing terminal input, and observes the agent primarily
|
|
11
|
+
through tool-specific hooks routed over local IPC.
|
|
12
|
+
|
|
13
|
+
The core premise is deliberate: the agent should not know it is running in a
|
|
14
|
+
special wrapper. Elwood does not replace Claude Code or Codex with print mode,
|
|
15
|
+
non-interactive mode, SDKs, or custom protocols. It runs the real interactive
|
|
16
|
+
TUI and wraps it.
|
|
17
|
+
|
|
18
|
+
Parent applications can use Elwood headlessly, or attach a terminal renderer
|
|
19
|
+
such as xterm.js to show the live session. A likely parent app is a local
|
|
20
|
+
Tauri-style desktop app that manages many agent sessions, renders their status,
|
|
21
|
+
and lets a user inspect the actual terminal when needed.
|
package/prd/02-goals.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## 2. Goals And Non-Goals
|
|
2
|
+
|
|
3
|
+
### 2.1 Goals
|
|
4
|
+
|
|
5
|
+
- Provide a TypeScript library for starting, controlling, stopping, killing,
|
|
6
|
+
resuming, and tearing down interactive Claude Code and Codex CLI sessions.
|
|
7
|
+
- Run each agent in a real PTY so its TUI behaves as it would in a normal
|
|
8
|
+
terminal.
|
|
9
|
+
- Start Claude from the user's normal macOS interactive login shell environment,
|
|
10
|
+
including startup files such as `~/.zshrc` when that is what Terminal.app
|
|
11
|
+
would load.
|
|
12
|
+
- Route every Claude Code and Codex hook event through Elwood via a generated
|
|
13
|
+
per-session hook bridge.
|
|
14
|
+
- Expose every hook event to the parent app with strong TypeScript types.
|
|
15
|
+
- Let parent hook handlers return event-specific, strongly typed responses that
|
|
16
|
+
map to Claude Code's hook output protocol.
|
|
17
|
+
- Provide a unified agent-control surface where callers can send messages and
|
|
18
|
+
observe a rich chronological activity stream without branching on the
|
|
19
|
+
underlying adapter for common chat-loop workflows.
|
|
20
|
+
- Fail open when no hook handler exists, a handler times out, or a handler
|
|
21
|
+
fails at runtime.
|
|
22
|
+
- Send prompts through terminal input exactly as a human would, including
|
|
23
|
+
multi-line prompts.
|
|
24
|
+
- Persist only session metadata needed for resume and cleanup, plus loop
|
|
25
|
+
definitions that a caller explicitly creates as durable automation state.
|
|
26
|
+
- Avoid persisting raw terminal contents, ordinary user prompts, hook payloads,
|
|
27
|
+
or conversation logs by default. A loop prompt is the narrow exception: its
|
|
28
|
+
exact text is persisted because explicit recurring automation cannot be
|
|
29
|
+
restored without it.
|
|
30
|
+
- Provide a small local test app for manual development and acceptance testing.
|
|
31
|
+
|
|
32
|
+
### 2.2 Non-Goals
|
|
33
|
+
|
|
34
|
+
- Windows or Linux support in the MVP.
|
|
35
|
+
- Additional non-Claude/non-Codex adapters in the MVP.
|
|
36
|
+
- Using Claude Code print mode (`claude -p`) as the primary control mechanism.
|
|
37
|
+
- Cloud execution, remote hosts, or multi-machine session sync.
|
|
38
|
+
- Full visual parsing of Claude's TUI as the source of truth.
|
|
39
|
+
- Installing or updating Claude Code or Codex, or performing the INITIAL
|
|
40
|
+
authentication for a fresh install. Elwood assumes `claude`/`codex` is
|
|
41
|
+
installed, on `PATH`, and initially authenticated. Elwood DOES detect a lapsed
|
|
42
|
+
Claude login and can drive the interactive `/login` re-authentication flow to
|
|
43
|
+
recover a session whose login expired mid-run (C-CLAUDE-17/18, C-API-43); it
|
|
44
|
+
never performs the human browser sign-in itself.
|
|
45
|
+
- A first-class multi-session manager. Parent apps own orchestration across
|
|
46
|
+
many sessions.
|
|
47
|
+
- Durable audit logging of terminal or hook activity.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
## 3. Terminology
|
|
2
|
+
|
|
3
|
+
**Elwood session.** One wrapped interactive agent CLI process. A successfully started
|
|
4
|
+
session owns exactly one main CLI process: a live `ClaudeSessionApi` owns one main
|
|
5
|
+
`claude` process, a live `CodexSessionApi` one main `codex` process. The ergonomic
|
|
6
|
+
`ClaudeSession`/`CodexSession` classes (§5.8) are lazy wrappers that own NO process
|
|
7
|
+
before start and at most one after — each wraps a single such live session.
|
|
8
|
+
|
|
9
|
+
**Elwood session ID.** Stable opaque identifier generated by Elwood and exposed
|
|
10
|
+
to callers as `elwoodSessionId`. Callers use this ID to resume or tear down a
|
|
11
|
+
session. Claude's own session ID is internal metadata.
|
|
12
|
+
|
|
13
|
+
**Agent adapter.** Tool-specific implementation layer. The MVP adapter is
|
|
14
|
+
`claude` and `codex`. Future adapters might support other TUI-based agentic
|
|
15
|
+
CLIs.
|
|
16
|
+
|
|
17
|
+
**PTY session.** The pseudoterminal process and byte streams that make the CLI
|
|
18
|
+
believe it is running in an interactive terminal.
|
|
19
|
+
|
|
20
|
+
**Headless terminal.** Elwood-owned xterm.js terminal instance that renders PTY
|
|
21
|
+
output into a screen model and emits terminal input back to the PTY. This model
|
|
22
|
+
is required even when no UI is shown.
|
|
23
|
+
|
|
24
|
+
**Terminal renderer.** Optional parent-app UI component, such as browser
|
|
25
|
+
xterm.js, that renders the same live PTY stream for users.
|
|
26
|
+
|
|
27
|
+
**Hook bridge.** Elwood-owned command installed into agent hook settings. The
|
|
28
|
+
agent invokes it for hook events; it forwards hook input to the owning Elwood
|
|
29
|
+
runtime over local IPC and returns the runtime's response to the agent.
|
|
30
|
+
|
|
31
|
+
**Parent app.** The application embedding Elwood, commonly a Tauri or Electron
|
|
32
|
+
desktop app.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
## 4. Product Architecture
|
|
2
|
+
|
|
3
|
+
### 4.1 Layers
|
|
4
|
+
|
|
5
|
+
Elwood has three conceptual layers:
|
|
6
|
+
|
|
7
|
+
1. **PTY layer.** Starts and owns the shell/agent process, terminal input,
|
|
8
|
+
terminal output, resize, graceful stop, force kill, and process status.
|
|
9
|
+
2. **Agent adapter layer.** Adds tool-specific startup, generated config,
|
|
10
|
+
hook bridge, session metadata, and resume behavior. MVP adapters: Claude Code
|
|
11
|
+
and Codex CLI.
|
|
12
|
+
3. **Public library layer.** Exposes TypeScript APIs and typed events to parent
|
|
13
|
+
applications.
|
|
14
|
+
|
|
15
|
+
The PTY layer and a headless xterm.js terminal model are required. Elwood MUST
|
|
16
|
+
write PTY output through the headless terminal before inspecting visible TUI
|
|
17
|
+
state, warnings, or startup prompts. Elwood MUST send `sendPrompt` and
|
|
18
|
+
`sendKeys` input through the headless terminal input path so parent app input,
|
|
19
|
+
programmatic prompts, and low-level keys share one terminal control mechanism.
|
|
20
|
+
|
|
21
|
+
### 4.2 macOS shell behavior
|
|
22
|
+
|
|
23
|
+
On macOS, Elwood MUST start the PTY in the user's configured shell in a way that
|
|
24
|
+
matches the effective environment of opening Terminal.app and running the agent.
|
|
25
|
+
The implementation should use the user's login shell and interactive/login flags
|
|
26
|
+
appropriate for that shell. For the common zsh case, this is expected to behave
|
|
27
|
+
like an interactive login shell and load the user's normal startup files.
|
|
28
|
+
|
|
29
|
+
This interactive login form (`-l -i` for zsh) applies to both the **agent PTY**
|
|
30
|
+
and one-shot, non-PTY preflight probes (`--version`, `update`, `--help`). The
|
|
31
|
+
same shell startup files MUST resolve the CLI for every lifecycle operation so
|
|
32
|
+
preflight, update, launch, and resume cannot operate on different installations.
|
|
33
|
+
|
|
34
|
+
Elwood MUST provide diagnostics or tests proving that environment variables from
|
|
35
|
+
the user's shell startup are visible to the launched agent process.
|
|
36
|
+
|
|
37
|
+
### 4.3 Claude settings behavior
|
|
38
|
+
|
|
39
|
+
Elwood MUST NOT mutate `.claude/settings.local.json` by default.
|
|
40
|
+
|
|
41
|
+
For each Claude session, Elwood generates a session-scoped settings file or JSON
|
|
42
|
+
object containing only Elwood's necessary overrides:
|
|
43
|
+
|
|
44
|
+
- hook bridge handlers for every Claude hook event;
|
|
45
|
+
- any session-specific environment/settings required for routing.
|
|
46
|
+
|
|
47
|
+
Elwood launches Claude with Claude Code's `--settings <file-or-json>` mechanism
|
|
48
|
+
and relies on Claude Code's native settings merge precedence. Omitted keys in
|
|
49
|
+
Elwood's generated settings must leave user, project, and local settings intact.
|
|
50
|
+
Requested permission mode and allowed/disallowed tool policy may be supplied as
|
|
51
|
+
Claude CLI flags instead of generated settings when Claude exposes a stable flag
|
|
52
|
+
for that policy.
|
|
53
|
+
|
|
54
|
+
Decision note: Claude currently advertises both camelCase and kebab-case aliases
|
|
55
|
+
for tool-policy flags, but Claude's bundled docs and older CLI references name
|
|
56
|
+
`--allowedTools` and `--disallowedTools`. Elwood uses those documented camelCase
|
|
57
|
+
flags to maximize compatibility. The tool rule list is encoded as one
|
|
58
|
+
comma-separated flag value because current `claude --help` explicitly accepts
|
|
59
|
+
comma-separated or space-separated tool lists.
|
|
60
|
+
|
|
61
|
+
Beyond the allow/deny rule pair, `tools` exposes Claude's `--tools` flag — a
|
|
62
|
+
true allowlist over the built-in tool set where only the named tools exist
|
|
63
|
+
and newly added CLI tools default to absent. An empty `tools` array encodes
|
|
64
|
+
`--tools ""`, which Claude documents as disabling all tools. `allowedTools`/
|
|
65
|
+
`disallowedTools` govern permission rules; `tools` governs existence.
|
|
66
|
+
|
|
67
|
+
### 4.4 Codex configuration behavior
|
|
68
|
+
|
|
69
|
+
Elwood MUST NOT mutate `.codex/config.toml`, `.codex/hooks.json`,
|
|
70
|
+
`~/.codex/config.toml`, or `~/.codex/hooks.json` by default.
|
|
71
|
+
|
|
72
|
+
For each Codex session, Elwood generates only Elwood-owned runtime files under
|
|
73
|
+
the Elwood session metadata directory, then launches Codex with session-scoped
|
|
74
|
+
`--config` overrides that install hook bridge handlers for every supported Codex
|
|
75
|
+
hook event. Elwood reserves `features.hooks=true` and `hookTrust="trust-all"`
|
|
76
|
+
as session-scoped Codex overrides because Codex hooks must be enabled and trusted
|
|
77
|
+
for Elwood to function. User, project, managed, and system Codex config must
|
|
78
|
+
still merge through Codex's normal precedence rules.
|
|
79
|
+
|
|
80
|
+
Decision note: `features.hooks=true` is treated as Elwood-owned configuration,
|
|
81
|
+
not caller configuration. Without it, some Codex versions can parse hook config
|
|
82
|
+
without running hooks, which would make Elwood observe less than the parent app
|
|
83
|
+
expects. Elwood-owned hook command overrides may be emitted before caller
|
|
84
|
+
`configOverrides`, but reserved enablement/trust overrides MUST be emitted after
|
|
85
|
+
caller overrides so callers cannot accidentally disable the bridge.
|
|
86
|
+
|
|
87
|
+
Codex hook trust is a Codex security feature, but Elwood is not useful without
|
|
88
|
+
trusted hooks. Elwood MUST always allow Codex hooks to run. It should use
|
|
89
|
+
`hookTrust="trust-all"` in its session-scoped Codex config and should also use
|
|
90
|
+
Codex's invocation-scoped hook-trust bypass flag when the installed CLI
|
|
91
|
+
advertises it. If Codex still shows an interactive `Hooks need review` prompt,
|
|
92
|
+
Elwood MUST select the numbered option whose label is `Trust all and continue`
|
|
93
|
+
through PTY input; it must not assume that option always has the same number.
|
|
94
|
+
Prompt detection MUST operate on raw PTY output, including ANSI-styled and
|
|
95
|
+
cursor-addressed full-screen output where menu text may arrive as `2.Skip`
|
|
96
|
+
rather than as line-oriented `2. Skip` text.
|
|
97
|
+
|
|
98
|
+
### 4.5 Environment fidelity
|
|
99
|
+
|
|
100
|
+
An Elwood session MUST behave like the same agent launched from the user's own
|
|
101
|
+
terminal in the same working directory, plus Elwood's additive instrumentation.
|
|
102
|
+
Concretely:
|
|
103
|
+
|
|
104
|
+
- Sessions launch through the user's login+interactive shell in the caller's
|
|
105
|
+
`cwd`, inheriting the parent process environment plus Elwood's own session
|
|
106
|
+
variables (such as `ELWOOD_SESSION_ID`).
|
|
107
|
+
- Elwood MUST NOT modify, move, or shadow user-owned configuration: user and
|
|
108
|
+
project Claude settings, `CLAUDE.md`/`AGENTS.md`, skills, MCP configuration,
|
|
109
|
+
and Codex `config.toml` all load exactly as they would without Elwood.
|
|
110
|
+
- Elwood's hook wiring MUST be additive. User-configured hooks for the same
|
|
111
|
+
events continue to run alongside Elwood's bridge hooks. For Claude this holds
|
|
112
|
+
because `--settings` is a merged settings source; for Codex this holds
|
|
113
|
+
because CLI `-c hooks.*` overrides combine with `config.toml` hook groups
|
|
114
|
+
(verified against codex-cli 0.142).
|
|
115
|
+
- Known deviations MUST be documented rather than silent. Current deviations:
|
|
116
|
+
Codex hook trust is bypassed inside Elwood sessions because Elwood's reserved
|
|
117
|
+
`hookTrust="trust-all"` and trust-bypass flag apply to the whole session, so
|
|
118
|
+
a user hook that Codex would normally prompt about runs without a prompt; and
|
|
119
|
+
caller-provided `settingsOverrides` (§5.1) may intentionally shadow
|
|
120
|
+
project-level Claude settings keys because the generated `--settings` file is
|
|
121
|
+
a high-precedence source.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
## 5.1 Starting Claude
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
type StartClaudeOptions = {
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
readonly stateDir?: string;
|
|
7
|
+
readonly name?: string;
|
|
8
|
+
readonly initialSize?: TerminalSize;
|
|
9
|
+
readonly hooks?: ClaudeHookHandlers;
|
|
10
|
+
readonly persona?: string;
|
|
11
|
+
readonly model?: string;
|
|
12
|
+
readonly reasoningEffort?: ClaudeReasoningEffort;
|
|
13
|
+
readonly permissionMode?: ClaudePermissionMode;
|
|
14
|
+
readonly allowedTools?: readonly ClaudeToolRule[];
|
|
15
|
+
readonly disallowedTools?: readonly ClaudeToolRule[];
|
|
16
|
+
readonly tools?: readonly ClaudeToolRule[];
|
|
17
|
+
readonly settingsOverrides?: ClaudeSettingsOverrides;
|
|
18
|
+
readonly highTrust?: boolean;
|
|
19
|
+
readonly autoupdate?: boolean;
|
|
20
|
+
readonly autotrust?: boolean;
|
|
21
|
+
readonly hookTimeoutMs?: number;
|
|
22
|
+
readonly strictVersionCheck?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type ClaudeReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
26
|
+
|
|
27
|
+
declare function startClaude(options: StartClaudeOptions): Promise<ClaudeSessionApi>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`cwd` is the working directory where Claude should start. `stateDir` overrides
|
|
31
|
+
the default Elwood state directory. `name` is a launch-time convenience forwarded
|
|
32
|
+
to Claude's documented `--name` flag; it is not persisted in Elwood state (§8.2).
|
|
33
|
+
`hooks` registers
|
|
34
|
+
launch-time handlers before Claude starts. When `autoupdate` is true, Elwood
|
|
35
|
+
runs `claude update` from the user's login shell before spawning Claude and
|
|
36
|
+
rechecks the version after the update. The update command runs at most once
|
|
37
|
+
per parent process per adapter, so a parent spawning a roster of sessions at
|
|
38
|
+
launch does not run N concurrent same-binary updates (C-LIFE-09); later
|
|
39
|
+
sessions in the same process reuse the already-updated binary. Separate Elwood
|
|
40
|
+
parent processes for the same macOS user also coordinate through an atomic,
|
|
41
|
+
per-adapter lease in a stable account cache independent of each process's temporary
|
|
42
|
+
directory: one process runs the global updater while contenders wait asynchronously
|
|
43
|
+
for it to finish, then skip their duplicate attempt and re-read the installed
|
|
44
|
+
version. The lease records its owner and generation; an old owner cannot remove a
|
|
45
|
+
successor's lease, a live owner is never evicted solely because of elapsed time,
|
|
46
|
+
and a crashed owner's stale lease is recoverable after a fixed bound longer than
|
|
47
|
+
the update probe's own timeout. `strictVersionCheck` makes unparseable Claude
|
|
48
|
+
versions fatal instead of warning-and-continuing. `permissionMode`
|
|
49
|
+
accepts Claude's documented launch values: `default`, `acceptEdits`, `plan`,
|
|
50
|
+
`auto`, `dontAsk`, and `bypassPermissions`.
|
|
51
|
+
|
|
52
|
+
`highTrust` is the agent-neutral "never ask for permissions" switch. On Claude,
|
|
53
|
+
`highTrust: true` expands to `permissionMode: "bypassPermissions"` before the
|
|
54
|
+
posture is persisted or the process is spawned, so the record, `config
|
|
55
|
+
effective`, and the launched CLI all agree. It is an expansion, not a second
|
|
56
|
+
axis: supplying an explicit `permissionMode` together with `highTrust: true` is a
|
|
57
|
+
configuration error rejected before spawn with the typed
|
|
58
|
+
`claude_high_trust_conflict` error. When Claude starts in bypass-permissions
|
|
59
|
+
mode it may render a one-time acceptance dialog (header `WARNING: Claude Code
|
|
60
|
+
running in Bypass Permissions mode`, affirmative `Yes, I accept`, decline `No,
|
|
61
|
+
exit`); that dialog is an allowlisted trust prompt answered under `autotrust`
|
|
62
|
+
with `startup_prompt` label `bypass_permissions`, and with `autotrust` off it is
|
|
63
|
+
a blocking prompt like any other trust gate. Whether the installed CLI shows the
|
|
64
|
+
dialog at all is version- and machine-dependent (the CLI may persist the
|
|
65
|
+
acceptance), so Elwood answers it when present and otherwise proceeds. When the
|
|
66
|
+
CLI itself refuses bypass mode (for example when running as root), Elwood does
|
|
67
|
+
not special-case it: the refusal surfaces as the ordinary start failure carrying
|
|
68
|
+
the CLI's own message.
|
|
69
|
+
|
|
70
|
+
`model` is forwarded to Claude's documented `--model` launch flag so the
|
|
71
|
+
session starts on the requested model. Elwood does not validate the value;
|
|
72
|
+
Claude owns model-name resolution and its own failure behavior.
|
|
73
|
+
|
|
74
|
+
`reasoningEffort`, when set, is forwarded to Claude's documented `--effort`
|
|
75
|
+
launch flag so the session starts at the requested reasoning effort. Unlike
|
|
76
|
+
`model`, the value is a fixed per-CLI enum, so Elwood validates it against the
|
|
77
|
+
`ClaudeReasoningEffort` set BEFORE spawning and rejects an out-of-enum value with
|
|
78
|
+
the typed `claude_invalid_reasoning_effort` error (listing the valid values), so a
|
|
79
|
+
typo fails fast at start rather than deferring to the CLI. The effort applies to
|
|
80
|
+
the current session only and is NOT persisted by Elwood; a resumed session must
|
|
81
|
+
re-supply it (§5.2), consistent with `model`. Reasoning effort and model are
|
|
82
|
+
independent axes on Claude (the flag is separate from `--model`).
|
|
83
|
+
|
|
84
|
+
`autotrust` is an opt-in convenience for embedded/headless parent apps.
|
|
85
|
+
When true, Elwood detects Claude's first-party workspace trust prompt in the
|
|
86
|
+
rendered terminal and chooses the trust/continue option through PTY input. The
|
|
87
|
+
default is false because trusting a workspace is a security-sensitive decision.
|
|
88
|
+
When Elwood answers the prompt, the session emits adapter-neutral
|
|
89
|
+
`startup_prompt` activity with label `workspace_trust`.
|
|
90
|
+
|
|
91
|
+
Separately from `autotrust`, Elwood MUST dismiss Claude's non-security
|
|
92
|
+
first-party onboarding prompts that would otherwise block readiness in an
|
|
93
|
+
embedded session, always choosing the option that declines or defers the
|
|
94
|
+
offered capability. The currently known prompt of this kind is the browser
|
|
95
|
+
tools onboarding prompt, declined through PTY input (Escape) with
|
|
96
|
+
`startup_prompt` activity emitted under the label `browser_tools`. This
|
|
97
|
+
automation is unconditional because declining grants nothing and an unanswered
|
|
98
|
+
onboarding prompt permanently wedges a headless session.
|
|
99
|
+
|
|
100
|
+
`persona` is an optional instruction message for the agent. When set, Elwood
|
|
101
|
+
enqueues it before control returns to the caller, so it is guaranteed to be the
|
|
102
|
+
session's first submitted user message: it is delivered through the standard
|
|
103
|
+
message queue as soon as the session first becomes ready, ahead of any
|
|
104
|
+
caller-queued messages. Persona delivery is observable through the normal hook
|
|
105
|
+
and activity events, exactly like a caller-submitted message. The persona is
|
|
106
|
+
not persisted in Elwood state (§8.3 applies) and is not re-sent on resume. If
|
|
107
|
+
the session terminates before ever becoming ready, the undelivered persona is
|
|
108
|
+
discarded silently.
|