@aragon-agent/cli 0.6.16 → 0.6.17

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 (55) hide show
  1. package/README.md +44 -47
  2. package/dist/agent/controller.js +39 -47
  3. package/dist/agent/reducer.js +0 -4
  4. package/dist/cli.js +0 -13
  5. package/dist/commands/builtins.js +3 -17
  6. package/dist/compaction/child.d.ts +10 -8
  7. package/dist/compaction/child.js +23 -25
  8. package/dist/compaction/limits.d.ts +0 -2
  9. package/dist/compaction/limits.js +0 -2
  10. package/dist/config/cli-commands.d.ts +1 -1
  11. package/dist/config/cli-commands.js +16 -15
  12. package/dist/config/load.d.ts +0 -17
  13. package/dist/config/load.js +0 -6
  14. package/dist/config/schema.d.ts +17 -5
  15. package/dist/config/schema.js +14 -11
  16. package/dist/exec/events.d.ts +12 -8
  17. package/dist/exec/events.js +2 -2
  18. package/dist/exec/runner.js +4 -1
  19. package/dist/fast/limits.d.ts +2 -2
  20. package/dist/fast/limits.js +2 -2
  21. package/dist/fast/prompt.d.ts +10 -11
  22. package/dist/fast/prompt.js +19 -20
  23. package/dist/fast/reviewer.d.ts +1 -1
  24. package/dist/fast/reviewer.js +1 -1
  25. package/dist/fast/types.d.ts +0 -2
  26. package/dist/fast/wiring.d.ts +18 -34
  27. package/dist/fast/wiring.js +20 -51
  28. package/dist/session/validate-session.js +3 -1
  29. package/dist/team/child-reviewer.d.ts +105 -0
  30. package/dist/team/child-reviewer.js +117 -0
  31. package/dist/team/normalize.d.ts +7 -26
  32. package/dist/team/normalize.js +9 -22
  33. package/dist/team/prompt.d.ts +0 -11
  34. package/dist/team/prompt.js +3 -12
  35. package/dist/team/report.d.ts +4 -19
  36. package/dist/team/report.js +1 -27
  37. package/dist/team/runtime.d.ts +5 -10
  38. package/dist/team/runtime.js +35 -39
  39. package/dist/team/subagent.d.ts +18 -18
  40. package/dist/team/subagent.js +80 -30
  41. package/dist/team/task-tool.d.ts +6 -26
  42. package/dist/team/task-tool.js +13 -47
  43. package/dist/team/types.d.ts +3 -42
  44. package/dist/ui/App.js +3 -4
  45. package/dist/ui/BottomStatusRow.js +2 -1
  46. package/dist/ui/StatusBar.js +2 -1
  47. package/dist/ui/TeamPanel.js +1 -1
  48. package/dist/ui/entries/TeamCard.js +1 -6
  49. package/dist/ui/layout/status-detail-layout.js +12 -2
  50. package/dist/ui/layout/status-layout.d.ts +4 -2
  51. package/dist/ui/layout/status-layout.js +24 -1
  52. package/dist/ui/overlays/HelpOverlay.js +1 -2
  53. package/package.json +2 -2
  54. package/dist/ui/ModeChip.d.ts +0 -18
  55. package/dist/ui/ModeChip.js +0 -24
package/README.md CHANGED
@@ -409,7 +409,7 @@ signal ends the run (`130` SIGINT, `143` SIGTERM, `129` SIGHUP).
409
409
  | `/team [on\|off\|max <n>]` | Team subagents: status and the live roster, the on/off switch, the fan-out width |
410
410
  | `/todo [on\|off\|panel on\|off\|follow <mode>\|clear\|continue]` | Todo planning: status, the on/off switch, the rail, follow-through, and picking an unfinished plan back up |
411
411
  | `/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 |
412
- | `/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 |
412
+ | `/fast [on\|off\|model <id>\|provider <id>\|same\|review <n\|off>]` | Fast model tier: status and this session's totals, the on/off switch, which model, the review cadence |
413
413
  | `/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 |
414
414
  | `/context` | What the context gauge is showing: occupancy split into measured and estimated, the window and **where it came from**, how stale the measurement is, compaction's actual state, and what the session-spend readout includes |
415
415
  | `/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 |
@@ -810,9 +810,11 @@ still exits the session outright.
810
810
 
811
811
  ### Where the mode is shown
812
812
 
813
- The status bar always names a non-default mode (`PLAN`, or `PLAN → BUILD` while
814
- a switch is pending), including on short terminals and with `--no-hints`. The composer additionally shows a `PLAN` chip on its hint row and
815
- tints its border, where there is room for it.
813
+ The status bar's first row always names the current mode (`BUILD` or `PLAN`,
814
+ an accent word right after the run state; `PLAN>BUILD` while a switch is
815
+ pending; `B` / `P` / `P>B` when the terminal is narrow), including on short
816
+ terminals and with `--no-hints`. The Ctrl+G details row repeats it, and the
817
+ composer tints its border in plan mode.
816
818
 
817
819
  ### Switching mid-run
818
820
 
@@ -821,7 +823,7 @@ during a run takes effect immediately — you pressed it *because* the agent is
821
823
  about to do something. Pressing it toward `BUILD` during a run is **deferred to
822
824
  the end of the run**, so a session you launched under a read-only guarantee
823
825
  cannot start writing files because of one stray keypress. The status bar shows
824
- `PLAN → BUILD` while that is pending. Approving a plan is the one exception, and
826
+ `PLAN>BUILD` while that is pending. Approving a plan is the one exception, and
825
827
  it is an informed act rather than a stray keypress.
826
828
 
827
829
  ### If `Shift+Tab` does nothing
@@ -839,7 +841,7 @@ node -e "process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on(
839
841
  | It prints | What it means |
840
842
  | --- | --- |
841
843
  | `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) |
842
- | `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`) |
844
+ | `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 `PLAN>BUILD`) |
843
845
  | nothing | Your terminal, multiplexer or remote-desktop stack never sent it; some never send `CSI Z` at all |
844
846
 
845
847
  On the first row the wheel is dead for the same reason, and with the `/` palette
@@ -1184,25 +1186,24 @@ children's tokens are folded into the `[usage]` footer.
1184
1186
  ## Fast model tier
1185
1187
 
1186
1188
  A second, cheaper model can be made a first-class citizen of a session. It is
1187
- **off by default** — with `fast.enabled: false` the system prompt, the `task`
1188
- tool schema, the transcript and the request payloads are exactly what they were
1189
- before this feature existed — and it buys you two things once you turn it on.
1190
-
1191
- **Delegation (the agent decides).** `task` gains one optional field per
1192
- subagent, `model: "fast"`. A child marked that way runs on the fast model and is
1193
- otherwise identical: same tools, same working directory, same `--confirm` gate,
1194
- same plan-mode gate, same skills ceiling. Delegating to a cheaper model is a
1195
- *model* choice, never a permission boundary. This is where "not complicated, but
1196
- very expensive in context" is actually paid for: a child that reads nine files
1197
- and reports four sentences moves ~200 KB of file bodies out of the lead's window
1198
- and onto a model that costs a fraction as much per token.
1189
+ **off by default** — with `fast.enabled: false` the system prompt, the
1190
+ transcript and the request payloads are exactly what they were before this
1191
+ feature existed — and it buys you two things once you turn it on.
1192
+
1193
+ **Every `task` child runs the lead's own model.** Per-child fast dispatch
1194
+ (`model: "fast"`) was removed for main-agent parity: a delegation is never a
1195
+ downgrade, and all the gates (tools, working directory, `--confirm`, plan mode,
1196
+ skills ceiling, compaction policy) are the lead's own.
1199
1197
 
1200
1198
  **Periodic review (the harness decides).** Every *N* completed turns, a small
1201
1199
  digest of what the lead has been doing goes to the fast model with one question
1202
1200
  — is this still on track? — and if the answer is not "on track" it is injected
1203
1201
  back into the running loop as a `<fast_review>` block. The lead sees a short
1204
1202
  second opinion mid-run, when it can still act on it, for a few hundred tokens
1205
- instead of a second full-size turn.
1203
+ instead of a second full-size turn. **Subagents get the same review**: when the
1204
+ tier is live and `fast.review` is on, each dispatched child runs with its own
1205
+ reviewer — same cadence, same budget per child, same fail-fast transport — so a
1206
+ team is as capable as the agent that dispatched it.
1206
1207
 
1207
1208
  ```
1208
1209
  > refactor the config loader and keep the tests green
@@ -1224,7 +1225,7 @@ separately. If the fast model is not in the built-in price table the number is
1224
1225
  shown as `unknown` rather than as `$0.00` — a feature that looks free while it
1225
1226
  is spending money is worse than one that admits it does not know.
1226
1227
 
1227
- **The review has a session budget, and delegation deliberately does not.** A
1228
+ **The review has a session budget.** A
1228
1229
  session is not one run — it is one run per message you send — so a cadence alone
1229
1230
  bounds nothing over a working afternoon. `fast.reviewMaxPerSession` (default
1230
1231
  **40**) is the total number of reviews a session may *start*; reaching it prints
@@ -1233,9 +1234,7 @@ first review rather than a bare count you can only size afterwards. Raise it liv
1233
1234
  with `/fast budget 80`. The budget is denominated in **reviews, not currency**,
1234
1235
  because the fast tier is precisely where a model the price table has never seen
1235
1236
  lives — a dollar ceiling would silently never fire for the configuration that
1236
- most needs one. Delegation is never budgeted: it is spend the lead chose in
1237
- service of a message you sent, it is already bounded by `team.*`, and it exists
1238
- to *reduce* total cost.
1237
+ most needs one.
1239
1238
 
1240
1239
  ### Turning it on
1241
1240
 
@@ -1255,9 +1254,8 @@ cannot grow a field mid-session.
1255
1254
  | `fast.provider` | `''` | Empty inherits the main provider. |
1256
1255
  | `fast.model` | `''` | The fast model id. Empty means *not configured* — it never falls back to the main model, so "same as main" stays distinguishable from a typo. |
1257
1256
  | `fast.baseUrl` | `''` | Empty inherits the session's base URL **only** when the provider matches. |
1258
- | `fast.thinkingLevel` | `'off'` | Applied to fast children and to reviews. |
1259
- | `fast.delegate` | `true` | Allow `model:"fast"` on `task`. |
1260
- | `fast.review` | `true` | Run the periodic review. |
1257
+ | `fast.thinkingLevel` | `'off'` | Applied to reviews (lead and subagents) and to fast-tier compaction summaries. |
1258
+ | `fast.review` | `true` | Run the periodic review — for the lead and for every `task` child. |
1261
1259
  | `fast.reviewEveryTurns` | `5` | Turns between reviews (1–50). |
1262
1260
  | `fast.reviewContextTurns` | `3` | Turns included in one digest (1–10). |
1263
1261
  | `fast.reviewMaxChars` | `280` | Ceiling on an injected critique (80–600). |
@@ -1281,7 +1279,6 @@ reporting one:
1281
1279
  /fast same run the fast tier on the main model
1282
1280
  /fast review <n> | off
1283
1281
  /fast budget [n] the session review budget; bare form reads it
1284
- /fast delegate on | off
1285
1282
  ```
1286
1283
 
1287
1284
  `/fast budget` with no argument is a **read**, so it is answerable while a `task`
@@ -1290,9 +1287,8 @@ like every other `/fast` setting.
1290
1287
 
1291
1288
  **What you see.** A `fast` chip in the status bar's right cluster while the tier
1292
1289
  is live (`fast*` while a review is in flight, dropped below 100 columns), a
1293
- one-line card per review in the transcript, and `n fast` plus a trailing `~` on
1294
- fast children in the team roster. `/fast status` reports the session totals the
1295
- status bar cannot: reviews run, children delegated, tokens and cost. Under `-p`
1290
+ one-line card per review in the transcript. `/fast status` reports the session totals the
1291
+ status bar cannot: reviews run, tokens and cost. Under `-p`
1296
1292
  nothing extra is written to stdout or stderr — the review spend is folded into
1297
1293
  the `[usage]` footer and the reviews are in the log.
1298
1294
 
@@ -1558,19 +1554,20 @@ refusal is explained. Automatic tail clipping and failure truncation are disable
1558
1554
 
1559
1555
  ### Sub-agents get the same protection
1560
1556
 
1561
- A `task` child is bounded by `team.maxTurnsPerSubagent` (24) and
1562
- `team.dispatchTimeoutMs` (15 minutes) — the same profile as a lead run that fills
1563
- a window. When a child overflowed, its history was discarded and the whole
1564
- dispatch reached you as one partial sentence.
1565
-
1566
- Children now compact their own history, under tighter bounds: **two** retained
1567
- turns instead of four, at most **two** compactions per child instead of five, the
1568
- fast tier preferred whenever it resolves, and failures preserve history. Each
1557
+ Children compact their own history **under the lead's own policy**: the same
1558
+ `keepRecentTurns`, the same `onFailure`, the same archive setting, the same
1559
+ per-run compaction bound, the fast tier preferred whenever it resolves. Each
1569
1560
  child owns its own context meter and counts compaction only after Core accepts
1570
1561
  the candidate. There is no transcript card for a
1571
1562
  child — the lead's transcript describes the lead's context. Turn it off with
1572
1563
  `compaction.subagents: false`.
1573
1564
 
1565
+ Since main-agent parity, the turn cap, the per-child wall clock and the dispatch
1566
+ wall clock all default to **0 = no limit**: a child runs until it finishes, like
1567
+ the lead, with only its own idle watchdog and your Esc as ceilings. Set any of
1568
+ `team.subagentTimeoutMs` / `team.dispatchTimeoutMs` / `team.maxTurnsPerSubagent`
1569
+ to a positive value to bring a ceiling back.
1570
+
1574
1571
  ### A compaction is no longer irreversible
1575
1572
 
1576
1573
  After a splice is adopted, the dropped messages are written verbatim to
@@ -1959,9 +1956,9 @@ env / `.env` → CLI flags**.
1959
1956
  | `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. |
1960
1957
  | `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. |
1961
1958
  | `team.maxConcurrent` | `3` | Subagents in flight at once, clamped to `[1, 10]` and capped at `maxSubagents`. A separate knob because provider rate limits are a different constraint from context economics. |
1962
- | `team.subagentTimeoutMs` | `300000` | One subagent's wall clock before it is aborted, clamped to `[30000, 1800000]`. A wedged subagent dies on its own ceiling without taking the dispatch with it. |
1963
- | `team.dispatchTimeoutMs` | `900000` | The whole dispatch's wall clock, clamped to `[60000, 3600000]`. |
1964
- | `team.maxTurnsPerSubagent` | `24` | Runaway-loop cap, clamped to `[4, 100]`. A subagent stopped here is reported as `stopped: turn cap reached`, with whatever it had produced. |
1959
+ | `team.subagentTimeoutMs` | `0` | One subagent's wall clock before it is aborted, clamped to `[0, 1800000]`. `0` (the default) = no ceiling — a wedged subagent still dies on its own idle watchdog, exactly like the lead. |
1960
+ | `team.dispatchTimeoutMs` | `0` | The whole dispatch's wall clock (and the `task` tool timeout), clamped to `[0, 3600000]`. `0` (the default) = no ceiling; only Esc ends a dispatch early. |
1961
+ | `team.maxTurnsPerSubagent` | `0` | Runaway-loop cap, clamped to `[0, 100]`. `0` (the default) = no cap, matching the lead. A subagent stopped at a positive cap is reported as `stopped: turn cap reached`, with whatever it had produced. |
1965
1962
  | `todo.enabled` | `true` | Master switch for todo planning. `false` means the `todo_write` tool is never registered — `/todo on` in such a session saves the setting for next launch and says so, rather than advertising a tool that is not there. |
1966
1963
  | `todo.panel` | `true` | Render the right-hand rail. Independent of `todo.enabled` on purpose: a screen-reader user wants the planning discipline without the column, and the system prompt varies one sentence accordingly. |
1967
1964
  | `todo.followThrough` | `"notify"` | What happens when a run ends with steps left. `notify` says so and stops; `auto` continues the plan after a 3-second grace window, bounded by two structural limits (one fruitless attempt, 25 continuations per plan) that are deliberately **not** settings; `off` is silent. An unrecognized value falls back to `notify`. |
@@ -1972,7 +1969,7 @@ env / `.env` → CLI flags**.
1972
1969
  | `retry.multiplier` | `2` | Growth factor, clamped to `[1, 5]`. The one **float** key in this section: `2.5` is a legitimate factor and is stored as typed. |
1973
1970
  | `retry.jitter` | `true` | Equal jitter (`d/2 + rand·d/2`), which de-correlates concurrent subagents hitting the same 429 while keeping the announced countdown within a factor of two of the truth. Full jitter can fire 40 ms after announcing 30 s, which reads as a bug. |
1974
1971
  | `retry.respectRetryAfter` | `true` | Honour `Retry-After` (and the provider rate-limit reset headers) as a **floor**, never a replacement — a `Retry-After: 1` on the ninth retry does not undo eight retries of backoff. A stated wait above 60 s is surfaced immediately instead of waited out. |
1975
- | `retry.maxElapsedMs` | `240000` | Wall-clock budget from the first failure, clamped to `[10000, 1800000]`. Deliberately **below** `team.subagentTimeoutMs`: an equal budget means a subagent is killed at the same instant its ladder ends, and the user is then told "subagent timed out" for what was a provider outage. |
1972
+ | `retry.maxElapsedMs` | `240000` | Wall-clock budget from the first failure, clamped to `[10000, 1800000]`. When `team.subagentTimeoutMs` is positive, keep this below it: an equal budget means a subagent is killed at the same instant its ladder ends, and the user is then told "subagent timed out" for what was a provider outage. |
1976
1973
  | `retry.onPartialStream` | `true` | Allow a restart after content has already streamed to the transcript. The view is rewound and the discarded tool cards removed; engine history is provably clean at that point, because it is only written on a completed turn. `false` refuses the whole path. |
1977
1974
  | `skills.enabled` | `true` | Master switch for the skill system. |
1978
1975
  | `skills.requireApproval` | `true` | Require a human to approve `skill_install` / `skill_create`. |
@@ -2364,19 +2361,19 @@ node packages/cli/dist/cli.js --version
2364
2361
  - `echo "list files" | node dist/cli.js -p` prints an answer and exits `0`.
2365
2362
  - A missing/invalid key produces a visible, actionable message (never a blank
2366
2363
  screen), both in the TUI and headless.
2367
- - `Shift+Tab` at 40, 80 and 120 columns - the chip, the hint and the status bar
2368
- are all legible and nothing wraps onto a second row.
2364
+ - `Shift+Tab` at 40, 80 and 120 columns - the status-bar mode word (`PLAN` / `P`)
2365
+ stays legible and nothing wraps onto a second row.
2369
2366
  - The question wizard at 24 rows with 5 questions x 4 options - no clipping, and
2370
2367
  the `n/N` counter matches what is on screen.
2371
2368
  - Approve a plan and watch the SAME run start writing files, with no second
2372
2369
  prompt.
2373
- - `--no-color` on a legacy `cmd.exe` - the chip renders `[PLAN]`, radios render
2374
- `*`/`o`, checkboxes render `[x]`/`[ ]`, no mojibake anywhere.
2370
+ - `--no-color` on a legacy `cmd.exe` - the mode word renders as plain `PLAN` text,
2371
+ radios render `*`/`o`, checkboxes render `[x]`/`[ ]`, no mojibake anywhere.
2375
2372
  - A plan with a 600-character summary and a 400-character step detail - the text
2376
2373
  wraps and scrolls, the footer count matches the real number of rows, and
2377
2374
  nothing is truncated to a single line.
2378
2375
  - `--no-hints` on a 20-row terminal in plan mode - the status bar still says
2379
- `PLAN` with no chip on screen.
2376
+ `PLAN`.
2380
2377
  - Dismiss a plan card with `Esc`, let the agent resubmit, then close the card and press `Esc` twice within 1.5 seconds:
2381
2378
  the run aborts and the session returns to an idle prompt. Do this one BY HAND
2382
2379
  as well as in a test - the point of the check is whether the exit is findable,
@@ -12,6 +12,7 @@ import { createBuiltinTools } from '../tools/index.js';
12
12
  import { createFileChangeStore } from '../tools/file-change-store.js';
13
13
  import { createToolOutputStore, } from '../tools/tool-output-store.js';
14
14
  import { TeamRuntime } from '../team/runtime.js';
15
+ import { createChildReviewer } from '../team/child-reviewer.js';
15
16
  import { TeamHumanQueue } from '../team/human-queue.js';
16
17
  import { createTaskTool } from '../team/task-tool.js';
17
18
  import { buildTeamBlock } from '../team/prompt.js';
@@ -161,14 +162,14 @@ export class AgentController {
161
162
  // Fast model tier (fast-model-tier §3.1 / §3.3)
162
163
  //
163
164
  // ONE FIELD AND FOUR THIN FORWARDERS. The flag pair, the tier cache, the
164
- // reviewer's lifetime and the `resolveTier` closure all live in
165
+ // reviewer's lifetime and the shared fail-fast review transport all live in
165
166
  // `fast/wiring.ts`, which is a BUDGET decision rather than an aesthetic one
166
167
  // (C-12 / RV-5): `CLAUDE.md` caps a source file at 1000 lines and this one had
167
168
  // 40 to spare when the design was written.
168
169
  //
169
170
  // `null` when the tier could not be resolved at construction, which is the
170
- // byte-identity branch: no reviewer is subscribed, `task` carries no `model`
171
- // property, and `<fast_tier>` is never spliced (I-2).
171
+ // byte-identity branch: no reviewer is subscribed and `<fast_tier>` is
172
+ // never spliced (I-2).
172
173
  // -----------------------------------------------------------------------
173
174
  fast;
174
175
  // -----------------------------------------------------------------------
@@ -311,8 +312,8 @@ export class AgentController {
311
312
  this.todos = this.todoRegistered ? new TodoStore() : null;
312
313
  // --- Fast model tier ---------------------------------------------------
313
314
  //
314
- // RESOLVED BEFORE THE TOOL ARRAY, because `fastRegistered` decides whether
315
- // `task`'s schema carries a `model` property at all and the array is built
315
+ // RESOLVED BEFORE THE AGENT, because `fastRegistered` decides whether the
316
+ // `FastWiring` (reviewer, tier cache) exists at all and the wiring is built
316
317
  // ONCE (C-2). `FastWiring` re-runs the same pure function in its own
317
318
  // constructor; the two agree by construction because they read the same
318
319
  // config through the same resolver.
@@ -324,24 +325,35 @@ export class AgentController {
324
325
  this.teamRuntime = this.teamRegistered
325
326
  ? new TeamRuntime({
326
327
  getConfig: () => this.config,
327
- // Read LIVE through the wiring, exactly as `resolveTier` below is: the
328
- // closure is only ever called from inside a dispatch, and
329
- // `childFactory()` re-reads `compaction.subagents` on every call so a
330
- // settings-screen edit reaches the next dispatch.
328
+ // Read LIVE: the closure is only ever called from inside a dispatch,
329
+ // and `childFactory()` re-reads `compaction.subagents` on every call
330
+ // so a settings-screen edit reaches the next dispatch.
331
331
  contextManagerFor: (req) => this.compaction?.childFactory()?.(req),
332
- // Read LIVE through the wiring, which does not exist yet — the closure
333
- // is only ever called from inside a dispatch, long after construction.
334
- resolveTier: (tier) => this.fast
335
- ? this.fast.resolveTier(tier)
336
- : {
337
- ref: {
338
- providerId: this.config.provider,
339
- modelId: this.config.model,
340
- ...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
341
- },
342
- thinkingLevel: this.config.thinkingLevel,
343
- role: 'main',
332
+ // PER-CHILD FAST REVIEW (main-agent parity): one reviewer per
333
+ // child, sharing the lead wiring's tier, policy and fail-fast
334
+ // transport. `this.fast` is null until after the Agent exists; these
335
+ // closures run only from inside a dispatch, long after construction.
336
+ review: {
337
+ // EVERY `this.fast` READ LIVES INSIDE A CLOSURE: the wiring is
338
+ // assigned AFTER this constructor block (it needs the Agent), and
339
+ // a read in the constructor's own flow would be a use-before-assign.
340
+ // The closures run only from inside a dispatch, long after
341
+ // construction, when the field has its final value.
342
+ active: () => this.fast !== null && this.fast.available() && this.config.fast.review,
343
+ modelId: () => {
344
+ const tier = this.fast?.getTier();
345
+ return tier && tier.ok ? tier.ref.modelId : '';
344
346
  },
347
+ create: (request) => this.fast
348
+ ? createChildReviewer(request, {
349
+ getConfig: () => this.config,
350
+ hasKey: (id, role) => this.hasApiKey(id, role),
351
+ getApiKey: (id, role) => this.resolveKey(id, role),
352
+ complete: (providerId, llmRequest) => this.fast.completeViaFastRegistry(providerId, llmRequest),
353
+ available: () => this.fast?.available() === true,
354
+ })
355
+ : undefined,
356
+ },
345
357
  providerRegistry: this.providerRegistry,
346
358
  getCwd: () => this.cwd,
347
359
  getMode: () => this.effectiveMode,
@@ -488,23 +500,12 @@ export class AgentController {
488
500
  runtime: this.teamRuntime,
489
501
  // Read LIVE: `/team off` flips the flag, it cannot unregister.
490
502
  isTeamEnabled: () => this.teamEnabled,
491
- // FROZEN AT CONSTRUCTION (C-2). `/fast on` in a session launched
492
- // without the tier cannot add a schema property, and it says so
493
- // rather than advertising one that is not there.
494
- fastRegistered,
495
- // Read LIVE at dispatch time (RV-3): the tier can stop resolving
496
- // mid-session, and the normalizer and the subagent factory must
497
- // agree about that at the same instant.
498
- fastAvailable: () => this.fast?.delegationAvailable() === true,
499
503
  maxSubagents: () => this.config.team.maxSubagents,
500
504
  hasApiKey: () => this.hasApiKey(),
501
505
  activeProvider: () => this.config.provider,
506
+ // Every child runs the lead's own model, so the lead's cost table
507
+ // is the one honest price for the dispatch total.
502
508
  modelCost: () => this.getModelInfo().cost,
503
- fastModelCost: () => {
504
- const ref = this.fast?.fastRef();
505
- return ref ? this.getModelInfoFor(ref).cost : undefined;
506
- },
507
- fastPricingUnknown: () => this.fast?.snapshot().pricingUnknown === true,
508
509
  withPausedWatchdog: (fn) => this.withPausedWatchdog(fn),
509
510
  }),
510
511
  ]
@@ -603,6 +604,10 @@ export class AgentController {
603
604
  // number only bites BECAUSE `task` races `ctx.signal` — the
604
605
  // executor's timeout is cooperative, so an override is inert for any
605
606
  // tool that does not listen (I-2).
607
+ //
608
+ // `dispatchTimeoutMs: 0` (the default) reaches the executor as an
609
+ // override of `0` = NO TIMER AT ALL: the dispatch lasts as long as its
610
+ // slowest child, and only an Esc ends it early (main-agent parity).
606
611
  ...(this.teamRegistered ? { task: config.team.dispatchTimeoutMs } : {}),
607
612
  },
608
613
  },
@@ -672,13 +677,6 @@ export class AgentController {
672
677
  });
673
678
  if (this.fast) {
674
679
  this.rebuildSystemPrompt();
675
- // Fast-tier children are counted from the outcome rather than from the
676
- // specs, so a downgraded child is not miscounted as a saving (§3.6).
677
- this.teamRuntime?.subscribe((event) => {
678
- if (event.type !== 'dispatch_end')
679
- return;
680
- this.fast?.noteDelegated(event.outcome.runs.filter((r) => r.tier === 'fast').length);
681
- });
682
680
  }
683
681
  // ONE LINE FOR THE TWO CONFIGURATION MISTAKES THAT ARE OTHERWISE INVISIBLE
684
682
  // (§3.3): the tier is on but unnamed, or named against a provider with no
@@ -738,11 +736,6 @@ export class AgentController {
738
736
  ? buildTeamBlock({
739
737
  maxSubagents: config.team.maxSubagents,
740
738
  maxConcurrent: config.team.maxConcurrent,
741
- // One cross-reference sentence, present only when the capability
742
- // is (§3.8). `this.fast` is null during the FIRST compose (the
743
- // wiring needs the Agent), which is why the constructor composes
744
- // again once it exists.
745
- fastDelegation: !!fastAvailable && config.fast.delegate,
746
739
  })
747
740
  : '',
748
741
  // BOTH FLAGS, exactly as team mode uses both, and for the identical
@@ -762,7 +755,6 @@ export class AgentController {
762
755
  fastBlock: fastAvailable
763
756
  ? buildFastBlock({
764
757
  model: tier.ok ? tier.ref.modelId : '',
765
- delegate: config.fast.delegate,
766
758
  review: config.fast.review,
767
759
  })
768
760
  : '',
@@ -860,10 +860,6 @@ export function viewReducer(state, action) {
860
860
  const runs = action.specs.map((spec) => ({
861
861
  label: spec.label,
862
862
  description: spec.description,
863
- // Already downgraded by the normalizer when the tier was unavailable, so
864
- // the card and the report agree from the first frame (fast-model-tier
865
- // §3.4).
866
- tier: spec.tier,
867
863
  phase: 'queued',
868
864
  turns: 0,
869
865
  toolCalls: 0,
package/dist/cli.js CHANGED
@@ -160,11 +160,6 @@ function toFlags(opts) {
160
160
  fastModel: opts.fastModel,
161
161
  fastProvider: opts.fastProvider,
162
162
  fastReview: opts.fastReview,
163
- // …and again for `--fast-delegate` / `--no-fast-delegate`, one feature
164
- // later — NINE features, one trap. It is `fast.delegate`'s only channel:
165
- // there is no `ARAGON_FAST_DELEGATE`, so missing this line leaves a wrapper
166
- // with no way at all to say "do not delegate on this run".
167
- fastDelegate: opts.fastDelegate,
168
163
  // `--verbose` is just a shorthand for `--log-level debug`; an explicit
169
164
  // `--log-level` still wins, so the two can be combined without surprise.
170
165
  logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
@@ -1537,14 +1532,6 @@ function buildProgram() {
1537
1532
  .option('--fast-model <id>', 'The fast model id (implies nothing about --fast)')
1538
1533
  .option('--fast-provider <id>', 'Provider for the fast model (default: the main provider)')
1539
1534
  .option('--fast-review <n|off>', 'Turns between automatic fast reviews, or "off"')
1540
- // BOTH FORMS, POSITIVE FIRST, and here it matters more than anywhere above:
1541
- // `fast.delegate` is PERSISTED and defaults to TRUE, so declaring only
1542
- // `--no-fast-delegate` would make commander default `opts.fastDelegate` to
1543
- // `true` and force `delegate: true` on EVERY run that passed no flag at all
1544
- // — silently overwriting a user's stored `false`. Same polarity, same trap
1545
- // and same fix as `--update` / `--compaction`.
1546
- .option('--fast-delegate', 'Let the fast tier take mechanical sub-steps (the default)')
1547
- .option('--no-fast-delegate', 'Run every sub-step on the main model')
1548
1535
  // BOTH FORMS, positive first, for the reason recorded seven times above —
1549
1536
  // and this is the pair whose omission would be worst. `update.mode` is
1550
1537
  // PERSISTED and defaults to `'auto'`, so a lone `--no-update` would make
@@ -106,8 +106,7 @@ function formatFastStatus(controller) {
106
106
  else {
107
107
  lines.push(`Fast tier: ${state}. Model: ${cfg.model || '(not set)'}.`);
108
108
  }
109
- lines.push(`Review: ${cfg.review ? `every ${cfg.reviewEveryTurns} turns` : 'off'}. ` +
110
- `Delegation: ${cfg.delegate ? 'on' : 'off'}.`);
109
+ lines.push(`Review: ${cfg.review ? `every ${cfg.reviewEveryTurns} turns` : 'off'}.`);
111
110
  const snapshot = status.snapshot;
112
111
  const cost = snapshot.pricingUnknown
113
112
  ? `unknown (no price table for ${snapshot.model})`
@@ -124,7 +123,6 @@ function formatFastStatus(controller) {
124
123
  // numbers are `0` placeholders and `reviews >= reviewBudget` would be true.
125
124
  const budget = snapshot.budgetReached ? ' (budget reached - raise with /fast budget <n>)' : '';
126
125
  lines.push(`This session: ${snapshot.reviews}/${snapshot.reviewBudget} reviews${budget}, ` +
127
- `${snapshot.delegated} delegated subagents, ` +
128
126
  `in ${formatTokens(snapshot.usage.inputTokens)} / out ` +
129
127
  `${formatTokens(snapshot.usage.outputTokens)}, cost ${cost}.`);
130
128
  return lines.join('\n');
@@ -339,7 +337,7 @@ const COMMANDS = [
339
337
  {
340
338
  name: 'fast',
341
339
  description: 'Fast model tier: status | on | off | model <id> | provider <id> | same | ' +
342
- 'review <n|off> | budget <n> | delegate on|off',
340
+ 'review <n|off> | budget <n>',
343
341
  /**
344
342
  * `/fast on` CANNOT turn the tier on in a session that started without it,
345
343
  * for the reason `/team on` above records at length: the tool array is built
@@ -516,20 +514,8 @@ const COMMANDS = [
516
514
  `${snapshot.reviews} used.`);
517
515
  return;
518
516
  }
519
- if (verb === 'delegate') {
520
- const raw = (value ?? '').trim().toLowerCase();
521
- if (raw !== 'on' && raw !== 'off') {
522
- ctx.notify('warn', 'Usage: /fast delegate <on|off>');
523
- return;
524
- }
525
- if (!persist({ delegate: raw === 'on' }))
526
- return;
527
- ctx.toast('success', `Fast delegation ${raw}.`);
528
- return;
529
- }
530
517
  ctx.notify('warn', `Unknown argument "${verb}" - use /fast ` +
531
- '[status|on|off|model <id>|provider <id>|same|review <n|off>|budget <n>|' +
532
- 'delegate on|off].');
518
+ '[status|on|off|model <id>|provider <id>|same|review <n|off>|budget <n>].');
533
519
  },
534
520
  },
535
521
  {
@@ -13,15 +13,17 @@
13
13
  * turn the child already ran, and receives one partial sentence for all of it.
14
14
  * Two fast-model summarizations is the cheap side of that trade.
15
15
  *
16
- * A CHILD MANAGER IS THE SAME `Compactor` UNDER A DIFFERENT POLICY, and the
17
- * overlay needs TWO mechanisms rather than one (RV-2):
16
+ * SINCE MAIN-AGENT PARITY, A CHILD MANAGER RUNS THE LEAD'S OWN POLICY. The only
17
+ * overlay left is the CONNECTION (provider / model / base URL / manual window),
18
+ * which must follow the model the child actually runs on. `keepRecentTurns`,
19
+ * `onFailure`, `useFastTier`, `archive` and the per-run compaction bound are all
20
+ * read from the SAME live config the lead reads, so a user who tunes compaction
21
+ * tunes every child identically - the requirement "same mode and configuration
22
+ * as the main agent", taken literally.
18
23
  *
19
- * A. A CONFIG VIEW, for `keepRecentTurns` / `onFailure` / `useFastTier`, which
20
- * `Compactor` reads through `deps.getConfig()`.
21
- * B. AN INSTANCE BOUND, for `maxPerRun`, which is STRUCTURAL and has no config
22
- * path by design (`limits.ts` header). Without it the child silently
23
- * inherits the lead's 5 - two and a half times the bound the reversal above
24
- * rests on.
24
+ * THE INSTANCE BOUND stays (mechanism B of RV-2) because `maxPerRun` is
25
+ * STRUCTURAL with no config path by design (`limits.ts` header); it is now the
26
+ * SAME `COMPACTION_LIMITS.maxPerRun` the lead's own manager runs under.
25
27
  *
26
28
  * NO UI. A child's compaction produces no transcript card (DH-7): the lead's
27
29
  * transcript describes the LEAD's context, and a card there would claim the
@@ -13,15 +13,17 @@
13
13
  * turn the child already ran, and receives one partial sentence for all of it.
14
14
  * Two fast-model summarizations is the cheap side of that trade.
15
15
  *
16
- * A CHILD MANAGER IS THE SAME `Compactor` UNDER A DIFFERENT POLICY, and the
17
- * overlay needs TWO mechanisms rather than one (RV-2):
16
+ * SINCE MAIN-AGENT PARITY, A CHILD MANAGER RUNS THE LEAD'S OWN POLICY. The only
17
+ * overlay left is the CONNECTION (provider / model / base URL / manual window),
18
+ * which must follow the model the child actually runs on. `keepRecentTurns`,
19
+ * `onFailure`, `useFastTier`, `archive` and the per-run compaction bound are all
20
+ * read from the SAME live config the lead reads, so a user who tunes compaction
21
+ * tunes every child identically - the requirement "same mode and configuration
22
+ * as the main agent", taken literally.
18
23
  *
19
- * A. A CONFIG VIEW, for `keepRecentTurns` / `onFailure` / `useFastTier`, which
20
- * `Compactor` reads through `deps.getConfig()`.
21
- * B. AN INSTANCE BOUND, for `maxPerRun`, which is STRUCTURAL and has no config
22
- * path by design (`limits.ts` header). Without it the child silently
23
- * inherits the lead's 5 - two and a half times the bound the reversal above
24
- * rests on.
24
+ * THE INSTANCE BOUND stays (mechanism B of RV-2) because `maxPerRun` is
25
+ * STRUCTURAL with no config path by design (`limits.ts` header); it is now the
26
+ * SAME `COMPACTION_LIMITS.maxPerRun` the lead's own manager runs under.
25
27
  *
26
28
  * NO UI. A child's compaction produces no transcript card (DH-7): the lead's
27
29
  * transcript describes the LEAD's context, and a card there would claim the
@@ -45,11 +47,11 @@ export function createChildContextManager(req, deps) {
45
47
  * which is the same reason `subagent.ts` reuses the lead's `getApiKey` closure
46
48
  * rather than the resolved key.
47
49
  *
48
- * `useFastTier: true` EXPRESSES A PREFERENCE, NOT AN OUTCOME. `resolveFastTier`
49
- * still decides, and when nothing resolves the child falls back exactly as the
50
- * lead does - to its OWN model, because `provider` / `model` are overlaid here
51
- * too.
52
- *
50
+ * `useFastTier` is read LIVE from the base config (main-agent
51
+ * parity - no overlay): it expresses a PREFERENCE, not an outcome.
52
+ * `resolveFastTier` still decides, and when nothing resolves the child
53
+ * falls back exactly as the lead does - to its OWN model, because
54
+ * `provider` / `model` are overlaid here too.
53
55
  * Failed summaries preserve the child's task just as they preserve the lead's.
54
56
  */
55
57
  const childConfig = () => {
@@ -63,16 +65,11 @@ export function createChildContextManager(req, deps) {
63
65
  baseUrl: req.model.baseUrl,
64
66
  // The lead's manual limit describes its connection, not every worker.
65
67
  contextWindow: sameConnection ? base.contextWindow : null,
66
- compaction: {
67
- ...base.compaction,
68
- keepRecentTurns: COMPACTION_LIMITS.childKeepRecentTurns,
69
- onFailure: 'stop',
70
- useFastTier: true,
71
- // ONE ARCHIVE PER LEAD COMPACTION IS AUDITABLE; TWENTY PER DISPATCH IS
72
- // NOISE. The child's dropped slice is therefore never retained either -
73
- // `runCompaction` reads this key before it stashes anything.
74
- archive: false,
75
- },
68
+ // NO POLICY OVERLAY (main-agent parity): `keepRecentTurns`, `onFailure`,
69
+ // `useFastTier` and `archive` are read LIVE from the base config, so the
70
+ // child compacts under exactly the policy the lead compacts under. A
71
+ // settings-screen edit mid-dispatch reaches a running child because this
72
+ // view is re-read on every call.
76
73
  };
77
74
  };
78
75
  const meter = new ContextMeter({
@@ -121,8 +118,9 @@ export function createChildContextManager(req, deps) {
121
118
  // user's transcript with no card to explain which agent it is about.
122
119
  notify: () => { },
123
120
  },
124
- // MECHANISM B - the instance bound (RV-2 / DH-16).
125
- { maxPerRun: COMPACTION_LIMITS.childMaxPerRun });
121
+ // MECHANISM B - the instance bound (RV-2 / DH-16). The SAME bound the
122
+ // lead's own manager runs under (main-agent parity).
123
+ { maxPerRun: COMPACTION_LIMITS.maxPerRun });
126
124
  return {
127
125
  // THERE IS NO `/compact` FOR A CHILD, so nothing is ever queued and the
128
126
  // manual branch of `shouldCompact` is unreachable by construction.
@@ -28,8 +28,6 @@ export declare const COMPACTION_LIMITS: {
28
28
  readonly archiveMaxFiles: 20;
29
29
  readonly archiveMaxAgeMs: 604800000;
30
30
  readonly archiveMaxBytes: 8000000;
31
- readonly childKeepRecentTurns: 2;
32
- readonly childMaxPerRun: 2;
33
31
  readonly cardTextRows: 6;
34
32
  readonly statusCompactCols: 100;
35
33
  };
@@ -29,8 +29,6 @@ export const COMPACTION_LIMITS = {
29
29
  archiveMaxFiles: 20,
30
30
  archiveMaxAgeMs: 604_800_000,
31
31
  archiveMaxBytes: 8_000_000,
32
- childKeepRecentTurns: 2,
33
- childMaxPerRun: 2,
34
32
  cardTextRows: 6,
35
33
  statusCompactCols: 100,
36
34
  };
@@ -42,7 +42,7 @@ export declare const RETRY_CONFIG_SET_KEYS: readonly ["retry.enabled", "retry.ma
42
42
  * would silently never take effect, and `config set` would still print
43
43
  * `Set fast.model = haiku`.
44
44
  */
45
- export declare const FAST_CONFIG_SET_KEYS: readonly ["fast.enabled", "fast.provider", "fast.model", "fast.baseUrl", "fast.thinkingLevel", "fast.delegate", "fast.review", "fast.reviewEveryTurns", "fast.reviewContextTurns", "fast.reviewMaxChars", "fast.reviewMaxPerSession"];
45
+ export declare const FAST_CONFIG_SET_KEYS: readonly ["fast.enabled", "fast.provider", "fast.model", "fast.baseUrl", "fast.thinkingLevel", "fast.review", "fast.reviewEveryTurns", "fast.reviewContextTurns", "fast.reviewMaxChars", "fast.reviewMaxPerSession"];
46
46
  /**
47
47
  * Config keys under `update.` that `aragon config set` accepts (cli-auto-update
48
48
  * §4.1).