@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
@@ -59,6 +59,15 @@ export interface CliFlags {
59
59
  * `false` on every run that passed no flag at all.
60
60
  */
61
61
  paste?: boolean;
62
+ /**
63
+ * `--keyboard-enhancement` ⇒ true, `--no-keyboard-enhancement` ⇒ false,
64
+ * absent ⇒ undefined.
65
+ *
66
+ * DECLARED AS A PAIR and carried through `toFlags`, for the reason the
67
+ * `paste` note above records: the key is persisted and defaults to
68
+ * `true`, so a lone negative flag must not read as silence.
69
+ */
70
+ keyboardEnhancement?: boolean;
62
71
  /** `--no-skills` ⇒ false. Turns the whole skill subsystem off (§7.5). */
63
72
  skills?: boolean;
64
73
  /** `--skill <name>` ×N — force Level 2 injection for this run. */
@@ -184,23 +193,6 @@ export interface CliFlags {
184
193
  * to keep in step with `toFlags`.
185
194
  */
186
195
  fastReview?: string;
187
- /**
188
- * `--fast-delegate` ⇒ true, `--no-fast-delegate` ⇒ false, absent ⇒ undefined
189
- * (web-use-tier-cooperation-and-control-closure §4.1.5).
190
- *
191
- * DECLARED AS A PAIR — MANDATORY, for the reason `--team`, `--todo-panel`,
192
- * `--render-governor`, `--retry` and `--fast` each record in turn, and here
193
- * with the SAME polarity as `--update`: `fast.delegate` is PERSISTED and
194
- * defaults to `true`, so a lone `--no-fast-delegate` would make commander
195
- * default `opts.fastDelegate` to `true` and silently overwrite a stored
196
- * `false` on every run that passed no flag at all.
197
- *
198
- * It exists because it is the ONLY `fast.*` sub-switch with no channel a
199
- * wrapper can reach: `--fast-review <n|off>` already covers `review` and
200
- * `reviewEveryTurns`, and `enabled` / `model` / `provider` have both a flag
201
- * and an environment variable.
202
- */
203
- fastDelegate?: boolean;
204
196
  /**
205
197
  * `--update` ⇒ true, `--no-update` ⇒ false, absent ⇒ undefined
206
198
  * (cli-auto-update §4.2).
@@ -78,6 +78,19 @@ function resolvePaste(flags, env, file) {
78
78
  return file.paste;
79
79
  return DEFAULT_CONFIG.paste;
80
80
  }
81
+ /**
82
+ * Keyboard enhancement: flag > env > file > default `true`. Shaped exactly
83
+ * like `resolvePaste` above, for the reason its own note gives.
84
+ */
85
+ function resolveKeyboardEnhancement(flags, env, file) {
86
+ if (flags.keyboardEnhancement !== undefined)
87
+ return flags.keyboardEnhancement;
88
+ if (env.keyboardEnhancement !== undefined)
89
+ return env.keyboardEnhancement;
90
+ if (file.keyboardEnhancement !== undefined)
91
+ return file.keyboardEnhancement;
92
+ return DEFAULT_CONFIG.keyboardEnhancement;
93
+ }
81
94
  /**
82
95
  * Idle scroll-resume delay: env › file › default 5000, clamped to [0, 120000].
83
96
  *
@@ -230,12 +243,6 @@ function resolveFastConfig(flags, env, file) {
230
243
  ...(flags.fastProvider !== undefined && flags.fastProvider !== ''
231
244
  ? { provider: flags.fastProvider }
232
245
  : {}),
233
- // `!== undefined`, for the reason stated on `flags.fast` above: commander
234
- // materialises a lone `--no-fast-delegate` as `opts.fastDelegate = true`
235
- // when the flag is absent, and `delegate` defaults to TRUE — so a truthiness
236
- // check would force it on for every run that passed no flag at all,
237
- // overwriting a stored `false` with nothing to show for it.
238
- ...(flags.fastDelegate !== undefined ? { delegate: flags.fastDelegate } : {}),
239
246
  ...reviewPatch,
240
247
  });
241
248
  }
@@ -585,6 +592,7 @@ export function loadConfig(flags = {}, options = {}) {
585
592
  mouse: resolveMouse(flags, env.partial, file),
586
593
  mouseSelect: resolveMouseSelect(flags, env.partial, file),
587
594
  paste: resolvePaste(flags, env.partial, file),
595
+ keyboardEnhancement: resolveKeyboardEnhancement(flags, env.partial, file),
588
596
  scrollResumeMs: resolveScrollResumeMs(env.partial, file),
589
597
  // From `<home>/state.json`, not the config file — see `ui-state.ts`.
590
598
  submitCount: readSubmitCount(),
@@ -0,0 +1,91 @@
1
+ /**
2
+ * User-declared context windows, read from `~/.aragon-agent/model-windows.json`
3
+ * (path owned by `app-paths.ts`).
4
+ *
5
+ * WHY THIS FILE EXISTS. `ModelRegistry.getContextWindow` resolves a window from
6
+ * API discovery, the builtin model lists and the ten-entry catalog - and for
7
+ * every other id (a gateway alias, a self-hosted model, a release too new for
8
+ * the table) it returns the explicitly-untrusted 128k `fallback`. The gauge
9
+ * then renders `?` forever, by design: it refuses to divide by a guessed
10
+ * denominator. The one number that can make the denominator real is the user's
11
+ * own, and `config.json`'s `contextWindow` key can only hold ONE of it. This
12
+ * file is the per-model form of the same assertion.
13
+ *
14
+ * THE APPLICATION NEVER WRITES IT. A file the user owns by hand cannot be
15
+ * clobbered by an upgrade, and its absence is the ordinary case, not an error.
16
+ *
17
+ * ASCII ONLY: `src/config/**` is inside the glyph scanner's scope
18
+ * (`glyphs.test.ts::inScope`).
19
+ */
20
+ /** Structural bounds only; there is no policy surface to tune here. */
21
+ export interface ModelWindowsLimits {
22
+ /** Entries beyond this many are dropped, in file order. */
23
+ readonly maxEntries: number;
24
+ /** Keys longer than this are dropped rather than trusted as ids. */
25
+ readonly maxKeyLength: number;
26
+ /** A window below the smallest real context (1k) is a typo, not a limit. */
27
+ readonly minWindow: number;
28
+ /** A window above 1e9 tokens is also a typo; no model ships that today. */
29
+ readonly maxWindow: number;
30
+ }
31
+ export declare const MODEL_WINDOWS_LIMITS: ModelWindowsLimits;
32
+ /** Normalized model id -> declared window. Keys are stored ALREADY normalized. */
33
+ export type ModelWindowTable = ReadonlyMap<string, number>;
34
+ /**
35
+ * Same normalization core's `catalogModelId` applies, restated here because it
36
+ * is not part of Core's public surface and a second exporter would be a
37
+ * dependency on an internal. Keep the two in step: a date-stamped id
38
+ * (`kimi-k3-20260716`) and its bare form must resolve to one entry.
39
+ */
40
+ export declare function normalizeModelKey(modelId: string): string;
41
+ /** What a parse produced, and whether the FILE itself was at fault. */
42
+ export interface ModelWindowsParse {
43
+ table: ModelWindowTable;
44
+ /**
45
+ * True only when the text could not be understood at all (bad JSON, wrong
46
+ * top-level type). A valid file with zero USABLE entries is `false`: the
47
+ * user wrote JSON, the values were the problem, and every dead value was
48
+ * already dropped rather than trusted.
49
+ */
50
+ malformed: boolean;
51
+ }
52
+ /**
53
+ * Parse already-decoded text into a table. NEVER THROWS - a hand-edited file
54
+ * is the rule here, not the exception, and a typo must cost the user one dead
55
+ * entry, not the session. Two accepted shapes:
56
+ *
57
+ * { "version": 1, "windows": { "kimi-k3": 1048576 } }
58
+ * { "kimi-k3": 1048576 } (bare map, same effect)
59
+ *
60
+ * The wrapper exists so the format can grow fields without a migration; the
61
+ * bare map is accepted because the first shape a user types is the simple one.
62
+ * A leading UTF-8 BOM is tolerated: `readFileSync(..., 'utf8')` does not strip
63
+ * it, `JSON.parse` cannot parse it, and Notepad writes it by default.
64
+ */
65
+ export declare function parseModelWindowsResult(text: string): ModelWindowsParse;
66
+ /** The table alone, for callers that do not care why it might be empty. */
67
+ export declare function parseModelWindows(text: string): ModelWindowTable;
68
+ /**
69
+ * mtime-cached lookup over the file.
70
+ *
71
+ * WHY A STAT PER LOOKUP RATHER THAN A LOAD AT STARTUP: `getModelInfoFor` is
72
+ * called on every measurement, and the meter's `refreshValidity` re-reads the
73
+ * window precisely so a moved denominator (a `/model` switch, a live config
74
+ * edit) re-measures without a relaunch. A user saving this file mid-session
75
+ * gets the same treatment - `statSync` is microseconds, and the read itself
76
+ * only happens when the stamp actually changes.
77
+ *
78
+ * THE STAMP IS mtimeMs+size, so a rewrite that lands in the SAME millisecond
79
+ * with the SAME byte count serves the previous table once. Squeezing that
80
+ * window shut would need a content hash per lookup, which is exactly the cost
81
+ * the stamp exists to avoid; the next differing write clears it regardless.
82
+ */
83
+ export declare class ModelWindows {
84
+ private readonly path;
85
+ private table;
86
+ private stamp;
87
+ constructor(path?: string);
88
+ /** The declared window for `modelId`, or `undefined` when nobody declared one. */
89
+ lookup(modelId: string): number | undefined;
90
+ private refresh;
91
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * User-declared context windows, read from `~/.aragon-agent/model-windows.json`
3
+ * (path owned by `app-paths.ts`).
4
+ *
5
+ * WHY THIS FILE EXISTS. `ModelRegistry.getContextWindow` resolves a window from
6
+ * API discovery, the builtin model lists and the ten-entry catalog - and for
7
+ * every other id (a gateway alias, a self-hosted model, a release too new for
8
+ * the table) it returns the explicitly-untrusted 128k `fallback`. The gauge
9
+ * then renders `?` forever, by design: it refuses to divide by a guessed
10
+ * denominator. The one number that can make the denominator real is the user's
11
+ * own, and `config.json`'s `contextWindow` key can only hold ONE of it. This
12
+ * file is the per-model form of the same assertion.
13
+ *
14
+ * THE APPLICATION NEVER WRITES IT. A file the user owns by hand cannot be
15
+ * clobbered by an upgrade, and its absence is the ordinary case, not an error.
16
+ *
17
+ * ASCII ONLY: `src/config/**` is inside the glyph scanner's scope
18
+ * (`glyphs.test.ts::inScope`).
19
+ */
20
+ import { readFileSync, statSync } from 'node:fs';
21
+ import { getLogger } from '../logging/logger.js';
22
+ import { getModelWindowsPath } from './app-paths.js';
23
+ export const MODEL_WINDOWS_LIMITS = {
24
+ maxEntries: 512,
25
+ maxKeyLength: 200,
26
+ minWindow: 1_024,
27
+ maxWindow: 1_000_000_000,
28
+ };
29
+ /**
30
+ * Same normalization core's `catalogModelId` applies, restated here because it
31
+ * is not part of Core's public surface and a second exporter would be a
32
+ * dependency on an internal. Keep the two in step: a date-stamped id
33
+ * (`kimi-k3-20260716`) and its bare form must resolve to one entry.
34
+ */
35
+ export function normalizeModelKey(modelId) {
36
+ return modelId.trim().toLowerCase()
37
+ .replace(/^(models|openai|anthropic|google)\//, '')
38
+ .replace(/-(?:\d{4}-\d{2}-\d{2}|\d{8})$/, '');
39
+ }
40
+ function isWindowValue(value) {
41
+ return typeof value === 'number' && Number.isSafeInteger(value) &&
42
+ value >= MODEL_WINDOWS_LIMITS.minWindow && value <= MODEL_WINDOWS_LIMITS.maxWindow;
43
+ }
44
+ /**
45
+ * Parse already-decoded text into a table. NEVER THROWS - a hand-edited file
46
+ * is the rule here, not the exception, and a typo must cost the user one dead
47
+ * entry, not the session. Two accepted shapes:
48
+ *
49
+ * { "version": 1, "windows": { "kimi-k3": 1048576 } }
50
+ * { "kimi-k3": 1048576 } (bare map, same effect)
51
+ *
52
+ * The wrapper exists so the format can grow fields without a migration; the
53
+ * bare map is accepted because the first shape a user types is the simple one.
54
+ * A leading UTF-8 BOM is tolerated: `readFileSync(..., 'utf8')` does not strip
55
+ * it, `JSON.parse` cannot parse it, and Notepad writes it by default.
56
+ */
57
+ export function parseModelWindowsResult(text) {
58
+ const body = text.replace(/^\uFEFF/, '');
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(body);
62
+ }
63
+ catch {
64
+ return { table: new Map(), malformed: true };
65
+ }
66
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
67
+ return { table: new Map(), malformed: true };
68
+ }
69
+ const record = parsed;
70
+ const source = record.windows !== undefined && record.windows !== null &&
71
+ typeof record.windows === 'object' && !Array.isArray(record.windows)
72
+ ? record.windows
73
+ : record;
74
+ const table = new Map();
75
+ for (const [rawKey, rawValue] of Object.entries(source)) {
76
+ const key = normalizeModelKey(rawKey);
77
+ if (key.length === 0 || key.length > MODEL_WINDOWS_LIMITS.maxKeyLength)
78
+ continue;
79
+ if (!isWindowValue(rawValue))
80
+ continue;
81
+ table.set(key, rawValue);
82
+ if (table.size >= MODEL_WINDOWS_LIMITS.maxEntries)
83
+ break;
84
+ }
85
+ return { table, malformed: false };
86
+ }
87
+ /** The table alone, for callers that do not care why it might be empty. */
88
+ export function parseModelWindows(text) {
89
+ return parseModelWindowsResult(text).table;
90
+ }
91
+ /**
92
+ * Decode the raw bytes the way a Windows text editor may have saved them.
93
+ *
94
+ * `readFileSync(path, 'utf8')` alone fails THREE ways on this machine's most
95
+ * common editors: a UTF-8 BOM survives into the string and kills `JSON.parse`,
96
+ * and Notepad's "Unicode" saves are UTF-16LE (or BE), which decode as NUL-laden
97
+ * mojibake. All three are silent feature-death for exactly the user who just
98
+ * tried to configure it, so the sniff happens before any parse.
99
+ */
100
+ function decodeModelWindowsFile(bytes) {
101
+ if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
102
+ return bytes.subarray(2).toString('utf16le');
103
+ }
104
+ if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
105
+ // Big-endian UTF-16: Node has no utf16be decoder, so swap pairs in place
106
+ // on a COPY and reuse the little-endian decoder.
107
+ const swapped = Buffer.from(bytes.subarray(2));
108
+ for (let i = 0; i + 1 < swapped.length; i += 2) {
109
+ const high = swapped[i];
110
+ swapped[i] = swapped[i + 1];
111
+ swapped[i + 1] = high;
112
+ }
113
+ return swapped.toString('utf16le');
114
+ }
115
+ if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
116
+ return bytes.subarray(3).toString('utf8');
117
+ }
118
+ return bytes.toString('utf8');
119
+ }
120
+ /**
121
+ * mtime-cached lookup over the file.
122
+ *
123
+ * WHY A STAT PER LOOKUP RATHER THAN A LOAD AT STARTUP: `getModelInfoFor` is
124
+ * called on every measurement, and the meter's `refreshValidity` re-reads the
125
+ * window precisely so a moved denominator (a `/model` switch, a live config
126
+ * edit) re-measures without a relaunch. A user saving this file mid-session
127
+ * gets the same treatment - `statSync` is microseconds, and the read itself
128
+ * only happens when the stamp actually changes.
129
+ *
130
+ * THE STAMP IS mtimeMs+size, so a rewrite that lands in the SAME millisecond
131
+ * with the SAME byte count serves the previous table once. Squeezing that
132
+ * window shut would need a content hash per lookup, which is exactly the cost
133
+ * the stamp exists to avoid; the next differing write clears it regardless.
134
+ */
135
+ export class ModelWindows {
136
+ path;
137
+ table = new Map();
138
+ stamp = null;
139
+ constructor(path = getModelWindowsPath()) {
140
+ this.path = path;
141
+ }
142
+ /** The declared window for `modelId`, or `undefined` when nobody declared one. */
143
+ lookup(modelId) {
144
+ this.refresh();
145
+ return this.table.get(normalizeModelKey(modelId));
146
+ }
147
+ refresh() {
148
+ let stamp;
149
+ try {
150
+ const stats = statSync(this.path);
151
+ stamp = { mtimeMs: stats.mtimeMs, size: stats.size };
152
+ }
153
+ catch (error) {
154
+ // A deleted file (ENOENT) is a decision: drop the entries. Any OTHER
155
+ // failure (EACCES, an antivirus lock) is transient, and blanking every
156
+ // declared window because a scanner held the file open for 50 ms would
157
+ // swing the gauge to `?` for a reason the user cannot see - keep the
158
+ // last good table and try again on the next lookup.
159
+ if (error.code === 'ENOENT') {
160
+ this.stamp = null;
161
+ this.table = new Map();
162
+ }
163
+ return;
164
+ }
165
+ if (stamp.mtimeMs === this.stamp?.mtimeMs && stamp.size === this.stamp.size)
166
+ return;
167
+ let bytes;
168
+ try {
169
+ bytes = readFileSync(this.path);
170
+ }
171
+ catch {
172
+ return; // Appeared in the stat, gone in the read: try again next lookup.
173
+ }
174
+ const result = parseModelWindowsResult(decodeModelWindowsFile(bytes));
175
+ if (result.malformed) {
176
+ getLogger().warn('config', 'model-windows.json could not be parsed; ignoring it until it is fixed', { path: this.path });
177
+ }
178
+ this.table = result.table;
179
+ this.stamp = stamp;
180
+ }
181
+ }
@@ -303,11 +303,25 @@ export interface TeamConfig {
303
303
  * failed subagent.
304
304
  */
305
305
  maxConcurrent: number;
306
- /** One child's wall clock before its own abort. */
306
+ /**
307
+ * One child's wall clock before its own abort. `0` (the default) means NO
308
+ * ceiling: a subagent runs until it finishes or is aborted, exactly like
309
+ * the lead, whose only in-run ceiling is the event-silence idle watchdog
310
+ * a child already keeps. A wedged child still dies on its OWN watchdog.
311
+ */
307
312
  subagentTimeoutMs: number;
308
- /** The whole dispatch's wall clock. Also the `task` tool-timeout override. */
313
+ /**
314
+ * The whole dispatch's wall clock, and the `task` tool-timeout override.
315
+ * `0` (the default) means NO ceiling: the executor override of `0` arms no
316
+ * timer (see Core `ToolExecutor`), so the fan-out lasts as long as its
317
+ * slowest child and only an Esc ends it early.
318
+ */
309
319
  dispatchTimeoutMs: number;
310
- /** Runaway-loop cap: turns one child may take before it is stopped. */
320
+ /**
321
+ * Runaway-loop cap: turns one child may take before it is stopped.
322
+ * `0` (the default) means NO cap - the same exposure the lead has, and
323
+ * the deliberate trade of "subagents as capable as the main agent".
324
+ */
311
325
  maxTurnsPerSubagent: number;
312
326
  }
313
327
  /**
@@ -458,8 +472,6 @@ export interface FastConfig {
458
472
  /** Applied to fast-tier children and to the review call. Defaults to `'off'`:
459
473
  * a "fast" model asked to think for 32 768 tokens is not fast (D-13). */
460
474
  thinkingLevel: ThinkingLevel;
461
- /** Allow `model: "fast"` on `task`. */
462
- delegate: boolean;
463
475
  /** Run the periodic asynchronous review. */
464
476
  review: boolean;
465
477
  /** Completed turns between reviews. */
@@ -786,6 +798,14 @@ export interface PersistedConfig {
786
798
  * asked for; a kill switch is what a bug report needs.
787
799
  */
788
800
  paste: boolean;
801
+ /**
802
+ * Ask the terminal to report modified keys distinctly so Shift+Enter can
803
+ * insert a newline (win32-input-mode on Windows, kitty disambiguate +
804
+ * modifyOtherKeys elsewhere). A KILL SWITCH in the exact sense `paste`
805
+ * above records: the encoding tables are structural and live in
806
+ * `input/win32-input-mode.ts` / `input/csiu-keys.ts`.
807
+ */
808
+ keyboardEnhancement: boolean;
789
809
  /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
790
810
  scrollResumeMs: number;
791
811
  /** Start every session in PLAN mode (plan-mode §4.4). */
@@ -971,6 +991,15 @@ export interface CliConfig {
971
991
  * reach none of them.
972
992
  */
973
993
  paste: boolean;
994
+ /**
995
+ * Whether this session may push the keyboard-enhancement modes.
996
+ *
997
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
998
+ * `paste` above records: `runInteractive` reads it -- where `config`
999
+ * is a `CliConfig` -- to decide the filter's `enhancedKeys` feature
1000
+ * and the `enableKeyboardEnhancement` push.
1001
+ */
1002
+ keyboardEnhancement: boolean;
974
1003
  /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
975
1004
  scrollResumeMs: number;
976
1005
  /**
@@ -447,15 +447,20 @@ export const DEFAULT_TEAM_CONFIG = {
447
447
  enabled: true,
448
448
  maxSubagents: 5,
449
449
  maxConcurrent: 3,
450
- subagentTimeoutMs: 300_000,
451
- dispatchTimeoutMs: 900_000,
452
- maxTurnsPerSubagent: 24,
450
+ // 0 = NO LIMIT. Subagents run under the same regime as the lead: no
451
+ // wall clock, no turn cap; only the idle watchdog and the user's Esc.
452
+ subagentTimeoutMs: 0,
453
+ dispatchTimeoutMs: 0,
454
+ maxTurnsPerSubagent: 0,
453
455
  };
454
456
  const SUBAGENTS_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
455
457
  const CONCURRENT_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
456
- const SUBAGENT_TIMEOUT_RANGE = { min: 30_000, max: 1_800_000 };
457
- const DISPATCH_TIMEOUT_RANGE = { min: 60_000, max: 3_600_000 };
458
- const TURNS_RANGE = { min: 4, max: 100 };
458
+ // `min: 0` is the documented OFF SWITCH for each bound, so these MUST use
459
+ // `clampIntAllowingZero`: plain `clampInt` folds `0` back to the fallback
460
+ // ("my setting won't stick"), the exact trap `scrollResumeMs` records above.
461
+ const SUBAGENT_TIMEOUT_RANGE = { min: 0, max: 1_800_000 };
462
+ const DISPATCH_TIMEOUT_RANGE = { min: 0, max: 3_600_000 };
463
+ const TURNS_RANGE = { min: 0, max: 100 };
459
464
  /**
460
465
  * THE single gate for every team-config read AND write, mirroring
461
466
  * `clampSkillsConfig` / `clampLogConfig` for the reason those two record:
@@ -474,9 +479,9 @@ export function clampTeamConfig(raw) {
474
479
  enabled: bool(src.enabled, DEFAULT_TEAM_CONFIG.enabled),
475
480
  maxSubagents,
476
481
  maxConcurrent: Math.min(maxSubagents, clampInt(src.maxConcurrent, DEFAULT_TEAM_CONFIG.maxConcurrent, CONCURRENT_RANGE)),
477
- subagentTimeoutMs: clampInt(src.subagentTimeoutMs, DEFAULT_TEAM_CONFIG.subagentTimeoutMs, SUBAGENT_TIMEOUT_RANGE),
478
- dispatchTimeoutMs: clampInt(src.dispatchTimeoutMs, DEFAULT_TEAM_CONFIG.dispatchTimeoutMs, DISPATCH_TIMEOUT_RANGE),
479
- maxTurnsPerSubagent: clampInt(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
482
+ subagentTimeoutMs: clampIntAllowingZero(src.subagentTimeoutMs, DEFAULT_TEAM_CONFIG.subagentTimeoutMs, SUBAGENT_TIMEOUT_RANGE),
483
+ dispatchTimeoutMs: clampIntAllowingZero(src.dispatchTimeoutMs, DEFAULT_TEAM_CONFIG.dispatchTimeoutMs, DISPATCH_TIMEOUT_RANGE),
484
+ maxTurnsPerSubagent: clampIntAllowingZero(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
480
485
  };
481
486
  }
482
487
  export const DEFAULT_TODO_CONFIG = {
@@ -615,7 +620,6 @@ export const DEFAULT_FAST_CONFIG = {
615
620
  model: '',
616
621
  baseUrl: '',
617
622
  thinkingLevel: 'off',
618
- delegate: true,
619
623
  review: true,
620
624
  reviewEveryTurns: 5,
621
625
  reviewContextTurns: 3,
@@ -655,7 +659,6 @@ export function clampFastConfig(raw) {
655
659
  model: text(src.model, DEFAULT_FAST_CONFIG.model),
656
660
  baseUrl: text(src.baseUrl, DEFAULT_FAST_CONFIG.baseUrl),
657
661
  thinkingLevel: clampThinkingLevel(src.thinkingLevel, DEFAULT_FAST_CONFIG.thinkingLevel),
658
- delegate: bool(src.delegate, DEFAULT_FAST_CONFIG.delegate),
659
662
  review: bool(src.review, DEFAULT_FAST_CONFIG.review),
660
663
  reviewEveryTurns: clampInt(src.reviewEveryTurns, DEFAULT_FAST_CONFIG.reviewEveryTurns, REVIEW_EVERY_RANGE),
661
664
  reviewContextTurns: clampInt(src.reviewContextTurns, DEFAULT_FAST_CONFIG.reviewContextTurns, REVIEW_CONTEXT_RANGE),
@@ -896,6 +899,7 @@ export const DEFAULT_CONFIG = {
896
899
  mouse: true,
897
900
  mouseSelect: DEFAULT_MOUSE_SELECT,
898
901
  paste: true,
902
+ keyboardEnhancement: true,
899
903
  scrollResumeMs: DEFAULT_SCROLL_RESUME_MS,
900
904
  planModeDefault: false,
901
905
  planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
@@ -219,11 +219,11 @@ export interface ExecFastReviewEvent {
219
219
  * not know is unaffected.
220
220
  *
221
221
  * THE PROBLEM IT SOLVES. `fast_review` only fires when a review produced TEXT,
222
- * so every other outcome — a review that failed, a tier that switched itself off
223
- * after three failures, a budget that ran out, a delegation that did or did not
224
- * happen — was invisible to a wrapper. The single most likely and least
225
- * self-diagnosable failure ("it runs but not one review card ever arrives") had
226
- * no signal at all on this stream.
222
+ * so every other outcome — a review that failed, a tier that switched itself
223
+ * off after three failures, or a budget that ran out — was invisible to a
224
+ * wrapper. The single most likely and least self-diagnosable failure ("it
225
+ * runs but not one review card ever arrives") had no signal at all on this
226
+ * stream.
227
227
  *
228
228
  * IT IS A STATE EVENT, NOT A LOG LINE. It fires on `tier_changed` and on every
229
229
  * `review_end`, which in a session with reviews is a handful per hundred turns.
@@ -249,7 +249,11 @@ export interface ExecFastTierEvent {
249
249
  reviews: number;
250
250
  reviewBudget: number;
251
251
  budgetReached: boolean;
252
- /** Fast-tier children dispatched — the only evidence delegation is working. */
252
+ /**
253
+ * DEPRECATED, ALWAYS `0`. Fast-tier children no longer exist (every `task`
254
+ * child runs the lead's model); the field is kept so schema-v1 consumers
255
+ * keep parsing. Do not build new readers on it.
256
+ */
253
257
  delegated: number;
254
258
  inFlight: boolean;
255
259
  /** Present only on the `review_end` branch. */
@@ -399,8 +403,8 @@ export interface ExecInitParams {
399
403
  *
400
404
  * · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
401
405
  * without killing the process.
402
- * · `'fast-policy'` — this build understands `--fast-delegate` and
403
- * `--fast-review`. FOR AFTER-THE-FACT DISPLAY ONLY:
406
+ * · `'fast-policy'` — this build understands `--fast-review`.
407
+ * FOR AFTER-THE-FACT DISPLAY ONLY:
404
408
  * argv is fixed before `system/init` arrives, so it
405
409
  * cannot gate whether the flags are passed.
406
410
  * · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
@@ -34,8 +34,8 @@ export const EXEC_SCHEMA_VERSION = 1;
34
34
  *
35
35
  * · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
36
36
  * without killing the process.
37
- * · `'fast-policy'` — this build understands `--fast-delegate` and
38
- * `--fast-review`. FOR AFTER-THE-FACT DISPLAY ONLY:
37
+ * · `'fast-policy'` — this build understands `--fast-review`.
38
+ * FOR AFTER-THE-FACT DISPLAY ONLY:
39
39
  * argv is fixed before `system/init` arrives, so it
40
40
  * cannot gate whether the flags are passed.
41
41
  * · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
@@ -153,7 +153,10 @@ export class ExecRunner {
153
153
  reviews: snapshot.reviews,
154
154
  reviewBudget: snapshot.reviewBudget,
155
155
  budgetReached: snapshot.budgetReached,
156
- delegated: snapshot.delegated,
156
+ // Deprecated in schema v1: always 0 since fast-tier children were
157
+ // removed (every task child runs the lead's model). Emitted so
158
+ // schema-v1 consumers keep parsing.
159
+ delegated: 0,
157
160
  inFlight: snapshot.inFlight,
158
161
  ...(event.type === 'review_end'
159
162
  ? {
@@ -76,7 +76,7 @@ export declare const FAST_LIMITS: {
76
76
  * A critique waiting for its injection window goes stale (RV-12).
77
77
  *
78
78
  * The window is not guaranteed to be prompt: one `task` dispatch is a single
79
- * tool call that can run for `team.dispatchTimeoutMs`, so a critique triggered
79
+ * tool call that can run for as long as its children need, so a critique triggered
80
80
  * just before one arrives at the far side describing a state five children
81
81
  * have since rewritten. Advice that is merely late is worse than no advice,
82
82
  * because the lead cannot tell that it is late.
@@ -95,4 +95,4 @@ export declare const FAST_LIMITS: {
95
95
  * changes, so a behaviour report can be tied to a block revision with one grep -
96
96
  * exactly what `TEAM_BLOCK_VERSION` and `TODO_BLOCK_VERSION` are for.
97
97
  */
98
- export declare const FAST_BLOCK_VERSION = "v1-2026-07";
98
+ export declare const FAST_BLOCK_VERSION = "v2-2026-10";
@@ -76,7 +76,7 @@ export const FAST_LIMITS = {
76
76
  * A critique waiting for its injection window goes stale (RV-12).
77
77
  *
78
78
  * The window is not guaranteed to be prompt: one `task` dispatch is a single
79
- * tool call that can run for `team.dispatchTimeoutMs`, so a critique triggered
79
+ * tool call that can run for as long as its children need, so a critique triggered
80
80
  * just before one arrives at the far side describing a state five children
81
81
  * have since rewritten. Advice that is merely late is worse than no advice,
82
82
  * because the lead cannot tell that it is late.
@@ -95,4 +95,4 @@ export const FAST_LIMITS = {
95
95
  * changes, so a behaviour report can be tied to a block revision with one grep -
96
96
  * exactly what `TEAM_BLOCK_VERSION` and `TODO_BLOCK_VERSION` are for.
97
97
  */
98
- export const FAST_BLOCK_VERSION = 'v1-2026-07';
98
+ export const FAST_BLOCK_VERSION = 'v2-2026-10';
@@ -19,23 +19,22 @@ export interface FastBlockParams {
19
19
  /** The resolved fast model id, interpolated so the prompt never names a model
20
20
  * the tier will not actually use (§3.9 / RV-3). */
21
21
  model: string;
22
- /** `fast.delegate` — whether `model: "fast"` is honoured on `task`. */
23
- delegate: boolean;
24
22
  /** `fast.review` — whether `<fast_review>` blocks can appear at all. */
25
23
  review: boolean;
26
24
  }
27
25
  /**
28
- * The lead's `<fast_tier>` block. Under 1100 characters on purpose: it is paid
29
- * for on EVERY turn of every fast-enabled session, so anything that does not
30
- * change a delegation decision does not belong in it (R-11).
26
+ * The `<fast_tier>` block, spliced for the lead AND for task children running
27
+ * under a per-child reviewer. Under 1100 characters on purpose: it is paid for
28
+ * on EVERY turn of every fast-enabled session, so anything that does not change
29
+ * a decision does not belong in it (R-11).
31
30
  *
32
- * EACH PARAGRAPH IS PRESENT ONLY WHEN ITS CAPABILITY IS. `fast.delegate: false,
33
- * fast.review: true` advertises only the review, because telling a model about a
34
- * `model: "fast"` field that the normalizer will downgrade every time is how you
35
- * get a model that keeps asking for something it can never have.
31
+ * REVIEW-ONLY SIN MAIN-AGENT PARITY: every `task` child runs the lead's own
32
+ * model, so the block no longer advertises a per-child `model:"fast"`. Telling
33
+ * a model about a field that no longer exists is how you get a model that keeps
34
+ * asking for something it can never have.
36
35
  *
37
- * Returns `''` when neither capability is live, which the splice treats as "no
38
- * block" and which keeps the byte-identity branch intact.
36
+ * Returns `''` when the review is off, which the splice treats as "no block"
37
+ * and which keeps the byte-identity branch intact.
39
38
  */
40
39
  export declare function buildFastBlock(params: FastBlockParams): string;
41
40
  export interface ReviewSystemPromptParams {