@aragon-agent/cli 0.5.12 → 0.6.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +322 -6
  3. package/dist/agent/agent-mode.d.ts +39 -0
  4. package/dist/agent/agent-mode.js +36 -0
  5. package/dist/agent/controller.d.ts +32 -0
  6. package/dist/agent/controller.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +16 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +143 -2
  10. package/dist/config/ui-state.d.ts +29 -0
  11. package/dist/config/ui-state.js +14 -4
  12. package/dist/diagnostics/cli-commands.d.ts +10 -0
  13. package/dist/diagnostics/cli-commands.js +61 -0
  14. package/dist/diagnostics/doctor.d.ts +29 -0
  15. package/dist/diagnostics/doctor.js +207 -0
  16. package/dist/diagnostics/info.d.ts +30 -0
  17. package/dist/diagnostics/info.js +99 -0
  18. package/dist/exec/cli-commands.d.ts +31 -0
  19. package/dist/exec/cli-commands.js +63 -0
  20. package/dist/exec/emitter.d.ts +80 -0
  21. package/dist/exec/emitter.js +127 -0
  22. package/dist/exec/events.d.ts +226 -0
  23. package/dist/exec/events.js +43 -0
  24. package/dist/exec/index.d.ts +56 -0
  25. package/dist/exec/index.js +439 -0
  26. package/dist/exec/options.d.ts +76 -0
  27. package/dist/exec/options.js +221 -0
  28. package/dist/exec/permission.d.ts +66 -0
  29. package/dist/exec/permission.js +96 -0
  30. package/dist/exec/runner.d.ts +204 -0
  31. package/dist/exec/runner.js +559 -0
  32. package/dist/exec/stdin-stream.d.ts +46 -0
  33. package/dist/exec/stdin-stream.js +144 -0
  34. package/dist/logging/install.d.ts +22 -0
  35. package/dist/logging/install.js +22 -0
  36. package/dist/session/cli-commands.d.ts +30 -0
  37. package/dist/session/cli-commands.js +184 -0
  38. package/dist/session/persist.d.ts +46 -0
  39. package/dist/session/persist.js +8 -0
  40. package/dist/session/store.d.ts +147 -0
  41. package/dist/session/store.js +442 -0
  42. package/dist/team/runtime.d.ts +12 -0
  43. package/dist/team/runtime.js +6 -0
  44. package/dist/team/subagent.d.ts +12 -0
  45. package/dist/team/subagent.js +4 -0
  46. package/dist/tools/index.d.ts +20 -0
  47. package/dist/tools/index.js +26 -0
  48. package/dist/ui/ActivityLine.d.ts +16 -0
  49. package/dist/ui/ActivityLine.js +23 -1
  50. package/dist/ui/App.d.ts +14 -1
  51. package/dist/ui/App.js +61 -7
  52. package/dist/ui/BottomStatusRow.d.ts +33 -1
  53. package/dist/ui/BottomStatusRow.js +20 -6
  54. package/dist/ui/Composer.d.ts +14 -1
  55. package/dist/ui/Composer.js +5 -5
  56. package/dist/ui/overlays/HelpOverlay.js +12 -1
  57. package/dist/ui/use-startup-notices.d.ts +98 -4
  58. package/dist/ui/use-startup-notices.js +108 -2
  59. package/dist/ui/win-vt-force.d.ts +136 -0
  60. package/dist/ui/win-vt-force.js +237 -0
  61. package/dist/ui/win-vt-input.d.ts +57 -0
  62. package/dist/ui/win-vt-input.js +62 -0
  63. package/package.json +7 -3
package/CHANGELOG.md CHANGED
@@ -5,10 +5,110 @@ All notable changes to `@aragon-agent/cli` are documented here. Entries at
5
5
  — those releases really were published under that name, so they are left as they
6
6
  were written.
7
7
 
8
+ ## 0.6.0
9
+
10
+ ### Added
11
+
12
+ - **`aragon exec` — a second, machine-facing face on the same binary.** The
13
+ package already installed from npm and already ran from a terminal, so "it has
14
+ a CLI" was true. It was nonetheless not *callable* by another program:
15
+ everything it exposed was shaped for a person, and a build script, a CI job, a
16
+ web backend or a second agent had to spawn it, capture free text, and guess.
17
+ There was no way to read the answer without also reading the narration, no way
18
+ to carry a conversation across two invocations, no way to say "you may read but
19
+ not write" when there is no human to answer `--confirm`, and no way to bound
20
+ what a run may spend.
21
+
22
+ `aragon exec` adds a **contract** rather than a feature: a versioned JSON event
23
+ schema on stdout, a session identifier the caller may mint itself, a tool
24
+ permission policy enforced at the tool boundary, caller-set budgets with their
25
+ own exit code, and a discovery command so a wrapper can ask what it just
26
+ installed before depending on it. Everything underneath is reused unchanged —
27
+ the same controller, the same tools, the same config resolution, the same
28
+ skills, teams, todo and fast-tier subsystems.
29
+
30
+ - `--output-format text | json | stream-json`. `stream-json` is NDJSON: one
31
+ object per line, `system/init` first, exactly one `result` last, on every
32
+ exit path including error, budget and signal. `text` is byte-identical to
33
+ `aragon -p`, because it **delegates** to the same renderer rather than
34
+ reimplementing it.
35
+ - `--session-id` / `--resume` / `--continue`, backed by `~/.aragon-agent/
36
+ sessions/` and an exclusive lock, so two runs cannot interleave and lose a
37
+ turn. A second run on a held id exits 2 with `session_busy` rather than
38
+ blocking.
39
+ - `--permission-mode auto|plan|strict` plus `--allow-tool` / `--deny-tool`.
40
+ A denied tool is **not registered at all**, so the model never sees it and
41
+ never plans around it — and the policy reaches subagents, so one `task` call
42
+ cannot bypass it.
43
+ - `--max-turns` / `--max-duration`, both aborting through the same path `Esc`
44
+ uses, both reporting `isError: false` and exit **3**: a budget firing is not
45
+ a malfunction, but it is not a complete answer either, and a shell script has
46
+ to be able to tell without parsing JSON.
47
+ - `--append-system-prompt` / `--append-system-prompt-file`. Appending only;
48
+ there is deliberately no full `--system-prompt` replacement, because the
49
+ builtin prompt carries tool discipline, todo planning, the skills catalog and
50
+ the plan-mode block, and replacing it silently disables half the product.
51
+ - `--input-format stream-json`, so one process can serve several turns from a
52
+ pipe. A malformed line is skipped with a non-fatal `error` event; an unknown
53
+ message type is ignored.
54
+ - **`aragon sessions`** (`list` / `show` / `rm` / `prune` / `path`),
55
+ **`aragon info [--json]`** and **`aragon doctor [--json] [--probe]`**.
56
+
57
+ Documented in full under
58
+ [Use it from another project](./README.md#use-it-from-another-project),
59
+ including an honest Limits section: there is no filesystem sandbox, process
60
+ exit is terminal on Windows whether or not a `result` arrived, an exec session
61
+ resumed in the TUI shows no scrollback, and `sessions prune` shares a directory
62
+ with the TUI's `/save`.
63
+
64
+ ### Changed
65
+
66
+ - **Exit code `3` is new, and the signal row is now stated as `128 + signo`.**
67
+ `3` means "stopped by a budget you set" and is reachable only from
68
+ `aragon exec`. The signal codes are unchanged in behaviour — this CLI has
69
+ always exited `128 + signo`, so `SIGTERM` has always been `143` — but the
70
+ README previously simplified all of them to `130`, which is fine for prose
71
+ about a CLI a human interrupts and not good enough for a wire contract.
72
+ - `interrupt` handling now settles the run before exiting when `aragon exec`
73
+ owns it, so a wrapper reading NDJSON gets a final `result` on Ctrl-C instead of
74
+ having to time out to learn the run ended. `aragon`, `aragon -p` and every
75
+ other subcommand keep the previous bare-exit behaviour byte for byte.
76
+
77
+ ### Unchanged, and asserted rather than claimed
78
+
79
+ - `aragon`, `aragon -p`, every existing flag, every slash command and every byte
80
+ of their output. `aragon exec --output-format text` and `runHeadless` are
81
+ compared for byte equality on **both** stdout and stderr by a regression test,
82
+ and `createBuiltinTools` with no permission policy is proven by object identity
83
+ to return the tools it returned before.
84
+
8
85
  ## Unreleased
9
86
 
10
87
  ### Fixed
11
88
 
89
+ - **The working spinner no longer disappears when an ack takes its row.** Steer a
90
+ run, approve a plan, or press `Ctrl+T` while the agent is working, and the row
91
+ above the composer traded its spinner for the acknowledgement — and for that
92
+ ack's full two and a half seconds, nothing anywhere on screen moved. The status
93
+ bar still read `running` and the tool card still read `running`, so no
94
+ information had left the frame; only the evidence that the process was alive
95
+ had, during exactly the long tool calls where "is it stuck?" is the question you
96
+ are already asking. Consecutive acks extended the window, and steering is
97
+ something the composer invites on every run.
98
+
99
+ That row was made the sole owner of animation in this same release, and
100
+ everything else on screen holds still for as long as the row is believed to be
101
+ showing one. Believed, not checked: an ack won the row outright, so the belief
102
+ and the frame disagreed, and the disagreement was invisible to every test
103
+ because no component was wrong on its own.
104
+
105
+ The ack still owns the row and keeps every word of its text. The spinner now
106
+ rides beside it as a bare glyph — no phrase, no clock — so there is exactly one
107
+ animation at every instant of a run, with an ack up or without one: never none,
108
+ and never the several this release set out to remove. Terminals without Unicode
109
+ and anyone running with `reducedMotion` come out byte for byte unchanged; they
110
+ had no animation to lose, and nothing is prefixed to their acks.
111
+
12
112
  - **`/clear` now takes the todo panel with it.** Finish a plan, type `/clear`,
13
113
  and the transcript emptied while the `7/7 done` rail stayed exactly where it
14
114
  was — on a screen you had just asked to be cleared. It stayed until your next
package/README.md CHANGED
@@ -10,6 +10,36 @@ steer / configure the session — all without leaving the terminal.
10
10
  > and there is **no per-action approval gate by default**. Only run it in
11
11
  > workspaces you trust. An opt-in confirmation mode is available via `--confirm`.
12
12
 
13
+ ## Requirements
14
+
15
+ Node **≥ 18**, any platform.
16
+
17
+ **On Windows with Node < 22.17.0 (or 24.0/24.1), aragon turns the missing
18
+ console flag on itself at startup**, which restores `Shift+Tab` on most
19
+ machines. Where that is blocked — a locked-down PowerShell, an old console
20
+ host — **`Ctrl+P` toggles the mode instead**; it is bound on every platform and
21
+ does not depend on the console mode, the Node version, or anything else.
22
+ Everything else — the agent loop, every tool, skills, teams, `aragon -p` — works
23
+ exactly the same on Node 18 and 20, which is why `engines` stays at `>=18`
24
+ rather than locking those users out.
25
+
26
+ The reason is not the terminal emulator and not a setting: on Windows both
27
+ `Shift+Tab` (`CSI Z`) and mouse reporting exist only while the console input
28
+ handle has `ENABLE_VIRTUAL_TERMINAL_INPUT` set, and Node only sets it from
29
+ v22.17.0 / v24.2.0 onward. On older versions libuv translates console input
30
+ itself, and that translation has no entry for `Shift+Tab` (it arrives as a
31
+ plain `Tab`) and discards mouse records outright. Node 20 is the current
32
+ Windows default LTS, so this is a common configuration rather than an exotic
33
+ one — it is why the same build behaves differently on two machines. Below that
34
+ version aragon sets `ENABLE_VIRTUAL_TERMINAL_INPUT` itself, from a short
35
+ PowerShell helper, right after it puts the console into raw mode — the flag is a
36
+ property of the console, not of Node, so any process attached to it can turn it
37
+ on. When that works, `Shift+Tab` arrives as `CSI Z` again and mouse reporting
38
+ goes back on with it — the wheel should follow by the same route, though only
39
+ the key has been measured. When it does not work, aragon says so at startup and
40
+ turns mouse reporting off rather than asking for reports that can never arrive;
41
+ `Ctrl+P` and `/plan` switch mode, and `Shift+↑` / `Shift+↓` scroll.
42
+
13
43
  ## Install
14
44
 
15
45
  ```bash
@@ -38,11 +68,268 @@ aragon "summarize README.md" # interactive, auto-submitting the prompt
38
68
  echo "list files" | aragon -p # one-shot, prints to stdout then exits
39
69
  ```
40
70
 
71
+ ## Use it from another project
72
+
73
+ `aragon exec` is the machine-facing face of the same binary: a versioned JSON
74
+ event stream on stdout, sessions that survive across process boundaries, tool
75
+ permissions enforced at the tool boundary, and budgets with their own exit code.
76
+ Install the package, spawn it, read the JSON — that is the whole integration
77
+ story. Nothing about the interactive TUI changes, and `aragon -p` keeps working
78
+ exactly as it always has.
79
+
80
+ ```bash
81
+ npm i -g @aragon-agent/cli # or add it as a devDependency and use npx
82
+
83
+ aragon exec --output-format json "summarize src/index.ts" | jq -r .result
84
+ ```
85
+
86
+ ### Discover what you installed
87
+
88
+ ```bash
89
+ aragon info --json # version, schema version, tools, formats, features
90
+ aragon doctor # "is this machine configured to run me?" — exit 0 = yes
91
+ ```
92
+
93
+ `info.features` is a flat list of strings, on purpose: a wrapper that tests
94
+ `features.includes('sessions')` keeps working across versions that add
95
+ capabilities.
96
+
97
+ ### Output formats
98
+
99
+ | Format | Stdout carries | Use it when |
100
+ | --- | --- | --- |
101
+ | `text` (default) | Exactly what `aragon -p` prints | A human reads the output, or you already parse `-p` |
102
+ | `json` | One `result` object, pretty-printed | You want the answer and the totals, nothing else |
103
+ | `stream-json` | One JSON object per line (NDJSON) | You want tool calls, todos and progress as they happen |
104
+
105
+ ```bash
106
+ # text — byte-identical to `aragon -p "hi"`
107
+ aragon exec "hi"
108
+
109
+ # json — one object, ideal for `jq`
110
+ aragon exec --output-format json "hi"
111
+
112
+ # stream-json — NDJSON, one object per line
113
+ aragon exec --output-format stream-json "read package.json and name the deps"
114
+ ```
115
+
116
+ A `stream-json` run looks like this (elided for width):
117
+
118
+ ```jsonc
119
+ {"type":"system","subtype":"init","schemaVersion":1,"sessionId":"6f2…","cli":"0.6.0","cwd":"/w","model":{"provider":"anthropic","id":"claude-…","baseUrl":null},"permissionMode":"auto","tools":["read_file","write_file",…],"resumed":false}
120
+ {"type":"user","sessionId":"6f2…","turn":1,"text":"read package.json…","source":"caller"}
121
+ {"type":"tool_call","sessionId":"6f2…","turn":1,"id":"t1","name":"read_file","input":{"path":"package.json"}}
122
+ {"type":"tool_result","sessionId":"6f2…","turn":1,"id":"t1","name":"read_file","isError":false,"durationMs":4,"output":"{\n \"name\": …"}
123
+ {"type":"assistant","sessionId":"6f2…","turn":1,"text":"The dependencies are …"}
124
+ {"type":"result","schemaVersion":1,"sessionId":"6f2…","isError":false,"stopReason":"end_turn","exitCode":0,"result":"The dependencies are …","turns":1,"durationMs":4120,"usage":{"inputTokens":1204,"outputTokens":88,"totalTokens":1292},"cost":{"amount":0.0049,"currency":"USD","known":true},"model":{"provider":"anthropic","id":"claude-…"},"todos":null,"error":null}
125
+ ```
126
+
127
+ Event types: `system` · `user` · `text_delta` · `thinking` · `assistant` ·
128
+ `tool_call` · `tool_result` · `todo` · `team` · `fast_review` · `retry` ·
129
+ `error` · `result`. `text_delta` and `thinking` are opt-in
130
+ (`--partial-messages`, `--include-thinking`) — a per-token line is thousands of
131
+ lines per turn and is only wanted by a caller rendering a live UI.
132
+
133
+ **Forward compatibility, stated as a contract.** `schemaVersion` appears on
134
+ `system` and on `result`. **Ignore event types you do not know, and ignore
135
+ fields you do not know**: adding either never bumps the version, and renaming or
136
+ removing one does. A consumer that holds up its end of that keeps working across
137
+ upgrades; one that does not will break on a release that adds an event.
138
+
139
+ One practical note: a `tool_result.output` line can approach 100 KB, so read
140
+ stdout with a line reader that has no small buffer cap.
141
+
142
+ ### Tool permissions
143
+
144
+ `--confirm` needs a human. For a program embedding an agent there are two
145
+ orthogonal controls instead, and both are enforced by the wiring — a denied tool
146
+ is **not registered at all**, so the model never sees it and never plans around
147
+ it.
148
+
149
+ | Flag | Meaning |
150
+ | --- | --- |
151
+ | `--permission-mode auto` | Every tool (the default) |
152
+ | `--permission-mode plan` | Every tool registered; the five that write refuse at call time. Exactly `aragon -p --plan` |
153
+ | `--permission-mode strict` | Nothing, until `--allow-tool` says otherwise |
154
+ | `--allow-tool <names>` | **Adds** to the baseline. Repeatable; comma lists accepted |
155
+ | `--deny-tool <names>` | **Removes**. Wins over `--allow-tool` |
156
+
157
+ Each of these reads as one sentence:
158
+
159
+ ```bash
160
+ # a research agent that cannot touch anything
161
+ aragon exec --permission-mode strict --allow-tool read_file,grep,glob,list_dir \
162
+ --output-format json "which files define the retry policy?"
163
+
164
+ # do anything except shell out
165
+ aragon exec --deny-tool bash --output-format json "fix the failing test"
166
+
167
+ # read-only research and a written plan, no edits
168
+ aragon exec --permission-mode plan --output-format json "how would you add OAuth?"
169
+ ```
170
+
171
+ The policy reaches **subagents**: a `task` dispatch cannot hand a child a tool
172
+ the parent was denied. An unknown tool name is a usage error (exit 2) rather
173
+ than a silent no-op.
174
+
175
+ ### Sessions across invocations
176
+
177
+ A wrapper that respawns per turn — a CI step, an HTTP handler, a queue worker —
178
+ needs the conversation to outlive the process.
179
+
180
+ ```bash
181
+ aragon exec --session-id ci-42 --output-format json "start the migration plan"
182
+ aragon exec --session-id ci-42 --output-format json "now do step 1"
183
+ ```
184
+
185
+ | Flag | If the session exists | If it does not |
186
+ | --- | --- | --- |
187
+ | `--session-id <id>` | Resume it | **Create** it (the wrapper-friendly upsert) |
188
+ | `--resume <id\|path>` | Resume it | Exit 2, `session_not_found` |
189
+ | `-c, --continue` | Resume the newest session for this directory | Exit 2, `no_session_for_cwd` |
190
+ | `--no-save-session` | — | Run statelessly; write nothing |
191
+
192
+ Sessions live in `~/.aragon-agent/sessions/`. An id must match
193
+ `[A-Za-z0-9][A-Za-z0-9._-]{0,63}` — it becomes a filename, and the caller
194
+ supplying it is frequently the least trusted input in the pipeline.
195
+
196
+ ```bash
197
+ aragon sessions list [--json] [-n 20] [--all]
198
+ aragon sessions show <id> [--json]
199
+ aragon sessions rm <id> --yes
200
+ aragon sessions prune [--older-than 7] [--dry-run] [--all] --yes
201
+ aragon sessions path
202
+ ```
203
+
204
+ Two same-id runs cannot interleave: the second exits 2 with `session_busy`
205
+ rather than blocking, because a wrapper that wants a queue can build one and a
206
+ CLI that blocks on a lock it cannot show you is a hang.
207
+
208
+ ### Budgets
209
+
210
+ ```bash
211
+ aragon exec --max-turns 20 --max-duration 600000 --output-format json "…"
212
+ ```
213
+
214
+ `--max-turns <n>` bounds assistant turns; `--max-duration <ms>` bounds the whole
215
+ run's wall clock. Both abort through the same path `Esc` uses, so subagents are
216
+ torn down and the engine settles normally. **Neither is an error** — `isError`
217
+ stays `false` — but both exit **3**, so a shell script can tell "ran out of
218
+ budget" from "finished" and from "broke" without parsing JSON.
219
+
220
+ Two neighbours they do not replace: `--idle-timeout` bounds a *silent stretch*
221
+ inside the run and still applies (a `--max-duration` longer than the idle
222
+ watchdog will usually be pre-empted by it, which surfaces as
223
+ `stopReason: "error"`); `--tool-timeout` bounds a *single tool call*.
224
+
225
+ A run that finishes on its own in exactly `--max-turns` turns exits `0` with
226
+ `stopReason: "end_turn"`. The budget fires only when a further turn would start.
227
+
228
+ ### Exit codes
229
+
230
+ | Code | Meaning |
231
+ | --- | --- |
232
+ | `0` | Success |
233
+ | `1` | Agent / runtime error (the run started and failed) |
234
+ | `2` | Configuration or usage error (the run never started; stdout is empty) |
235
+ | `3` | Stopped by a caller-set budget (`--max-turns`, `--max-duration`) |
236
+ | `130` / `143` / `129` | Interrupted by `SIGINT` / `SIGTERM` / `SIGHUP` (`128 + signo`) |
237
+
238
+ ### Spawning it from Node
239
+
240
+ ```js
241
+ import { spawn } from 'node:child_process';
242
+ import readline from 'node:readline';
243
+
244
+ function runAragon(prompt, args = []) {
245
+ return new Promise((resolve, reject) => {
246
+ const child = spawn(
247
+ 'aragon',
248
+ ['exec', '--output-format', 'stream-json', ...args, prompt],
249
+ { stdio: ['ignore', 'pipe', 'inherit'] },
250
+ );
251
+
252
+ let result = null;
253
+ const lines = readline.createInterface({ input: child.stdout });
254
+ lines.on('line', (line) => {
255
+ let event;
256
+ try {
257
+ event = JSON.parse(line);
258
+ } catch {
259
+ return; // Never fatal: forward compatibility runs both ways.
260
+ }
261
+ if (event.type === 'result') result = event;
262
+ // Ignore every type you do not know. That is the contract.
263
+ });
264
+
265
+ // RESOLVE ON EXIT AS WELL AS ON `result`. Process exit is terminal whether
266
+ // or not a `result` arrived — see Limits below — and a consumer that waits
267
+ // only for `result` hangs when the run is killed.
268
+ child.on('error', reject);
269
+ child.on('close', (code) => resolve({ code, result }));
270
+ });
271
+ }
272
+ ```
273
+
274
+ ### In CI
275
+
276
+ ```bash
277
+ aragon exec \
278
+ --permission-mode plan \
279
+ --max-turns 20 \
280
+ --max-duration 900000 \
281
+ --output-format json \
282
+ --session-id "ci-${GITHUB_RUN_ID}" \
283
+ "Review the diff on this branch and list the risks." \
284
+ | jq -r .result
285
+ ```
286
+
287
+ `aragon doctor --json` exits 1 with a named failing check when the machine is
288
+ not configured, which makes a good first step in the job.
289
+
290
+ Feeding several turns to one process:
291
+
292
+ ```bash
293
+ printf '%s\n' \
294
+ '{"type":"user","text":"list the files you would change"}' \
295
+ '{"type":"user","text":"now change the first one"}' \
296
+ '{"type":"end"}' \
297
+ | aragon exec --input-format stream-json --output-format stream-json
298
+ ```
299
+
300
+ ### Limits
301
+
302
+ Four things this does **not** do, stated plainly because the audience for this
303
+ section is the one that would be misled by their omission.
304
+
305
+ 1. **There is no filesystem sandbox.** Tool paths are not confined to the
306
+ working directory: an absolute path resolves to itself. Tool permission —
307
+ not a directory allowlist — is the boundary that is real, and a run with
308
+ `bash` enabled can reach anything the invoking user can. There is deliberately
309
+ no `--add-dir`, because it would be a promise the wiring does not keep.
310
+ 2. **Process exit is terminal, `result` or no `result`.** On POSIX, `SIGINT`,
311
+ `SIGTERM` and `SIGHUP` all produce a final `result` before the process ends.
312
+ **On Windows only Ctrl-C does** — Node does not deliver the other two to
313
+ listeners, so `taskkill` ends the process outright. Resolve on process exit as
314
+ well as on `result`, or a killed run hangs your parent process.
315
+ 3. **A session created by `aragon exec` and resumed with `aragon --resume`
316
+ carries the model's full memory and shows no scrollback** for the turns that
317
+ ran headlessly. The reverse direction is fully faithful: a TUI `/save` resumed
318
+ by `aragon exec --resume <id>` keeps everything.
319
+ 4. **`aragon sessions prune` shares a directory with the TUI's `/save`**, so it
320
+ only ever touches sessions `exec` created — files that carry an exec `meta`
321
+ block — unless you pass `--all`. `sessions show` and `sessions rm` accept any
322
+ id, because you named it.
323
+
41
324
  ## Usage
42
325
 
43
326
  ```
44
327
  aragon [prompt] Start the interactive TUI (or one-shot with -p / piped stdin)
45
328
  aragon -p, --print [prompt] Headless: stream the answer to stdout, then exit
329
+ aragon exec [prompt] Machine-facing run: JSON events, sessions, permissions, budgets
330
+ aragon sessions <sub> [id] Manage exec sessions: list | show | rm | prune | path
331
+ aragon info [--json] Print what this installation supports
332
+ aragon doctor [--json] [--probe] Check this machine is configured to run aragon
46
333
  aragon config Open the settings screen
47
334
  aragon config set <key> <value> Non-interactive config write (e.g. model, provider)
48
335
  aragon config path Print the config file path
@@ -51,6 +338,11 @@ aragon --version | -v Print the version
51
338
  aragon --help | -h Print help
52
339
  ```
53
340
 
341
+ `aragon exec` is documented in full under
342
+ [Use it from another project](#use-it-from-another-project). Every global flag
343
+ below works there too, in either position — `aragon --model m exec "…"` and
344
+ `aragon exec --model m "…"` parse identically.
345
+
54
346
  ### Global flags
55
347
 
56
348
  | Flag | Meaning |
@@ -95,7 +387,8 @@ aragon --help | -h Print help
95
387
  | `--quiet` | (print mode) suppress tool/usage lines on stderr |
96
388
 
97
389
  Exit codes: `0` success · `1` agent/runtime error · `2` config/usage error ·
98
- `130` interrupted.
390
+ `3` stopped by a caller-set budget (`aragon exec` only) · `128 + signo` when a
391
+ signal ends the run (`130` SIGINT, `143` SIGTERM, `129` SIGHUP).
99
392
 
100
393
  ## Slash commands (interactive)
101
394
 
@@ -138,7 +431,8 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
138
431
  | --- | --- |
139
432
  | `Enter` | Submit (idle) / queue a steering message (running) |
140
433
  | `Alt+Enter` / `Shift+Enter` | Insert a newline |
141
- | `Shift+Tab` | Toggle **plan mode** (`BUILD` <-> `PLAN`). Equivalent: `/plan` |
434
+ | `Shift+Tab` | Toggle **plan mode** (`BUILD` <-> `PLAN`). Equivalent: `/plan`. On Windows this needs a console flag Node < 22.17 does not set; aragon sets it at startup — see [Requirements](#requirements) |
435
+ | `Ctrl+P` | The same toggle, over a channel no Windows console can swallow. Use it if `Shift+Tab` does nothing |
142
436
  | `Esc` | Abort the run / close an overlay / close a popup |
143
437
  | `Ctrl+C` ×2 | Exit (first press warns) |
144
438
  | `Ctrl+L` | Redraw the frame (clear the screen in inline mode) |
@@ -146,7 +440,7 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
146
440
  | `Ctrl+O` | Expand / collapse the most recent tool card — including its diff |
147
441
  | `PgUp` / `PgDn` | Scroll the transcript a page (full-screen mode) |
148
442
  | `Shift+↑` / `Shift+↓` | Scroll the transcript a line (full-screen mode) |
149
- | Mouse wheel | Scroll the transcript (or the open overlay) |
443
+ | Mouse wheel | Scroll the transcript (or the open overlay). On Windows needs Node ≥ 22.17 — see [Requirements](#requirements) |
150
444
  | `Shift`+wheel | Scroll the transcript a page |
151
445
  | `Home` / `End`, `Ctrl+A` / `Ctrl+E` | Cursor to line start / end |
152
446
  | `Alt/Ctrl+←` / `→` | Word-wise cursor jump |
@@ -177,6 +471,9 @@ before you meet them:
177
471
  something this CLI can opt out of while still reading wheel events.
178
472
  - **`tmux` with `mouse on` keeps the wheel for itself**, so the app never sees
179
473
  it. `set -g mouse off` hands it back.
474
+ - **A Windows console on Node below 22.17.0 cannot report the wheel at all**,
475
+ so aragon does not ask it to. See [Requirements](#requirements); `PgUp` /
476
+ `PgDn` and `Shift+↑` / `Shift+↓` scroll from the keyboard meanwhile.
180
477
 
181
478
  Turn the whole thing off with `--no-mouse`, `ARAGON_MOUSE=0`, or
182
479
  `aragon config set mouse false`. With mouse support off the wheel simply does
@@ -460,9 +757,28 @@ it is an informed act rather than a stray keypress.
460
757
 
461
758
  ### If `Shift+Tab` does nothing
462
759
 
463
- Some multiplexers and remote-desktop stacks never send `CSI Z`. `/plan`,
464
- `/plan on`, `/plan off` and `/plan status` are first-class equivalents, and
465
- `--plan` / `ARAGON_PLAN=1` / `planModeDefault` cover the startup case.
760
+ **Press `Ctrl+P`.** It is the same toggle and it does not depend on any of the
761
+ machinery below.
762
+
763
+ To find out why, run this in the same terminal, press `Shift+Tab` once, then
764
+ `Ctrl+C`:
765
+
766
+ ```powershell
767
+ node -e "process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',b=>{console.log(b.toString('hex'));if(b[0]===3)process.exit(0)})"
768
+ ```
769
+
770
+ | It prints | What it means |
771
+ | --- | --- |
772
+ | `09` | The console is swallowing the modifier — the key is arriving as an ordinary `Tab`. Check `node -v`: below 22.17.0 (or on 24.0/24.1) this is expected, and aragon's own attempt to fix it was blocked. See [Requirements](#requirements) |
773
+ | `1b5b5a` | The key reaches the process fine, so something inside aragon is eating it — most often an overlay is open, or the run is still going and the switch is pending (the status bar shows `BUILD → PLAN`) |
774
+ | nothing | Your terminal, multiplexer or remote-desktop stack never sent it; some never send `CSI Z` at all |
775
+
776
+ On the first row the wheel is dead for the same reason, and with the `/` palette
777
+ or an `@` popup open the stray `Tab` completes that entry over your draft.
778
+
779
+ `/plan`, `/plan on`, `/plan off` and `/plan status` are first-class equivalents
780
+ in every case, and `--plan` / `ARAGON_PLAN=1` / `planModeDefault` cover the
781
+ startup case. `aragon doctor` reports this machine's answer as `vt-input`.
466
782
 
467
783
  ### Headless
468
784
 
@@ -28,6 +28,45 @@ export declare const MODE_LABEL: Record<AgentMode, string>;
28
28
  * and nothing else. Two ship today (§1 non-goals).
29
29
  */
30
30
  export declare function nextMode(current: AgentMode): AgentMode;
31
+ /**
32
+ * The keys that toggle the mode, as the user sees them written
33
+ * (shift-tab-mode-toggle-still-dead-on-windows, C2).
34
+ *
35
+ * `fallback` exists because `primary` CANNOT be made to work everywhere. On a
36
+ * Windows console without `ENABLE_VIRTUAL_TERMINAL_INPUT`, libuv takes the
37
+ * character branch for `VK_TAB` and drops the modifier wholesale, so `Shift+Tab`
38
+ * and `Ctrl+Tab` both arrive as a bare `0x09` - byte-identical to `Tab`. C1
39
+ * turns that bit back on where it can, but it has three known ways to fail that
40
+ * are outside this package (an old conhost refusing the bit, a locked-down
41
+ * PowerShell, a foreign process resetting the mode), so a rung that depends on
42
+ * NOTHING has to exist underneath it.
43
+ *
44
+ * `ctrl+p` IS MEASURED, not guessed. On the lossy path it arrives as `0x10`,
45
+ * because Windows generates a real control character for `Ctrl+<letter>` and
46
+ * libuv's character branch forwards it untouched; `Alt+M` produces no bytes at
47
+ * all and `Ctrl+Tab` collapses exactly like `Shift+Tab` (analysis section 3.6).
48
+ * Any idle `Ctrl+<letter>` in the `0x01-0x1A` range would work; this one is free
49
+ * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/l/t/o,
50
+ * and `PromptInput` returns early on every `key.ctrl` - so binding it takes no
51
+ * behaviour away from anyone. `Ctrl+B` survives the same path but is tmux's
52
+ * default prefix, and this binding is unconditional and cross-platform, so it
53
+ * would be silently dead for a whole class of macOS and Linux users.
54
+ *
55
+ * BOUND UNCONDITIONALLY, on every platform. A binding that only exists where it
56
+ * is needed cannot be documented in one place, cannot be taught by muscle
57
+ * memory, and is missing on precisely the machine whose capability probe was
58
+ * wrong.
59
+ *
60
+ * THESE ARE THE LABELS, NOT THE BINDING. Editing a value here changes only what
61
+ * the user is TOLD to press - the hint row, the startup notice, `doctor`. The
62
+ * key that is actually listened for is `input === 'p'` in `App`'s `useInput`,
63
+ * and the help overlay and README spell both keys out in their own title case.
64
+ * Change one without the others and the product advertises a key it ignores.
65
+ */
66
+ export declare const MODE_TOGGLE_KEYS: {
67
+ readonly primary: "shift+tab";
68
+ readonly fallback: "ctrl+p";
69
+ };
31
70
  /** The two tools that only exist because a human is attached. */
32
71
  export declare const PLAN_TOOL_NAMES: readonly ["ask_user", "submit_plan"];
33
72
  /**
@@ -30,6 +30,42 @@ export function nextMode(current) {
30
30
  const index = AGENT_MODES.indexOf(current);
31
31
  return AGENT_MODES[(index + 1) % AGENT_MODES.length];
32
32
  }
33
+ /**
34
+ * The keys that toggle the mode, as the user sees them written
35
+ * (shift-tab-mode-toggle-still-dead-on-windows, C2).
36
+ *
37
+ * `fallback` exists because `primary` CANNOT be made to work everywhere. On a
38
+ * Windows console without `ENABLE_VIRTUAL_TERMINAL_INPUT`, libuv takes the
39
+ * character branch for `VK_TAB` and drops the modifier wholesale, so `Shift+Tab`
40
+ * and `Ctrl+Tab` both arrive as a bare `0x09` - byte-identical to `Tab`. C1
41
+ * turns that bit back on where it can, but it has three known ways to fail that
42
+ * are outside this package (an old conhost refusing the bit, a locked-down
43
+ * PowerShell, a foreign process resetting the mode), so a rung that depends on
44
+ * NOTHING has to exist underneath it.
45
+ *
46
+ * `ctrl+p` IS MEASURED, not guessed. On the lossy path it arrives as `0x10`,
47
+ * because Windows generates a real control character for `Ctrl+<letter>` and
48
+ * libuv's character branch forwards it untouched; `Alt+M` produces no bytes at
49
+ * all and `Ctrl+Tab` collapses exactly like `Shift+Tab` (analysis section 3.6).
50
+ * Any idle `Ctrl+<letter>` in the `0x01-0x1A` range would work; this one is free
51
+ * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/l/t/o,
52
+ * and `PromptInput` returns early on every `key.ctrl` - so binding it takes no
53
+ * behaviour away from anyone. `Ctrl+B` survives the same path but is tmux's
54
+ * default prefix, and this binding is unconditional and cross-platform, so it
55
+ * would be silently dead for a whole class of macOS and Linux users.
56
+ *
57
+ * BOUND UNCONDITIONALLY, on every platform. A binding that only exists where it
58
+ * is needed cannot be documented in one place, cannot be taught by muscle
59
+ * memory, and is missing on precisely the machine whose capability probe was
60
+ * wrong.
61
+ *
62
+ * THESE ARE THE LABELS, NOT THE BINDING. Editing a value here changes only what
63
+ * the user is TOLD to press - the hint row, the startup notice, `doctor`. The
64
+ * key that is actually listened for is `input === 'p'` in `App`'s `useInput`,
65
+ * and the help overlay and README spell both keys out in their own title case.
66
+ * Change one without the others and the product advertises a key it ignores.
67
+ */
68
+ export const MODE_TOGGLE_KEYS = { primary: 'shift+tab', fallback: 'ctrl+p' };
33
69
  /** The two tools that only exist because a human is attached. */
34
70
  export const PLAN_TOOL_NAMES = ['ask_user', 'submit_plan'];
35
71
  /**
@@ -11,6 +11,7 @@
11
11
  import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
12
12
  import type { CliConfig, FastConfig, RetryConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
13
13
  import { type ConfirmRequest } from '../tools/index.js';
14
+ import type { ToolPermission } from '../exec/permission.js';
14
15
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
15
16
  import type { FilePatch } from '../tools/patch.js';
16
17
  import type { TeamEvent, TeamSnapshot } from '../team/types.js';
@@ -70,6 +71,28 @@ export interface ControllerDeps {
70
71
  * mid-run, and the sentence is guidance rather than a contract.
71
72
  */
72
73
  todoPanelCapable?: boolean;
74
+ /**
75
+ * The `aragon exec` tool policy (cli-integration-surface §4.2).
76
+ *
77
+ * FORWARDED TO **TWO** PLACES, AND BOTH ARE REQUIRED (R-4 / R-5). It goes into
78
+ * `createBuiltinTools` for the lead's own array, and into `TeamRuntimeDeps` so
79
+ * children inherit it. Without the second, `--deny-tool bash` is bypassed by
80
+ * one `task` call and NOTHING ANYWHERE REPORTS IT — a security control that
81
+ * silently does not hold. Plan mode's "reaches one level down" guarantee is
82
+ * the precedent; AC-12 is the test.
83
+ *
84
+ * Absent on every existing path (the TUI, `-p`, every subcommand), which is
85
+ * what keeps the tool array identical there — see the option's own comment in
86
+ * `tools/index.ts`.
87
+ */
88
+ permission?: ToolPermission;
89
+ /**
90
+ * `--append-system-prompt` text, appended verbatim to the composed prompt
91
+ * (§4.1). Threaded through `composeSystemPrompt` so it survives every rebuild
92
+ * — a `/cwd` change, a skills rescan, a mode flip — rather than being spliced
93
+ * once at construction and lost at the first `rebuildSystemPrompt()`.
94
+ */
95
+ appendSystemPrompt?: string;
73
96
  }
74
97
  /** What `setAgentMode` actually adopted — see `AgentController.setAgentMode`. */
75
98
  export interface AgentModeState {
@@ -98,6 +121,15 @@ export declare class AgentController {
98
121
  private onSkillsChanged?;
99
122
  /** Mutable session cwd — tools resolve relative paths against this. */
100
123
  private cwd;
124
+ /**
125
+ * `--append-system-prompt` text (cli-integration-surface §4.1).
126
+ *
127
+ * A FIELD, NOT A LOCAL, because `composeSystemPrompt()` is called again on
128
+ * every rebuild — a `/cwd` change, a skills rescan, a plan-mode flip — and a
129
+ * value spliced once at construction would silently vanish at the first one.
130
+ * Empty on every path but `aragon exec`.
131
+ */
132
+ private readonly appendSystemPrompt;
101
133
  private effectiveMode;
102
134
  private pendingMode;
103
135
  private askRounds;