@aragon-agent/cli 0.6.3 → 0.6.4

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 (94) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +95 -2
  3. package/dist/agent/controller.d.ts +119 -0
  4. package/dist/agent/controller.js +275 -1
  5. package/dist/agent/reducer.d.ts +69 -0
  6. package/dist/agent/reducer.js +100 -0
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/cli.js +84 -22
  10. package/dist/commands/builtins.js +92 -1
  11. package/dist/compaction/child.js +5 -0
  12. package/dist/compaction/command.js +14 -1
  13. package/dist/compaction/compactor.d.ts +34 -0
  14. package/dist/compaction/compactor.js +158 -37
  15. package/dist/compaction/types.d.ts +36 -0
  16. package/dist/compaction/wiring.d.ts +9 -0
  17. package/dist/compaction/wiring.js +22 -1
  18. package/dist/config/env.d.ts +1 -1
  19. package/dist/config/env.js +48 -1
  20. package/dist/config/load.d.ts +11 -0
  21. package/dist/config/load.js +39 -1
  22. package/dist/config/schema.d.ts +85 -0
  23. package/dist/config/schema.js +33 -0
  24. package/dist/config/store.js +21 -1
  25. package/dist/input/limits.d.ts +72 -0
  26. package/dist/input/limits.js +63 -0
  27. package/dist/input/paste-parse.d.ts +71 -0
  28. package/dist/input/paste-parse.js +95 -0
  29. package/dist/input/stdin-filter.d.ts +81 -0
  30. package/dist/input/stdin-filter.js +379 -0
  31. package/dist/logging/install.d.ts +2 -0
  32. package/dist/logging/install.js +53 -1
  33. package/dist/proc/classify.d.ts +52 -0
  34. package/dist/proc/classify.js +143 -0
  35. package/dist/proc/kill-tree.d.ts +42 -0
  36. package/dist/proc/kill-tree.js +119 -0
  37. package/dist/proc/limits.d.ts +84 -0
  38. package/dist/proc/limits.js +84 -0
  39. package/dist/proc/log-ring.d.ts +68 -0
  40. package/dist/proc/log-ring.js +102 -0
  41. package/dist/proc/prompt.d.ts +24 -0
  42. package/dist/proc/prompt.js +49 -0
  43. package/dist/proc/readiness.d.ts +102 -0
  44. package/dist/proc/readiness.js +219 -0
  45. package/dist/proc/supervisor.d.ts +103 -0
  46. package/dist/proc/supervisor.js +465 -0
  47. package/dist/proc/types.d.ts +175 -0
  48. package/dist/proc/types.js +15 -0
  49. package/dist/session/persist.js +36 -1
  50. package/dist/tools/bash-tool.d.ts +51 -2
  51. package/dist/tools/bash-tool.js +321 -51
  52. package/dist/tools/fs-tools.d.ts +17 -0
  53. package/dist/tools/index.d.ts +55 -2
  54. package/dist/tools/index.js +39 -1
  55. package/dist/tools/proc-tools.d.ts +25 -0
  56. package/dist/tools/proc-tools.js +102 -0
  57. package/dist/ui/App.d.ts +15 -2
  58. package/dist/ui/App.js +256 -7
  59. package/dist/ui/Composer.d.ts +54 -1
  60. package/dist/ui/Composer.js +50 -6
  61. package/dist/ui/PromptInput.d.ts +49 -3
  62. package/dist/ui/PromptInput.js +184 -52
  63. package/dist/ui/StatusBar.d.ts +19 -0
  64. package/dist/ui/StatusBar.js +5 -2
  65. package/dist/ui/Transcript.js +29 -1
  66. package/dist/ui/composer-limits.d.ts +31 -0
  67. package/dist/ui/composer-limits.js +50 -0
  68. package/dist/ui/composer-rows.d.ts +73 -0
  69. package/dist/ui/composer-rows.js +201 -0
  70. package/dist/ui/editor-reducer.d.ts +37 -0
  71. package/dist/ui/editor-reducer.js +88 -12
  72. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  73. package/dist/ui/entries/ServiceCard.js +116 -0
  74. package/dist/ui/entries/UserEntry.d.ts +17 -3
  75. package/dist/ui/entries/UserEntry.js +34 -5
  76. package/dist/ui/layout/budget.d.ts +34 -6
  77. package/dist/ui/layout/budget.js +32 -7
  78. package/dist/ui/layout/virtual-window.js +51 -1
  79. package/dist/ui/overlays/HelpOverlay.js +3 -0
  80. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  81. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  82. package/dist/ui/overlays/SettingsScreen.js +14 -1
  83. package/dist/ui/paste-frames.d.ts +87 -0
  84. package/dist/ui/paste-frames.js +112 -0
  85. package/dist/ui/paste-tokens.d.ts +90 -0
  86. package/dist/ui/paste-tokens.js +147 -0
  87. package/dist/ui/screen.d.ts +16 -0
  88. package/dist/ui/screen.js +45 -2
  89. package/dist/ui/selection/selection-controller.d.ts +1 -1
  90. package/dist/ui/transcript-text.js +49 -0
  91. package/dist/ui/use-wheel-routing.d.ts +1 -1
  92. package/package.json +2 -2
  93. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  94. package/dist/input/stdin-mouse-filter.js +0 -153
package/CHANGELOG.md CHANGED
@@ -9,6 +9,111 @@ were written.
9
9
 
10
10
  ### Added
11
11
 
12
+ - **The composer understands pasting.** Until now it did not have the concept at
13
+ all: every byte the terminal delivered on stdin was a keystroke, and a
14
+ clipboard block is thousands of bytes arriving in an unpredictable number of
15
+ chunks. Three separate failures fell out of that one omission, and on Windows
16
+ you hit all three at once.
17
+
18
+ A carriage return that landed alone in a chunk **sent the half-finished
19
+ message** — a fifteen-line paste could arrive as a dozen separate messages,
20
+ interleaved with the agent's replies. A carriage return in the middle of a
21
+ chunk was inserted into the draft as a literal `\r`, which the terminal obeyed
22
+ by **overwriting the row it was drawn on**. And a paste that survived both grew
23
+ the composer to the height of the pasted text, pushing the transcript out of a
24
+ frame whose row accounting still believed the composer was three rows tall.
25
+
26
+ Pasting now works the way it reads:
27
+
28
+ - **A paste never submits.** Line breaks become buffer newlines, exactly one
29
+ per source line break, `CRLF` and lone `CR` alike.
30
+ - **A short paste appears verbatim.** At or below 6 lines and 400 characters it
31
+ is inserted as-is, indentation intact, and renders exactly like typed text.
32
+ - **A large paste collapses to a placeholder** — `[Pasted text #1 +218 lines]`
33
+ — which occupies one line, carries its own size in its label, deletes as a
34
+ single unit (one `Backspace`, and `Ctrl+W` / `Ctrl+U` / `Ctrl+K` cannot leave
35
+ half of one behind), and expands back to the full text the moment you send.
36
+ The model receives every byte; the composer never has to draw them.
37
+ - **No pasted byte is executed as a terminal control.** `\r`, `\x1b`, NUL, DEL
38
+ and the C1 range are stripped from the payload — never from your keystrokes,
39
+ so `Ctrl+A` and Escape are untouched.
40
+ - **The composer has a ceiling.** A draft of any length occupies at most 3 / 6 /
41
+ 10 rows depending on terminal height, scrolls internally to keep the caret
42
+ visible, shows how many rows are hidden above and below, and reports its true
43
+ height to the layout budget. Four hundred presses of `Shift+Enter` were
44
+ already able to destroy the frame; they no longer are.
45
+ - **A submitted message cannot make the transcript unresponsive.** A user entry
46
+ renders at most 40 rows plus a `... +N more lines` tail. The full text is kept
47
+ and the exit transcript is uncapped.
48
+
49
+ On terminals that support it, detection is **exact**: aragon asks for bracketed
50
+ paste (`DECSET 2004`) in full-screen mode and reads the markers the terminal
51
+ wraps your paste in. Where that is unavailable — inline mode, an old Windows
52
+ console — a heuristic on chunk shape catches the same cases, and it needs no
53
+ cooperation from the terminal at all. Both feed one code path.
54
+
55
+ Turn it off with `--no-paste`, `ARAGON_PASTE=0`, or
56
+ `aragon config set paste false`; the input path is then byte-for-byte what
57
+ 0.6.2 did.
58
+
59
+ ### Fixed
60
+
61
+ - **A pasted API key is stored correctly again.** Ink broadcasts each stdin chunk
62
+ to every mounted input handler, so the settings screen, the question overlay
63
+ and the plan-review feedback field all see what the composer sees. Without
64
+ this, a ~100-character key pasted into Settings would have been stored wrapped
65
+ in invisible framing bytes, masked on screen by the password dots, and would
66
+ have failed to authenticate on every request afterwards with nothing anywhere
67
+ explaining why.
68
+
69
+ - **`--no-mouse` still writes no mouse sequence.** Paste support installs a
70
+ stdin filter, and `paste` defaults to on, so the filter now exists in sessions
71
+ that disabled the mouse. Whether the mouse is on is a separate question from
72
+ whether the stream is wrapped, and the four places that used to conflate them
73
+ now ask the right one: `aragon --no-mouse` writes no `?1000h`, no `?1006h` and
74
+ no `?1002h`, leaves your terminal's own click-drag selection alone, and reports
75
+ `/mouse` as off — while pasting still works.
76
+
77
+ ### Changed
78
+
79
+ - **Auto-compaction no longer reports compactions that did not happen.** A long
80
+ unattended run used to print a red `context not compacted #2: nothing_to_drop`
81
+ card into the transcript. The card was accurate; nobody had asked for it.
82
+ Auto-compaction evaluates itself at every turn boundary, and a checkpoint that
83
+ looks at the history and finds nothing worth dropping has changed nothing,
84
+ called nothing and spent nothing — a smoke alarm announcing "no fire" twice an
85
+ hour, in error colours, with a machine reason attached.
86
+
87
+ A compaction now announces itself **if and only if it commits to doing work**.
88
+ That is a position in the control flow, not a list of reason strings: everything
89
+ decided before the first spend is silent, and everything decided after it —
90
+ including every failure, the truncation fallback, an Esc, and a host fault — is
91
+ as loud as it ever was. A declined `pressure` checkpoint is now
92
+ indistinguishable from a turn on which the threshold was never crossed.
93
+
94
+ **`/compact` and overflow recovery are unchanged.** A compaction you asked for
95
+ is owed an answer, and a reactive compaction running after the provider has
96
+ already refused your request is the one moment the diagnosis matters most. Both
97
+ still report on every outcome.
98
+
99
+ **Quiet, not unrecorded.** Every attempt is still written to the diagnostic log
100
+ (its `compaction` line gains an `announced` field, so `announced: false` is one
101
+ grep), still charged to the anti-loop guards, and still counted by
102
+ `/compact status` — which now reads
103
+ `This session: 1 compaction (2 checkpoints declined), 94.2k tokens reclaimed`
104
+ when checkpoints declined, and is byte-identical to before when none did. If
105
+ declines keep happening, the self-disable notice still says so out loud, once,
106
+ naming the cause and the remedies. Card numbers are also contiguous again: `#N`
107
+ counts compactions that ran, not checkpoints that fired.
108
+
109
+ **Behaviour change for wrapper scripts.** A declined `pressure` compaction no
110
+ longer emits a `{ type: 'compaction', subtype: 'start' | 'end' }` pair on the
111
+ `--output-format json` stream, and no longer prints `[compaction] skipped:` on
112
+ `-p` stderr. The pairs stay balanced, so only consumers counting *attempts* are
113
+ affected; the diagnostic log reports those more accurately.
114
+
115
+ ### Added
116
+
12
117
  - **Context compaction, hardened — the trigger now counts the whole history, the
13
118
  recent turns can no longer wedge a run, sub-agents get the same protection, and
14
119
  a compaction is no longer irreversible.**
package/README.md CHANGED
@@ -413,6 +413,7 @@ signal ends the run (`130` SIGINT, `143` SIGTERM, `129` SIGHUP).
413
413
  | `/retry [show\|on\|off\|max <n>]` | API retry: the effective ladder, the on/off switch, the retry count. Takes effect **in the running session**, children included |
414
414
  | `/fast [on\|off\|model <id>\|provider <id>\|same\|review <n\|off>\|delegate on\|off]` | Fast model tier: status and this session's totals, the on/off switch, which model, the review cadence |
415
415
  | `/compact [status\|on\|off\|threshold <n>\|keep <n>\|history\|show <n>\|<instructions>]` | Context compaction: occupancy and this session's totals, the on/off switch, when it fires, how much it keeps. With free text, compacts now and tells the summarizer what to pay attention to |
416
+ | `/bg [list\|logs <id> [n]\|stop <id\|all>\|status]` | Background services: what is running, its log tail, and how to stop it. `list` is the default |
416
417
  | `/update [status\|now\|skip\|off]` | Auto-update: what is running, what is available, where it came from, when it looks again. `now` forces a check past the throttle; `skip` declines the offered version until a newer one appears |
417
418
  | `/theme <auto\|warm\|cool\|light>` | Switch the color theme live (persisted) |
418
419
  | `/expand [n]` | Expand / collapse the n-th-from-last (default last) tool card |
@@ -437,10 +438,12 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
437
438
  | `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) |
438
439
  | `Ctrl+P` | The same toggle, over a channel no Windows console can swallow. Use it if `Shift+Tab` does nothing |
439
440
  | `Esc` | Abort the run / close an overlay / close a popup |
440
- | `Ctrl+C` ×2 | Exit (first press warns) |
441
+ | `Esc` ×2 | **Force-stop** a run that did not abort: kills the shell command it is blocked on and returns the view to `idle`. The second press only does anything while the run is *still* running, so in a healthy run it is a no-op |
442
+ | `Ctrl+C` | Stop the background **services** the agent started, when there are any. It does not arm exit |
443
+ | `Ctrl+C` ×2 | Exit (first press warns), once no services are running |
441
444
  | `Ctrl+L` | Redraw the frame (clear the screen in inline mode) |
442
445
  | `Ctrl+T` | Show / hide thinking (**off by default**). In inline mode it applies to the live entry and everything drawn after it — settled entries are already in your terminal's scrollback and cannot be repainted |
443
- | `Ctrl+O` | Expand / collapse the most recent tool card — including its diff |
446
+ | `Ctrl+O` | Expand / collapse the most recent tool, team, compaction or **service** card — including a diff or a service's log tail |
444
447
  | `PgUp` / `PgDn` | Scroll the transcript a page (full-screen mode) |
445
448
  | `Shift+↑` / `Shift+↓` | Scroll the transcript a line (full-screen mode) |
446
449
  | Mouse wheel | Scroll the transcript (or the open overlay). On Windows needs Node ≥ 22.17 — see [Requirements](#requirements) |
@@ -450,6 +453,7 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
450
453
  | `Ctrl+W`, `Alt+Backspace` | Delete the previous word |
451
454
  | `Ctrl+U` / `Ctrl+K` | Kill to line start / end |
452
455
  | `Up` / `Down` | Move between draft lines; recall prompt history at the edges |
456
+ | Paste | Inserted as-is up to 6 lines / 400 chars; larger collapses to `[Pasted text #1 +15 lines]`, which deletes as one unit and expands in full when you send. `--no-paste` restores the pre-0.6.3 behaviour |
453
457
  | `?` | Open help (empty input) |
454
458
 
455
459
  In **full-screen mode** the transcript is scrolled by the app itself: `PgUp` /
@@ -908,6 +912,94 @@ delegation tool (see below). `--no-team` removes it again.
908
912
  With todo planning on — also the default — one more is added: `todo_write`, the
909
913
  planning tool (see above). `--no-todo` removes it again.
910
914
 
915
+ With background services on — also the default — two more are added:
916
+ `bash_output` and `bash_kill` (see below), and `bash` itself gains a `background`
917
+ parameter. `bash.background: false` removes all three again.
918
+
919
+ ## Background services
920
+
921
+ The agent can write a web app; it also has to be able to **run** one. A command
922
+ that never exits on its own — `npm run dev`, `uvicorn main:app`, `vite`,
923
+ `docker compose up`, `tsc --watch` — is not a hung tool, it is a server, and
924
+ running it in the foreground stalls the turn until the tool timeout kills it
925
+ three minutes later.
926
+
927
+ So those commands are **supervised** instead. `bash` gains one optional
928
+ parameter:
929
+
930
+ ```jsonc
931
+ { "command": "npm run dev", "background": true }
932
+ ```
933
+
934
+ `true` supervises it, `false` never does, and **omitting it lets the runtime
935
+ decide** from the command itself — a closed allowlist over the leading token and
936
+ the script name (`npm|pnpm|yarn|bun run dev|start|serve|watch|preview`,
937
+ `next dev`, `vite`, `nuxt dev`, `ng serve`, `nodemon`, `tsc --watch`, `uvicorn`,
938
+ `gunicorn`, `flask run`, `manage.py runserver`, `rails server`, `php -S`,
939
+ `cargo watch`, `docker compose up` without `-d`, `tail -f`, `watch `). It never
940
+ fires on a chained command, because a chain's tail is what you actually wanted
941
+ the result of, and `npm run build` and `npm test` are deliberately not on it.
942
+
943
+ A supervised launch returns as soon as there is something true to say — the
944
+ **first** of ready, exited, or four seconds — so a server that crashes on boot is
945
+ reported in 400 ms with the stack in the result rather than after a three-minute
946
+ stall:
947
+
948
+ ```
949
+ $ npm run dev
950
+ [background] service s1 started (pid 24188)
951
+ [ready] http://localhost:3000 (detected from output after 1.4s)
952
+ [log tail]
953
+ > next dev
954
+ - Local: http://localhost:3000
955
+ Read more with bash_output({ service: "s1" }); stop it with bash_kill({ service: "s1" }).
956
+ ```
957
+
958
+ Readiness is a URL the server printed, or one TCP connect to a port the command
959
+ named — and if that port was **already answering before the launch**, the probe
960
+ is switched off and the card says so, rather than calling somebody else's server
961
+ yours. A process that stays up without listening on anything (`tsc --watch`) is
962
+ reported as `running`, which is the honest third answer.
963
+
964
+ Each service gets a transcript card and a `svc N` chip in the status bar. Two
965
+ tools read and stop them:
966
+
967
+ - `bash_output({ service, since? })` — the log tail and the current status.
968
+ `since` is the cursor the previous call returned, so a watcher can be read
969
+ incrementally. The tail is a bounded in-memory ring of 200 rows; there are no
970
+ log files on disk.
971
+ - `bash_kill({ service })` — stop one, or `"all"`.
972
+
973
+ **Services do not survive the CLI.** Quitting, `Ctrl+C`, `SIGTERM` and a crash
974
+ all reap the tree on a best-effort basis; there is no daemon, no PID file and no
975
+ reattach across launches. A `SIGKILL` of aragon itself is unrecoverable by
976
+ construction, and if a stop cannot confirm the process is gone it says
977
+ `may have left a detached child` rather than claiming success.
978
+
979
+ From your side there are two keys and one command: `Ctrl+C` stops every live
980
+ service (and does *not* arm exit), `/bg` lists them, and `/bg logs s1` prints a
981
+ tail without going through the model.
982
+
983
+ ### Turning it off
984
+
985
+ ```jsonc
986
+ // ~/.aragon/config.json
987
+ { "bash": { "background": false } }
988
+ ```
989
+
990
+ `ARAGON_BASH_BACKGROUND=0` does the same for one run;
991
+ `ARAGON_BASH_AUTO_BACKGROUND=0` keeps the tools but stops the classifier firing
992
+ on a command that did not ask. With `background: false` the build behaves exactly
993
+ as it did before this feature existed — no `background` parameter, no extra
994
+ tools, no prompt block, no chip.
995
+
996
+ Two things are **not** governed by that switch, because they are fixes rather
997
+ than features. `bash` now always settles: it returns when the process exits
998
+ rather than when its output stream closes, so a command that detaches a
999
+ grandchild can no longer leave the turn pending forever, and it is handed a
1000
+ closed stdin so a command that prompts fails instead of blocking on a read
1001
+ nobody will answer. And `Esc` twice always force-stops, whatever the flag says.
1002
+
911
1003
  ## Team subagents
912
1004
 
913
1005
  The agent can hand parts of a job to short-lived **subagents** that run in
@@ -1647,6 +1739,7 @@ env / `.env` → CLI flags**.
1647
1739
  | `planModeDefault` | `false` | Start every session in plan mode. `--no-plan` still overrides it for a single run. |
1648
1740
  | `planModeMaxAskRounds` | `4` | `ask_user` rounds allowed per user message, clamped to `[1, 10]`. The counter resets on your next message; steering does **not** reset it. |
1649
1741
  | `planModeHumanTimeoutMs` | `1800000` | Ceiling on a single question / plan wait, clamped to `[60000, 7200000]`. Thirty minutes is far outside "went to get coffee", but a ceiling has to exist so an unattended terminal cannot wedge a CI job forever. |
1742
+ | `paste` | `true` | Recognise pasted text before Ink's key parser sees it: line breaks become buffer newlines instead of fifteen separate messages, control bytes are stripped instead of overwriting the row they land on, and a paste above 6 lines or 400 characters collapses to a one-line placeholder that expands when you send. `false` treats pasted bytes as keystrokes, which is what 0.6.2 did. |
1650
1743
  | `historyEnabled` | `true` | Record submitted prompts to `prompt-history.jsonl` for `↑` recall. Setting it to `false` stops new writes; it does not delete what is already stored (that is `aragon history clear --yes`) and does not stop reading. See [Prompt history](#prompt-history). |
1651
1744
  | `team.enabled` | `true` | Master switch for team subagents. `false` means the `task` tool is never registered — `/team on` in such a session saves the setting for next launch and says so, rather than advertising a tool that is not there. |
1652
1745
  | `team.maxSubagents` | `5` | Subagents per dispatch, clamped to `[1, 10]`. The 10 is the hard ceiling and is enforced twice — on the config value and again on what the model asks for — so hand-editing this file cannot raise it. |
@@ -15,6 +15,8 @@ import type { ToolPermission } from '../exec/permission.js';
15
15
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
16
16
  import type { FilePatch } from '../tools/patch.js';
17
17
  import type { TeamEvent, TeamSnapshot } from '../team/types.js';
18
+ import { ProcSupervisor } from '../proc/supervisor.js';
19
+ import type { ProcEventListener, ServiceSnapshot } from '../proc/types.js';
18
20
  import { type FastStatus } from '../fast/wiring.js';
19
21
  import type { FastEventListener } from '../fast/types.js';
20
22
  import type { CompactionEventListener, CompactionSnapshot } from '../compaction/types.js';
@@ -176,6 +178,33 @@ export declare class AgentController {
176
178
  private readonly outputListeners;
177
179
  private readonly fast;
178
180
  private readonly compaction;
181
+ private readonly procs;
182
+ /**
183
+ * The host's notice channel, kept so `prompt()`'s totality has somewhere to
184
+ * report to (I-8). Every other consumer reads `deps.notify` through a closure
185
+ * built in the constructor; this one is called from a method, which is why it
186
+ * needs a field.
187
+ */
188
+ private readonly notifyHostFn;
189
+ /** Whether the SERVICE half is on: tools, prompt block, `background` param. */
190
+ private readonly bashBackgroundRegistered;
191
+ /** Release the exit reaper. Called by `dispose()`. */
192
+ private readonly releaseSignalHook;
193
+ /**
194
+ * Bumped by `forceStop()`; captured by `App` when it subscribes to a run.
195
+ *
196
+ * THE VIEW AND THE ENGINE DELIBERATELY DISAGREE AFTER A FORCE-STOP (D-10 /
197
+ * I-7). Rung two dispatches `runEnd` locally so the composer is usable
198
+ * immediately, but the engine keeps unwinding and keeps EMITTING -
199
+ * `turn_start`, `tool_call_start`, `turn_end`, `agent_end` - and
200
+ * `runStart`/`turnStart` would set `status: 'running'` again, bouncing the
201
+ * view straight back out of the `idle` the user was just given and appending
202
+ * entries to a transcript they believe is finished. `App` drops every event
203
+ * whose generation is stale, which is the same shape as `abortRequested`
204
+ * above and exists for the same reason: the loop legitimately emits after an
205
+ * abort lands.
206
+ */
207
+ private runGen;
179
208
  /**
180
209
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
181
210
  * Cleared at the next `agent_start`.
@@ -234,7 +263,97 @@ export declare class AgentController {
234
263
  * never rejects, so callers must not treat resolution as success.
235
264
  */
236
265
  prompt(text: string): Promise<void>;
266
+ /**
267
+ * Wait for the engine to unwind, bounded.
268
+ *
269
+ * BOUNDED BECAUSE THE UNBOUNDED CASE IS THE ONE THIS FEATURE IS ABOUT: a tool
270
+ * that never settles is exactly why rung two exists, and `Agent.waitForIdle()`
271
+ * alone would hang here for as long as it hangs there. The budget is the two
272
+ * graces a wedged `bash` can legitimately spend, plus the executor's own.
273
+ */
274
+ private waitForEngineIdle;
275
+ /** The pre-round body of `prompt`, unchanged apart from being private. */
276
+ private startPrompt;
237
277
  abort(): void;
278
+ /**
279
+ * Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
280
+ * (§3.6 / G3).
281
+ *
282
+ * FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
283
+ *
284
+ * 1. `abort()` - everything it does today, unchanged.
285
+ * 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
286
+ * child. THIS is what actually unblocks a wedged `await tool.execute`;
287
+ * an abort signal a tool ignores is advisory, and rung two exists for
288
+ * precisely the tools that ignore it.
289
+ * 3. `runGen += 1` - so `App` can drop the events the engine will keep
290
+ * emitting while it unwinds (I-7). Without it the view bounces straight
291
+ * back out of the `idle` the user was just given.
292
+ * 4. IT DOES NOT TOUCH BACKGROUND SERVICES. `Esc` interrupts the AGENT;
293
+ * `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
294
+ * sentences and it is exactly the kind of thing a later reader will
295
+ * "fix" by making this kill everything - which would mean a user who
296
+ * pressed Esc twice to stop a runaway turn also lost the dev server they
297
+ * had been working against for an hour. It is invariant I-4.
298
+ *
299
+ * UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
300
+ * constructed; with services off its foreground registry is all there is, and
301
+ * that is the half this method uses.
302
+ */
303
+ forceStop(): void;
304
+ /**
305
+ * The current run generation (I-7).
306
+ *
307
+ * `App` captures it when it subscribes to a run and drops every engine event
308
+ * whose generation is stale. Same shape as the `abortRequested` guard the fast
309
+ * reviewer already uses, and for the same reason: the loop legitimately emits
310
+ * after an abort lands.
311
+ */
312
+ get runGeneration(): number;
313
+ /** Whether `bash_output` / `bash_kill` are in `this.tools`. Fixed at construction. */
314
+ isBackgroundRegistered(): boolean;
315
+ /**
316
+ * Subscribe to the CLI-local `ProcEvent` stream.
317
+ *
318
+ * Returns a live unsubscribe even when services are off, because the
319
+ * supervisor exists either way - the contract `subscribeTeam` /
320
+ * `subscribeTodos` / `subscribeFast` / `subscribeToolOutput` already set. With
321
+ * services off nothing is ever emitted, so an unconditional subscription in
322
+ * `App` costs one `Set` entry.
323
+ */
324
+ subscribeProc(listener: ProcEventListener): () => void;
325
+ getServiceSnapshot(id: string): ServiceSnapshot | undefined;
326
+ listServices(): ServiceSnapshot[];
327
+ /** `starting | ready | running`. Drives the status chip and the Ctrl+C rung. */
328
+ liveServiceCount(): number;
329
+ readServiceLog(id: string, since?: number): ReturnType<ProcSupervisor['read']>;
330
+ /**
331
+ * Stop one service, or every service when `id` is `'all'`.
332
+ *
333
+ * The GRACEFUL ladder (`SIGTERM` -> `SIGKILL` after `stopGraceMs`), because
334
+ * this path has a live event loop to run it on. `reapServicesSync` is the
335
+ * other one, and it is the only one an exit path may use.
336
+ */
337
+ stopService(id: string): Promise<ServiceSnapshot[]>;
338
+ /**
339
+ * Rung one of `Ctrl+C`: stop every live service (§3.6 / G4).
340
+ *
341
+ * `force: true` means `reapSync()` SEMANTICS - immediate `SIGKILL`, no
342
+ * `stopGraceMs` - and is what `doExit` uses, because `doExit` awaits nothing
343
+ * and must not start (§3.6 step 3). The graceful ladder belongs to `Ctrl+C`,
344
+ * `bash_kill` and `/bg stop`.
345
+ */
346
+ stopAllServices(opts?: {
347
+ force?: boolean;
348
+ }): Promise<ServiceSnapshot[]>;
349
+ /**
350
+ * The synchronous reaper (I-9).
351
+ *
352
+ * Exposed so `doExit` can call it without awaiting anything. It is also what
353
+ * the signal hook registered in the constructor calls, and it is idempotent -
354
+ * which it has to be, since a normal quit reaches it twice.
355
+ */
356
+ reapServicesSync(): void;
238
357
  steer(text: string): void;
239
358
  followUp(text: string): void;
240
359
  isRunning(): boolean;