@aragon-agent/cli 0.6.15 → 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 (72) hide show
  1. package/README.md +108 -71
  2. package/dist/agent/controller.d.ts +10 -0
  3. package/dist/agent/controller.js +52 -47
  4. package/dist/agent/reducer.js +0 -4
  5. package/dist/cli.js +49 -15
  6. package/dist/commands/builtins.js +27 -43
  7. package/dist/compaction/child.d.ts +10 -8
  8. package/dist/compaction/child.js +23 -25
  9. package/dist/compaction/context-command.js +14 -3
  10. package/dist/compaction/limits.d.ts +0 -2
  11. package/dist/compaction/limits.js +0 -2
  12. package/dist/compaction/types.d.ts +6 -1
  13. package/dist/config/app-paths.d.ts +10 -0
  14. package/dist/config/app-paths.js +12 -0
  15. package/dist/config/cli-commands.d.ts +1 -1
  16. package/dist/config/cli-commands.js +16 -15
  17. package/dist/config/env.d.ts +2 -1
  18. package/dist/config/env.js +13 -1
  19. package/dist/config/load.d.ts +9 -17
  20. package/dist/config/load.js +14 -6
  21. package/dist/config/model-windows.d.ts +91 -0
  22. package/dist/config/model-windows.js +181 -0
  23. package/dist/config/schema.d.ts +34 -5
  24. package/dist/config/schema.js +15 -11
  25. package/dist/exec/events.d.ts +12 -8
  26. package/dist/exec/events.js +2 -2
  27. package/dist/exec/runner.js +4 -1
  28. package/dist/fast/limits.d.ts +2 -2
  29. package/dist/fast/limits.js +2 -2
  30. package/dist/fast/prompt.d.ts +10 -11
  31. package/dist/fast/prompt.js +19 -20
  32. package/dist/fast/reviewer.d.ts +1 -1
  33. package/dist/fast/reviewer.js +1 -1
  34. package/dist/fast/types.d.ts +0 -2
  35. package/dist/fast/wiring.d.ts +18 -34
  36. package/dist/fast/wiring.js +20 -51
  37. package/dist/input/csiu-keys.d.ts +66 -0
  38. package/dist/input/csiu-keys.js +170 -0
  39. package/dist/input/keyboard-enhancement.d.ts +42 -0
  40. package/dist/input/keyboard-enhancement.js +65 -0
  41. package/dist/input/stdin-filter.d.ts +9 -0
  42. package/dist/input/stdin-filter.js +45 -9
  43. package/dist/input/win32-input-mode.d.ts +61 -0
  44. package/dist/input/win32-input-mode.js +251 -0
  45. package/dist/session/validate-session.js +3 -1
  46. package/dist/team/child-reviewer.d.ts +105 -0
  47. package/dist/team/child-reviewer.js +117 -0
  48. package/dist/team/normalize.d.ts +7 -26
  49. package/dist/team/normalize.js +9 -22
  50. package/dist/team/prompt.d.ts +0 -11
  51. package/dist/team/prompt.js +3 -12
  52. package/dist/team/report.d.ts +4 -19
  53. package/dist/team/report.js +1 -27
  54. package/dist/team/runtime.d.ts +5 -10
  55. package/dist/team/runtime.js +35 -39
  56. package/dist/team/subagent.d.ts +18 -18
  57. package/dist/team/subagent.js +80 -30
  58. package/dist/team/task-tool.d.ts +6 -26
  59. package/dist/team/task-tool.js +13 -47
  60. package/dist/team/types.d.ts +3 -42
  61. package/dist/ui/App.js +3 -4
  62. package/dist/ui/BottomStatusRow.js +2 -1
  63. package/dist/ui/StatusBar.js +2 -1
  64. package/dist/ui/TeamPanel.js +1 -1
  65. package/dist/ui/entries/TeamCard.js +1 -6
  66. package/dist/ui/layout/status-detail-layout.js +12 -2
  67. package/dist/ui/layout/status-layout.d.ts +4 -2
  68. package/dist/ui/layout/status-layout.js +24 -1
  69. package/dist/ui/overlays/HelpOverlay.js +1 -2
  70. package/package.json +2 -2
  71. package/dist/ui/ModeChip.d.ts +0 -18
  72. package/dist/ui/ModeChip.js +0 -24
package/dist/cli.js CHANGED
@@ -46,6 +46,7 @@ import { createFrameDiffer } from './ui/frame-differ.js';
46
46
  import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
47
47
  import { setFrameStatsProvider } from './commands/perf.js';
48
48
  import { tryCreateStdinFilter } from './input/stdin-filter.js';
49
+ import { enableKeyboardEnhancement, NOOP_KEYBOARD_ENHANCEMENT, } from './input/keyboard-enhancement.js';
49
50
  import { createScrollbarBridge } from './ui/scrollbar-controller.js';
50
51
  import { createPointerRouter } from './input/pointer-router.js';
51
52
  import { createFrameObserver } from './ui/frame-observer.js';
@@ -120,6 +121,10 @@ function toFlags(opts) {
120
121
  // `CliFlags`, AND a line here; `config.test.ts` is the regression pin that
121
122
  // exists because `--no-mouse` shipped inert once already.
122
123
  paste: opts.paste,
124
+ // Site three of three for `--keyboard-enhancement` /
125
+ // `--no-keyboard-enhancement`, for the reason `paste` one line up
126
+ // records: the key is persisted and defaults to true.
127
+ keyboardEnhancement: opts.keyboardEnhancement,
123
128
  skills: opts.skills,
124
129
  skill: opts.skill,
125
130
  skillsYes: opts.skillsYes,
@@ -155,11 +160,6 @@ function toFlags(opts) {
155
160
  fastModel: opts.fastModel,
156
161
  fastProvider: opts.fastProvider,
157
162
  fastReview: opts.fastReview,
158
- // …and again for `--fast-delegate` / `--no-fast-delegate`, one feature
159
- // later — NINE features, one trap. It is `fast.delegate`'s only channel:
160
- // there is no `ARAGON_FAST_DELEGATE`, so missing this line leaves a wrapper
161
- // with no way at all to say "do not delegate on this run".
162
- fastDelegate: opts.fastDelegate,
163
163
  // `--verbose` is just a shorthand for `--log-level debug`; an explicit
164
164
  // `--log-level` still wins, so the two can be combined without surprise.
165
165
  logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
@@ -465,8 +465,22 @@ function runInteractive(flags, extras = {}) {
465
465
  // advising the user about a mode they just disabled.
466
466
  const wantPaste = config.paste && !!process.stdin.isTTY;
467
467
  const pasteBridge = { notify: null };
468
+ // --- Keyboard enhancement (Shift+Enter newline). --------------------------
469
+ //
470
+ // THE THIRD STREAM GATE, AND THE SAME DISCIPLINE AS THE OTHER TWO. On
471
+ // win32 the enhancement rides the SAME `vtInputSupported` measurement
472
+ // the mouse does: Node < 22.17 / < 24.2 never turns on
473
+ // `ENABLE_VIRTUAL_TERMINAL_INPUT`, the win32-input-mode records cannot
474
+ // reach the process, and pushing `?9001h` there would be a mode nobody
475
+ // can observe -- the exact gap the mouse gate closes for `?1000h`.
476
+ // Elsewhere the push is kitty disambiguate + modifyOtherKeys, which a
477
+ // terminal that implements neither ignores (verified byte-identical on
478
+ // conhost), so no capability gate is possible or needed there.
479
+ const wantEnhancedKeys = config.keyboardEnhancement &&
480
+ !!process.stdin.isTTY &&
481
+ (process.platform !== 'win32' || vtInputSupported);
468
482
  const filter = process.stdin.isTTY
469
- ? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
483
+ ? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste, enhancedKeys: wantEnhancedKeys }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
470
484
  : null;
471
485
  /**
472
486
  * THE ONE EXPRESSION EVERY MOUSE SITE READS (I-11 / D-17).
@@ -478,6 +492,8 @@ function runInteractive(flags, extras = {}) {
478
492
  const mouseOn = wantMouse && filter !== null;
479
493
  /** Same discipline for DEC 2004: already decided here, never re-derived. */
480
494
  const pasteOn = wantPaste && filter !== null;
495
+ /** Same discipline: the translation runs only when a filter fronts Ink. */
496
+ const enhancedKeysOn = wantEnhancedKeys && filter !== null;
481
497
  const disposeStdinFilter = () => filter?.dispose();
482
498
  // --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
483
499
  //
@@ -565,6 +581,16 @@ function runInteractive(flags, extras = {}) {
565
581
  ...(snapshot.droppedEntries ? { droppedEntries: snapshot.droppedEntries } : {}),
566
582
  }));
567
583
  };
584
+ // AFTER the filter, BEFORE the render: the enhanced encodings this mode
585
+ // turns on must never reach Ink un-translated, and the ordering invariant
586
+ // `stdin-filter.ts` states for `?1000h` / `?2004h` covers `?9001h`
587
+ // and the CSI-u pushes identically.
588
+ const keyboardEnhancement = enhancedKeysOn
589
+ ? enableKeyboardEnhancement(process.stdout, {
590
+ platform: process.platform,
591
+ vtInputSupported,
592
+ })
593
+ : NOOP_KEYBOARD_ENHANCEMENT;
568
594
  {
569
595
  // `mouse` here means "a mouse-parsing filter is installed", NOT "the user
570
596
  // wants mouse support" — see `AltScreenOptions`. `screen.ts` must never
@@ -581,7 +607,14 @@ function runInteractive(flags, extras = {}) {
581
607
  motion: mouseOn,
582
608
  bracketedPaste: pasteOn,
583
609
  });
584
- const restore = () => screen?.restore();
610
+ // Input-encoding modes unwind FIRST, for the same reason the mouse
611
+ // reporting does inside `screen.restore()`: the terminal must stop
612
+ // sending enhanced encodings before anything else tears the reader
613
+ // down, or a keystroke taken in that instant survives into the shell.
614
+ const restore = () => {
615
+ keyboardEnhancement.restore();
616
+ screen?.restore();
617
+ };
585
618
  // Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
586
619
  // - `waitUntilExit().then()` is a microtask and may never be reached when
587
620
  // the process is killed;
@@ -948,6 +981,9 @@ const CONFIG_SET_KEYS = new Set([
948
981
  // `mouse` records: a key present here but absent there falls through every
949
982
  // case, writes nothing, and still prints `Set paste = false`.
950
983
  'paste',
984
+ // Listed here AND cased in the switch below, for the reason the note above
985
+ // `paste` records.
986
+ 'keyboardEnhancement',
951
987
  'scrollResumeMs',
952
988
  'historyEnabled',
953
989
  'planModeDefault',
@@ -1187,6 +1223,9 @@ function runConfigSetValue(key, value) {
1187
1223
  case 'paste':
1188
1224
  patch.paste = value === 'true' || value === '1';
1189
1225
  break;
1226
+ case 'keyboardEnhancement':
1227
+ patch.keyboardEnhancement = value === 'true' || value === '1';
1228
+ break;
1190
1229
  // Clamped rather than rejected, for the same reason `planModeDefault` below
1191
1230
  // records: hardening only the READ path leaves a bad value on disk that
1192
1231
  // reverts to the default on every launch, which presents as "my setting
@@ -1451,6 +1490,9 @@ function buildProgram() {
1451
1490
  // silence, and silently overriding a stored `false` on every run.
1452
1491
  .option('--paste', 'Collapse large pastes into a placeholder (the default)')
1453
1492
  .option('--no-paste', 'Treat pasted bytes as keystrokes (pre-0.6.3 behavior)')
1493
+ // Same persisted-defaults-true trap as `--paste` above.
1494
+ .option('--keyboard-enhancement', 'Make Shift+Enter insert a newline (the default where the terminal can)')
1495
+ .option('--no-keyboard-enhancement', 'Never push keyboard-enhancement modes (Shift+Enter then needs a terminal binding)')
1454
1496
  .option('--no-exit-transcript', 'Do not replay the session summary after exiting')
1455
1497
  .option('--plan', 'Start the session in PLAN mode (read-only research + review)')
1456
1498
  .option('--no-plan', 'Start the session in BUILD mode (overrides planModeDefault)')
@@ -1490,14 +1532,6 @@ function buildProgram() {
1490
1532
  .option('--fast-model <id>', 'The fast model id (implies nothing about --fast)')
1491
1533
  .option('--fast-provider <id>', 'Provider for the fast model (default: the main provider)')
1492
1534
  .option('--fast-review <n|off>', 'Turns between automatic fast reviews, or "off"')
1493
- // BOTH FORMS, POSITIVE FIRST, and here it matters more than anywhere above:
1494
- // `fast.delegate` is PERSISTED and defaults to TRUE, so declaring only
1495
- // `--no-fast-delegate` would make commander default `opts.fastDelegate` to
1496
- // `true` and force `delegate: true` on EVERY run that passed no flag at all
1497
- // — silently overwriting a user's stored `false`. Same polarity, same trap
1498
- // and same fix as `--update` / `--compaction`.
1499
- .option('--fast-delegate', 'Let the fast tier take mechanical sub-steps (the default)')
1500
- .option('--no-fast-delegate', 'Run every sub-step on the main model')
1501
1535
  // BOTH FORMS, positive first, for the reason recorded seven times above —
1502
1536
  // and this is the pair whose omission would be worst. `update.mode` is
1503
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
  {
@@ -1054,55 +1040,53 @@ const COMMANDS = [
1054
1040
  },
1055
1041
  {
1056
1042
  name: 'terminal-setup',
1057
- description: 'How to make Shift+Enter send a newline in your terminal',
1043
+ description: 'How Shift+Enter sends a newline in your terminal',
1058
1044
  run: (ctx) => {
1059
- // READ-ONLY BY DESIGN (3.6): pushing kitty keyboard mode or
1060
- // modifyOtherKeys=2 would re-encode EVERY modified key as CSI-u and
1061
- // blind Ink's parser for the whole session. One user-side binding
1062
- // reaches the same result for the one key that needs it.
1063
- //
1064
- // Ctrl+J and Alt+Enter work with no setup at all; CSI-u terminals
1065
- // (kitty / WezTerm / foot with keyboard enhancement on) need none
1066
- // either. The lines below are for the terminals that send a plain
1067
- // CR for Shift+Enter and CAN be configured.
1045
+ // THE APP NOW PUSHES THE MODES ITSELF (shift-enter-newline fix):
1046
+ // win32-input-mode on Windows, kitty disambiguate + modifyOtherKeys=2
1047
+ // elsewhere, each translated back to legacy bytes before Ink parses
1048
+ // them. The lines below cover the terminals where pushing is not
1049
+ // enough: macOS Terminal.app and conhost under an old Node publish no
1050
+ // keyboard-enhancement protocol at all, and a terminal that ignores
1051
+ // the push still sends a plain CR for Shift+Enter.
1068
1052
  const lines = [
1069
1053
  'Shift+Enter newline - terminal setup',
1070
1054
  '',
1071
- 'Ctrl+J inserts a newline when delivered; Alt+Enter may be intercepted.',
1072
- 'To get a dedicated Shift+Enter key, make the terminal send',
1073
- 'the CSI-u sequence \\u001b[13;2u for it:',
1055
+ 'This app asks the terminal to report modified keys itself:',
1056
+ ' Windows: win32-input-mode (conhost and Windows Terminal)',
1057
+ ' macOS/Linux: kitty disambiguate + modifyOtherKeys (iTerm2,',
1058
+ ' kitty, ghostty, WezTerm, foot, Alacritty)',
1059
+ 'A terminal that implements none of these sends a plain CR for',
1060
+ 'Shift+Enter, indistinguishable from Enter. There Ctrl+J inserts a',
1061
+ 'newline with no setup, and Alt+Enter may also work.',
1062
+ '',
1063
+ 'If your terminal can be configured, make Shift+Enter send the',
1064
+ 'CSI-u sequence \\u001b[13;2u and it works even without the push:',
1074
1065
  '',
1075
1066
  ' Windows Terminal settings.json, merge root-level arrays:',
1076
1067
  ' "copyOnSelect": false,',
1077
1068
  ' "actions": [{ "id": "User.AragonNewline", "command":',
1078
1069
  ' { "action": "sendInput", "input": "\\u001b[13;2u" } }],',
1079
1070
  ' "keybindings": [{ "keys": "shift+enter", "id": "User.AragonNewline" }]',
1080
- ' PowerShell runs inside the terminal; configure this binding in',
1081
- ' Windows Terminal, not in the PowerShell profile. If both keys',
1082
- ' send CR, the application cannot distinguish Shift+Enter from Enter.',
1083
1071
  ' VS Code terminal keybindings.json:',
1084
1072
  ' { "key": "shift+enter",',
1085
1073
  ' "command": "workbench.action.terminal.sendSequence",',
1086
1074
  ' "when": "terminalFocus",',
1087
1075
  ' "args": { "text": "\\u001b[13;2u" } }',
1088
- ' User settings: "terminal.integrated.copyOnSelection": false',
1089
1076
  ' iTerm2 Settings > Profiles > Keys > Key Mappings:',
1090
1077
  ' Shift+Enter -> Send Escape Sequence: [13;2u',
1091
1078
  ' kitty/WezTerm/foot native once keyboard enhancement is on',
1092
1079
  ' (kitty: map shift+enter send_text all \\u001b[13;2u)',
1093
1080
  ' Alacritty alacritty.toml, [keyboard] bindings:',
1094
1081
  ' { key = "Enter", mods = "Shift", chars = "\\u001b[13;2u" }',
1095
- ' macOS Terminal.app / conhost cannot rebind Enter; use Ctrl+J',
1096
- ' or Alt+Enter',
1082
+ ' macOS Terminal.app / old conhost cannot rebind Enter; use Ctrl+J',
1097
1083
  '',
1084
+ 'Windows needs Node >= 22.17 (or >= 24.2): older versions never',
1085
+ 'turn on virtual-terminal input, so no report can arrive.',
1086
+ 'Opt out of the push entirely:',
1087
+ ' aragon config set keyboardEnhancement false',
1088
+ ' --no-keyboard-enhancement / ARAGON_KEYBOARD_ENHANCEMENT=0',
1098
1089
  'This command only prints advice; it changes no terminal modes.',
1099
- 'Host selections (Shift+drag or /mouse off) belong to the terminal.',
1100
- 'App selections wait for Ctrl+C; only a native tool exit confirms copying.',
1101
- 'SSH uses OSC 52: the terminal may deny it; sent does not mean confirmed.',
1102
- 'Without the stdin filter, Delete falls back to Backspace.',
1103
- 'Without paste support, multiline paste cannot be guaranteed safe.',
1104
- 'IME confirmation has no composition signal in PTY bytes. If it emits CR,',
1105
- 'edit externally and use bracketed paste; this host combination needs validation.',
1106
1090
  ];
1107
1091
  ctx.notify('info', lines.join('\n'));
1108
1092
  },
@@ -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.
@@ -74,7 +74,9 @@ function occupancyLine(usage) {
74
74
  return `${pct} ${formatTokens(usage.occupied)} of ${formatTokens(usage.window)} ${breakdown}`;
75
75
  }
76
76
  /**
77
- * Where the denominator came from. THREE SOURCES, ALL NAMED.
77
+ * Where the denominator came from. FOUR SOURCES, ALL NAMED - the fourth
78
+ * being the user's own model-windows.json, which exists precisely because
79
+ * the other three cannot know a gateway alias released last week.
78
80
  *
79
81
  * THE PLACEHOLDER BRANCH IS THE ONE THAT MATTERS. `buildRuntimeModel` returns a
80
82
  * flat 128000 for any model the static table has never seen - a custom
@@ -85,11 +87,20 @@ function occupancyLine(usage) {
85
87
  */
86
88
  function windowLine(usage) {
87
89
  if (usage.windowOverridden) {
88
- return `${usage.window} from contextWindow configuration (overrides API and model table)`;
90
+ return `${usage.window} from contextWindow configuration (overrides API, model table, and model-windows.json)`;
89
91
  }
90
92
  if (!usage.windowKnown) {
91
93
  return (`UNKNOWN - no verified model limit; internal fallback budget ${usage.window}; ` +
92
- 'set contextWindow to correct it');
94
+ 'set contextWindow or list the model in model-windows.json to correct it');
95
+ }
96
+ if (usage.windowSource === 'user') {
97
+ return `${usage.window} from model-windows.json (your per-model table)`;
98
+ }
99
+ if (usage.windowSource === undefined) {
100
+ // A child agent can be windowKnown through the pricing-table heuristic
101
+ // without naming a source; claiming 'the model table' there would be an
102
+ // evidence-free assertion in the one report that exists to be checked.
103
+ return `${usage.window} source unverified`;
93
104
  }
94
105
  return `${usage.window} from ${usage.windowSource === 'api' ? 'the model API' : 'the model table'}`;
95
106
  }
@@ -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
  };
@@ -65,7 +65,12 @@ export interface CompactionDecision {
65
65
  deltaTokens: number;
66
66
  }
67
67
  export interface Pressure {
68
- windowSource?: 'api' | 'catalog' | 'fallback';
68
+ /**
69
+ * `user` means the CLI's model-windows.json supplied the denominator - a
70
+ * hand-typed window for a model no table knows. It outranks `api` and
71
+ * `catalog` in `getModelInfoFor` precisely because the user asserted it.
72
+ */
73
+ windowSource?: 'api' | 'catalog' | 'fallback' | 'user';
69
74
  occupied: number;
70
75
  contextWindow: number;
71
76
  /** `occupied / contextWindow`, clamped to [0, 1]. */
@@ -12,6 +12,7 @@
12
12
  * ~/.aragon-agent/
13
13
  * ├── config.json persisted config (0600 on POSIX)
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
+ * ├── model-windows.json per-model context windows, user-edited
15
16
  * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
17
  * ├── state.json UI bookkeeping scalars (0600 on POSIX)
17
18
  * ├── update-state.json auto-update coordination (0600 on POSIX)
@@ -72,6 +73,15 @@ export declare function getHomeRoot(): string;
72
73
  export declare function getConfigPath(): string;
73
74
  /** Written by `aragon config edit` before spawning the editor. */
74
75
  export declare function getConfigBackupPath(): string;
76
+ /**
77
+ * `<home>/model-windows.json` — per-model context windows the user declares by
78
+ * hand for ids no table knows (`kimi-k3`, a gateway alias, a self-hosted model).
79
+ *
80
+ * READ-ONLY from the application's side: nothing ever writes or creates it, so
81
+ * a user's file cannot be clobbered by an upgrade and its absence is not an
82
+ * error. `config/model-windows.ts` owns the format and the lookup.
83
+ */
84
+ export declare function getModelWindowsPath(): string;
75
85
  /**
76
86
  * `<home>/prompt-history.jsonl` — the prompts the user submitted, one JSON
77
87
  * object per line. Deliberately NOT under `logs/`: that directory is governed
@@ -12,6 +12,7 @@
12
12
  * ~/.aragon-agent/
13
13
  * ├── config.json persisted config (0600 on POSIX)
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
+ * ├── model-windows.json per-model context windows, user-edited
15
16
  * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
17
  * ├── state.json UI bookkeeping scalars (0600 on POSIX)
17
18
  * ├── update-state.json auto-update coordination (0600 on POSIX)
@@ -129,6 +130,17 @@ export function getConfigPath() {
129
130
  export function getConfigBackupPath() {
130
131
  return join(resolution.root, 'config.json.bak');
131
132
  }
133
+ /**
134
+ * `<home>/model-windows.json` — per-model context windows the user declares by
135
+ * hand for ids no table knows (`kimi-k3`, a gateway alias, a self-hosted model).
136
+ *
137
+ * READ-ONLY from the application's side: nothing ever writes or creates it, so
138
+ * a user's file cannot be clobbered by an upgrade and its absence is not an
139
+ * error. `config/model-windows.ts` owns the format and the lookup.
140
+ */
141
+ export function getModelWindowsPath() {
142
+ return join(resolution.root, 'model-windows.json');
143
+ }
132
144
  /**
133
145
  * `<home>/prompt-history.jsonl` — the prompts the user submitted, one JSON
134
146
  * object per line. Deliberately NOT under `logs/`: that directory is governed
@@ -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).
@@ -14,7 +14,7 @@ import { getConfigBackupPath, getConfigPath, getHomeRoot, getLogsDir, getPromptH
14
14
  import { getHomeMigrationBreadcrumbPath } from './migrate-home.js';
15
15
  import { loadPersistedConfig, updatePersistedConfig } from './store.js';
16
16
  import { normalizeModelProfiles, validateModelProfiles } from './model-profiles.js';
17
- import { clampFastConfig, clampRetryConfig, clampCompactionConfig, parseThresholdInput, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_COMPACTION_CONFIG, DEFAULT_UPDATE_CONFIG, } from './schema.js';
17
+ import { clampFastConfig, clampRetryConfig, clampCompactionConfig, clampTeamConfig, parseThresholdInput, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_COMPACTION_CONFIG, DEFAULT_UPDATE_CONFIG, } from './schema.js';
18
18
  import { clampLogLevel } from '../logging/levels.js';
19
19
  /** Config keys under `log.` that `aragon config set` accepts. */
20
20
  export const LOG_CONFIG_SET_KEYS = [
@@ -81,7 +81,6 @@ export const FAST_CONFIG_SET_KEYS = [
81
81
  'fast.model',
82
82
  'fast.baseUrl',
83
83
  'fast.thinkingLevel',
84
- 'fast.delegate',
85
84
  'fast.review',
86
85
  'fast.reviewEveryTurns',
87
86
  'fast.reviewContextTurns',
@@ -177,25 +176,29 @@ export function applyLogConfigSet(key, value) {
177
176
  */
178
177
  export function applyTeamConfigSet(key, value) {
179
178
  const patch = (team) => ({ team });
179
+ // EVERY NUMERIC KEY ROUTES THROUGH `clampTeamConfig`, not through
180
+ // `coercePositiveInt`, for the reason `applyRetryConfigSet` records one block
181
+ // below: `coercePositiveInt` returns its fallback for `n <= 0`, so
182
+ // `config set team.subagentTimeoutMs 0` would write 300000 (the old default)
183
+ // instead of the documented NO-LIMIT value, silently. The caller echoes the
184
+ // STORED value, which is what makes an out-of-range input visible.
185
+ const clampOne = (field) => {
186
+ const merged = clampTeamConfig({ ...DEFAULT_TEAM_CONFIG, [field]: value });
187
+ return { [field]: merged[field] };
188
+ };
180
189
  switch (key) {
181
190
  case 'team.enabled':
182
191
  return patch({ enabled: isTrue(value) });
183
192
  case 'team.maxSubagents':
184
- return patch({ maxSubagents: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.maxSubagents) });
193
+ return patch(clampOne('maxSubagents'));
185
194
  case 'team.maxConcurrent':
186
- return patch({ maxConcurrent: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.maxConcurrent) });
195
+ return patch(clampOne('maxConcurrent'));
187
196
  case 'team.subagentTimeoutMs':
188
- return patch({
189
- subagentTimeoutMs: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.subagentTimeoutMs),
190
- });
197
+ return patch(clampOne('subagentTimeoutMs'));
191
198
  case 'team.dispatchTimeoutMs':
192
- return patch({
193
- dispatchTimeoutMs: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.dispatchTimeoutMs),
194
- });
199
+ return patch(clampOne('dispatchTimeoutMs'));
195
200
  case 'team.maxTurnsPerSubagent':
196
- return patch({
197
- maxTurnsPerSubagent: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent),
198
- });
201
+ return patch(clampOne('maxTurnsPerSubagent'));
199
202
  default:
200
203
  return null;
201
204
  }
@@ -273,8 +276,6 @@ export function applyFastConfigSet(key, value) {
273
276
  return patch(clampOne('baseUrl'));
274
277
  case 'fast.thinkingLevel':
275
278
  return patch(clampOne('thinkingLevel'));
276
- case 'fast.delegate':
277
- return patch({ delegate: isTrue(value) });
278
279
  case 'fast.review':
279
280
  return patch({ review: isTrue(value) });
280
281
  case 'fast.reviewEveryTurns':
@@ -7,7 +7,8 @@
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
8
  * ARAGON_SHOW_THINKING,
9
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
10
- * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_KEYBOARD_ENHANCEMENT,
11
+ * ARAGON_PLAN, ARAGON_TEAM,
11
12
  * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
13
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
14
  * ARAGON_LOG_LEVEL,
@@ -7,7 +7,8 @@
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
8
  * ARAGON_SHOW_THINKING,
9
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
10
- * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_KEYBOARD_ENHANCEMENT,
11
+ * ARAGON_PLAN, ARAGON_TEAM,
11
12
  * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
13
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
14
  * ARAGON_LOG_LEVEL,
@@ -151,6 +152,17 @@ export function readEnvConfig() {
151
152
  if (paste !== undefined && paste.length > 0) {
152
153
  partial.paste = paste === '1' || paste === 'true' || paste === 'on' || paste === 'yes';
153
154
  }
155
+ // ARAGON_KEYBOARD_ENHANCEMENT=0 stops the Shift+Enter mode push and its
156
+ // translation -- the one-env revert to pre-enhancement input handling.
157
+ // Parsed by the POSITIVE list, exactly as its three neighbours above are.
158
+ const keyboardEnhancement = process.env.ARAGON_KEYBOARD_ENHANCEMENT?.trim().toLowerCase();
159
+ if (keyboardEnhancement !== undefined && keyboardEnhancement.length > 0) {
160
+ partial.keyboardEnhancement =
161
+ keyboardEnhancement === '1' ||
162
+ keyboardEnhancement === 'true' ||
163
+ keyboardEnhancement === 'on' ||
164
+ keyboardEnhancement === 'yes';
165
+ }
154
166
  // ARAGON_SCROLL_RESUME_MS=<n> — idle delay before a paused viewport returns to
155
167
  // the newest line; `0` disables auto-resume. THE ONLY CHANNEL BESIDES THE
156
168
  // CONFIG FILE, because §6.1 gives this key no CLI flag on purpose. `load.ts`