@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
@@ -60,6 +60,15 @@ export interface StdinFilterFeatures {
60
60
  readonly mouse: boolean;
61
61
  /** Recognise pastes and deliver them framed. */
62
62
  readonly paste: boolean;
63
+ /**
64
+ * Translate keyboard-enhancement encodings (win32-input-mode records on
65
+ * Windows, CSI-u sequences from kitty/modifyOtherKeys elsewhere) back to
66
+ * legacy bytes BEFORE Ink parses them. TOLD, never derived: `cli.tsx`
67
+ * resolves config, TTY state and `vtInputSupported`, and passes the same
68
+ * boolean to `enableKeyboardEnhancement`, so the filter only ever
69
+ * translates for a session whose mode push actually happened.
70
+ */
71
+ readonly enhancedKeys?: boolean;
63
72
  }
64
73
  export interface StdinFilter {
65
74
  /** Hand this to Ink: `render(<App/>, { stdin: filter.stdin })`. */
@@ -36,6 +36,8 @@ import { MAX_PENDING_MOUSE_CHARS, splitMouseEvents, isMousePrefix, } from './mou
36
36
  import { ENTER_NEWLINE_FRAME, PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
37
37
  import { PASTE_BEGIN_MARK, PASTE_END_MARK, classifyChunk, sanitisePaste, trailingPastePrefixLength, } from './paste-parse.js';
38
38
  import { splitEnterSequences, trailingEnterPrefixLength } from './enter-sequences.js';
39
+ import { splitCsiUKeys, trailingCsiUPrefixLength, translateCsiUKey } from './csiu-keys.js';
40
+ import { splitWin32KeySequences, trailingWin32PrefixLength, translateWin32Key, } from './win32-input-mode.js';
39
41
  /**
40
42
  * How long a possible-prefix tail is held before being flushed as text.
41
43
  *
@@ -82,6 +84,7 @@ function trailingMousePrefixLength(text) {
82
84
  return 0;
83
85
  }
84
86
  export function createStdinFilter(real, features, bridge, onWarn) {
87
+ const enhancedKeys = features.enhancedKeys === true;
85
88
  const wrapper = new PassThrough();
86
89
  const listeners = new Set();
87
90
  let pending = '';
@@ -251,17 +254,50 @@ export function createStdinFilter(real, features, bridge, onWarn) {
251
254
  else
252
255
  writeInput(split.pending);
253
256
  };
257
+ /**
258
+ * THE ENHANCED-KEY WALK, OUTSIDE PASTES ONLY. win32-input-mode records are
259
+ * split first (their bodies are digits/semicolons, so no paste marker or
260
+ * CSI-u sequence can live inside one), then CSI-u sequences in the
261
+ * remaining text, and only then the legacy Enter family. Each translated
262
+ * key terminates a heuristic burst exactly like an Enter frame does -- a
263
+ * keystroke mid-burst is the definition of "not a paste".
264
+ */
265
+ const writeEnhancedKey = (legacy) => {
266
+ if (legacy.length === 0)
267
+ return;
268
+ flushBurst();
269
+ writeInput(legacy);
270
+ };
254
271
  const handleOutsidePaste = (text, holdTail) => {
255
- const segments = splitEnterSequences(text);
256
- for (let index = 0; index < segments.length; index += 1) {
257
- const segment = segments[index];
258
- if (segment.kind === 'text') {
259
- handleText(segment.text, holdTail && index === segments.length - 1);
272
+ const win32 = enhancedKeys ? splitWin32KeySequences(text)
273
+ : [{ kind: 'text', text }];
274
+ for (let w = 0; w < win32.length; w += 1) {
275
+ const wSeg = win32[w];
276
+ const wLast = holdTail && w === win32.length - 1;
277
+ if (wSeg.kind === 'key') {
278
+ writeEnhancedKey(translateWin32Key(wSeg.record));
260
279
  continue;
261
280
  }
262
- // A key terminates the heuristic paste before its internal frame is written.
263
- flushBurst();
264
- writeInput(segment.kind === 'submit' ? '\r' : ENTER_NEWLINE_FRAME);
281
+ const csiu = enhancedKeys ? splitCsiUKeys(wSeg.text)
282
+ : [{ kind: 'text', text: wSeg.text }];
283
+ for (let c = 0; c < csiu.length; c += 1) {
284
+ const cSeg = csiu[c];
285
+ if (cSeg.kind === 'key') {
286
+ writeEnhancedKey(translateCsiUKey(cSeg.key));
287
+ continue;
288
+ }
289
+ const segments = splitEnterSequences(cSeg.text);
290
+ for (let index = 0; index < segments.length; index += 1) {
291
+ const segment = segments[index];
292
+ if (segment.kind === 'text') {
293
+ handleText(segment.text, wLast && c === csiu.length - 1
294
+ && index === segments.length - 1);
295
+ continue;
296
+ }
297
+ // A key terminates the heuristic paste before its internal frame is written.
298
+ writeEnhancedKey(segment.kind === 'submit' ? '\r' : ENTER_NEWLINE_FRAME);
299
+ }
300
+ }
265
301
  }
266
302
  };
267
303
  const appendBracketed = (text) => {
@@ -310,7 +346,7 @@ export function createStdinFilter(real, features, bridge, onWarn) {
310
346
  // the ESC / ESC-[ head bytes, which the max keeps for whoever needs
311
347
  // them. The existing 12 ms flush and the 32-char ceiling bound the
312
348
  // hold, so no new timer is needed.
313
- const keep = Math.max(trailingPastePrefixLength(rest), features.mouse ? trailingMousePrefixLength(rest) : 0, trailingEnterPrefixLength(rest));
349
+ const keep = Math.max(trailingPastePrefixLength(rest), features.mouse ? trailingMousePrefixLength(rest) : 0, trailingEnterPrefixLength(rest), enhancedKeys ? trailingWin32PrefixLength(rest) : 0, enhancedKeys ? trailingCsiUPrefixLength(rest) : 0);
314
350
  const tail = rest.slice(rest.length - keep);
315
351
  handleOutsidePaste(rest.slice(0, rest.length - keep), true);
316
352
  pending += tail;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Win32-input-mode translation (win32 keyboard-enhancement section of the
3
+ * Shift+Enter newline fix).
4
+ *
5
+ * WHY THIS EXISTS. With `CSI ? 9001 h` the Windows console (conhost inside
6
+ * ConPTY included -- verified byte-level on both conhost and Windows Terminal
7
+ * 1.24, see docs/diagnoses/shift-enter-newline-dead-by-default) reports EVERY
8
+ * key as `ESC [ vk ; scan ; char ; keyDown ; mods ; repeat _`. That is the
9
+ * only Windows-side encoding in which Shift+Enter is distinguishable from
10
+ * Enter: mods carries SHIFT_PRESSED (0x10) on the record. Nothing else
11
+ * survives ConPTY: kitty `CSI > 1 u` is swallowed (a `CSI ? u` query never
12
+ * answers) and modifyOtherKeys is a no-op.
13
+ *
14
+ * THE CONTRACT. `splitWin32KeySequences` recognises the records in a chunk;
15
+ * `translateWin32Key` rewrites each one to the LEGACY bytes the same keypress
16
+ * would have produced with the mode off -- except the modified-Enter family,
17
+ * which becomes `ENTER_NEWLINE_FRAME` exactly like `enter-sequences.ts` maps
18
+ * its CSI-u cousins. Ink therefore sees the stream it has always seen. Key-up
19
+ * records are dropped (every press arrives as a down/up pair in one chunk).
20
+ *
21
+ * Pure: no node/React imports, callable from any test environment.
22
+ */
23
+ /** Enable / disable win32-input-mode (private mode 9001). */
24
+ export declare const WIN32_INPUT_ENABLE = "\u001B[?9001h";
25
+ export declare const WIN32_INPUT_DISABLE = "\u001B[?9001l";
26
+ /** One decoded `ESC [ vk ; sc ; char ; down ; mods ; rep _` record. */
27
+ export interface Win32KeyRecord {
28
+ readonly vk: number;
29
+ readonly scan: number;
30
+ readonly char: number;
31
+ readonly down: boolean;
32
+ readonly mods: number;
33
+ readonly repeat: number;
34
+ }
35
+ export type Win32Segment = {
36
+ kind: 'text';
37
+ text: string;
38
+ } | {
39
+ kind: 'key';
40
+ record: Win32KeyRecord;
41
+ };
42
+ /**
43
+ * Split a chunk into text spans and win32-input-mode records.
44
+ *
45
+ * MATCHES ONLY `ESC [ <digits/semicolons> _`: mouse reports need `<`, paste
46
+ * markers end in `~`, CSI-u ends in `u`, DECRPM replies start with `?`. A
47
+ * `_`-terminated all-numeric-parameter sequence is therefore unambiguous in
48
+ * the stream this filter ever sees.
49
+ */
50
+ export declare function splitWin32KeySequences(text: string): Win32Segment[];
51
+ /**
52
+ * Translate one record to the legacy bytes the same keypress would have
53
+ * produced with win32-input-mode off (Enter family excepted: a modified Enter
54
+ * becomes the newline frame, mirroring `ENTER_SEQUENCES`).
55
+ */
56
+ export declare function translateWin32Key(record: Win32KeyRecord): string;
57
+ /**
58
+ * Length of the trailing run of `text` that could still complete into a
59
+ * win32-input-mode record, or 0. Longest match, mirroring the other families.
60
+ */
61
+ export declare function trailingWin32PrefixLength(text: string): number;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Win32-input-mode translation (win32 keyboard-enhancement section of the
3
+ * Shift+Enter newline fix).
4
+ *
5
+ * WHY THIS EXISTS. With `CSI ? 9001 h` the Windows console (conhost inside
6
+ * ConPTY included -- verified byte-level on both conhost and Windows Terminal
7
+ * 1.24, see docs/diagnoses/shift-enter-newline-dead-by-default) reports EVERY
8
+ * key as `ESC [ vk ; scan ; char ; keyDown ; mods ; repeat _`. That is the
9
+ * only Windows-side encoding in which Shift+Enter is distinguishable from
10
+ * Enter: mods carries SHIFT_PRESSED (0x10) on the record. Nothing else
11
+ * survives ConPTY: kitty `CSI > 1 u` is swallowed (a `CSI ? u` query never
12
+ * answers) and modifyOtherKeys is a no-op.
13
+ *
14
+ * THE CONTRACT. `splitWin32KeySequences` recognises the records in a chunk;
15
+ * `translateWin32Key` rewrites each one to the LEGACY bytes the same keypress
16
+ * would have produced with the mode off -- except the modified-Enter family,
17
+ * which becomes `ENTER_NEWLINE_FRAME` exactly like `enter-sequences.ts` maps
18
+ * its CSI-u cousins. Ink therefore sees the stream it has always seen. Key-up
19
+ * records are dropped (every press arrives as a down/up pair in one chunk).
20
+ *
21
+ * Pure: no node/React imports, callable from any test environment.
22
+ */
23
+ import { ENTER_NEWLINE_FRAME } from './limits.js';
24
+ /** Enable / disable win32-input-mode (private mode 9001). */
25
+ export const WIN32_INPUT_ENABLE = '\x1b[?9001h';
26
+ export const WIN32_INPUT_DISABLE = '\x1b[?9001l';
27
+ /** Windows console `dwControlKeyState` modifier bits this module cares about. */
28
+ const RIGHT_ALT = 0x1;
29
+ const LEFT_ALT = 0x2;
30
+ const RIGHT_CTRL = 0x4;
31
+ const LEFT_CTRL = 0x8;
32
+ const SHIFT = 0x10;
33
+ /** Lock states, ENHANCED_KEY and friends must never look like modifiers. */
34
+ const MODIFIER_MASK = SHIFT | LEFT_CTRL | RIGHT_CTRL | LEFT_ALT | RIGHT_ALT;
35
+ /** Virtual-key codes the translator names explicitly. */
36
+ const VK_BACK = 0x08;
37
+ const VK_TAB = 0x09;
38
+ const VK_ENTER = 0x0d;
39
+ const VK_ESCAPE = 0x1b;
40
+ const VK_PACKET = 0xe7;
41
+ /**
42
+ * Parse one candidate body (`digits and semicolons`, the `_` excluded).
43
+ * Returns null when the field count is impossible for a real record -- that
44
+ * is what keeps a random `ESC [ 1 ; 2 3 _`-shaped paste from being eaten.
45
+ */
46
+ function parseRecord(body) {
47
+ const fields = body.split(';');
48
+ if (fields.length !== 6)
49
+ return null;
50
+ const nums = fields.map(Number);
51
+ if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 0x10ffff))
52
+ return null;
53
+ const [vk, , char, down, mods, repeat] = nums;
54
+ if (vk === undefined || char === undefined || down === undefined)
55
+ return null;
56
+ if (mods === undefined || repeat === undefined)
57
+ return null;
58
+ if (vk > 0xff)
59
+ return null;
60
+ return { vk, scan: nums[1] ?? 0, char, down: down === 1, mods, repeat };
61
+ }
62
+ /**
63
+ * Split a chunk into text spans and win32-input-mode records.
64
+ *
65
+ * MATCHES ONLY `ESC [ <digits/semicolons> _`: mouse reports need `<`, paste
66
+ * markers end in `~`, CSI-u ends in `u`, DECRPM replies start with `?`. A
67
+ * `_`-terminated all-numeric-parameter sequence is therefore unambiguous in
68
+ * the stream this filter ever sees.
69
+ */
70
+ export function splitWin32KeySequences(text) {
71
+ const segments = [];
72
+ let start = 0;
73
+ let index = 0;
74
+ while (index < text.length) {
75
+ if (text[index] !== '\x1b' || text[index + 1] !== '[') {
76
+ index += 1;
77
+ continue;
78
+ }
79
+ let end = index + 2;
80
+ while (end < text.length && text[end] !== '_') {
81
+ const c = text[end];
82
+ if (c < '0' || c > '9') {
83
+ if (c !== ';') {
84
+ end = -1;
85
+ break;
86
+ }
87
+ }
88
+ end += 1;
89
+ }
90
+ if (end === -1 || end >= text.length) {
91
+ index += 1;
92
+ continue;
93
+ }
94
+ const record = parseRecord(text.slice(index + 2, end));
95
+ if (!record) {
96
+ index += 1;
97
+ continue;
98
+ }
99
+ if (index > start)
100
+ segments.push({ kind: 'text', text: text.slice(start, index) });
101
+ segments.push({ kind: 'key', record });
102
+ index = end + 1;
103
+ start = index;
104
+ }
105
+ if (start < text.length)
106
+ segments.push({ kind: 'text', text: text.slice(start) });
107
+ return segments;
108
+ }
109
+ /** Legacy modifier parameter (xterm): 1 + shift + 2*alt + 4*ctrl. */
110
+ function legacyModifierParam(m) {
111
+ let p = 1;
112
+ if (m & SHIFT)
113
+ p += 1;
114
+ if (m & (LEFT_ALT | RIGHT_ALT))
115
+ p += 2;
116
+ if (m & (LEFT_CTRL | RIGHT_CTRL))
117
+ p += 4;
118
+ return p;
119
+ }
120
+ /** Functional keys named by vk, as (letter | tilde-code) legacy shapes. */
121
+ const FUNCTIONAL_KEYS = new Map([
122
+ [0x21, { code: 5 }], // PgUp
123
+ [0x22, { code: 6 }], // PgDn
124
+ [0x23, { letter: 'F' }], // End
125
+ [0x24, { letter: 'H' }], // Home
126
+ [0x25, { letter: 'D' }], // Left
127
+ [0x26, { letter: 'A' }], // Up
128
+ [0x27, { letter: 'C' }], // Right
129
+ [0x28, { letter: 'B' }], // Down
130
+ [0x2d, { code: 2 }], // Insert
131
+ [0x2e, { code: 3 }], // Delete
132
+ [0x70, { letter: 'P' }], // F1
133
+ [0x71, { letter: 'Q' }], // F2
134
+ [0x72, { letter: 'R' }], // F3
135
+ [0x73, { letter: 'S' }], // F4
136
+ [0x74, { code: 15 }], // F5
137
+ [0x75, { code: 17 }], // F6
138
+ [0x76, { code: 18 }], // F7
139
+ [0x77, { code: 19 }], // F8
140
+ [0x78, { code: 20 }], // F9
141
+ [0x79, { code: 21 }], // F10
142
+ [0x7a, { code: 23 }], // F11
143
+ [0x7b, { code: 24 }], // F12
144
+ ]);
145
+ /** Emit the legacy form of a functional key under `m` modifiers. */
146
+ function functionalLegacy(vk, m) {
147
+ const key = FUNCTIONAL_KEYS.get(vk);
148
+ if (!key)
149
+ return '';
150
+ if (m === 0) {
151
+ return 'letter' in key
152
+ ? (key.letter === 'P' || key.letter === 'Q' || key.letter === 'R' || key.letter === 'S'
153
+ ? `\x1bO${key.letter}`
154
+ : `\x1b[${key.letter}`)
155
+ : `\x1b[${key.code}~`;
156
+ }
157
+ const p = legacyModifierParam(m);
158
+ return 'letter' in key ? `\x1b[1;${p}${key.letter}` : `\x1b[${key.code};${p}~`;
159
+ }
160
+ /** Control code for Ctrl+letter, or the char itself when not a letter. */
161
+ function ctrlByte(char) {
162
+ if (char >= 97 && char <= 122)
163
+ return char - 96; // a-z -> 1-26
164
+ if (char >= 65 && char <= 90)
165
+ return char - 64; // A-Z -> 1-26
166
+ return char;
167
+ }
168
+ const isPrintable = (char) => char >= 0x20 && char !== 0x7f;
169
+ /**
170
+ * Translate one record to the legacy bytes the same keypress would have
171
+ * produced with win32-input-mode off (Enter family excepted: a modified Enter
172
+ * becomes the newline frame, mirroring `ENTER_SEQUENCES`).
173
+ */
174
+ export function translateWin32Key(record) {
175
+ if (!record.down)
176
+ return '';
177
+ const m = record.mods & MODIFIER_MASK;
178
+ const ctrl = (m & (LEFT_CTRL | RIGHT_CTRL)) !== 0;
179
+ const alt = (m & (LEFT_ALT | RIGHT_ALT)) !== 0;
180
+ const shift = (m & SHIFT) !== 0;
181
+ const char = record.char === 0 ? '' : String.fromCodePoint(record.char);
182
+ // AltGr arrives as RightCtrl|RightAlt (or lone RightAlt) WITH the composed
183
+ // character already in `char`; prefixing ESC or control-translating it would
184
+ // break every non-US layout. Plain typing first, exotic combos never.
185
+ if (isPrintable(record.char) && (m === RIGHT_CTRL + RIGHT_ALT || m === RIGHT_ALT)) {
186
+ return char;
187
+ }
188
+ switch (record.vk) {
189
+ case VK_ENTER:
190
+ // A modified Enter NEVER submits -- same rule as `ENTER_SEQUENCES`.
191
+ return m === 0 ? '\r' : ENTER_NEWLINE_FRAME;
192
+ case VK_TAB:
193
+ return shift ? '\x1b[Z' : '\t';
194
+ case VK_ESCAPE:
195
+ return alt ? '\x1b\x1b' : '\x1b';
196
+ case VK_BACK:
197
+ // POST-normalisation bytes: `handleText` rewrites DEL (0x7f) to BS
198
+ // (0x08) for Ink, and translated records bypass that walk -- emitting
199
+ // the final form keeps the stream byte-identical to the baseline.
200
+ if (alt)
201
+ return '\x1b\x08';
202
+ return '\x08';
203
+ default:
204
+ break;
205
+ }
206
+ if (record.char !== 0 && record.char < 0x20 && record.vk !== VK_PACKET) {
207
+ // Ctrl combos: Windows already resolves the control character (Ctrl+C is
208
+ // 0x03). Alt keeps the ESC prefix; Ctrl+Alt prefixes ESC over the code.
209
+ if (!ctrl && !alt && !shift)
210
+ return char;
211
+ if (!ctrl && alt)
212
+ return `\x1b${char}`;
213
+ return ctrl ? `${alt ? '\x1b' : ''}${char}` : char;
214
+ }
215
+ if (isPrintable(record.char)) {
216
+ if (ctrl) {
217
+ const code = ctrlByte(record.char);
218
+ return `${alt ? '\x1b' : ''}${code === record.char ? char : String.fromCharCode(code)}`;
219
+ }
220
+ if (alt)
221
+ return `\x1b${char}`;
222
+ return char;
223
+ }
224
+ return functionalLegacy(record.vk, m);
225
+ }
226
+ /** True when `s` is a PROPER prefix of some win32-input-mode record. */
227
+ function isWin32Prefix(s) {
228
+ // A bare ESC (or ESC + anything else) belongs to the Enter/mouse/paste
229
+ // families' hold; this family only ever extends `\x1b[` with digits and
230
+ // semicolons, up to but never including the `_`.
231
+ if (s.length < 3 || s[0] !== '\x1b' || s[1] !== '[')
232
+ return false;
233
+ for (let i = 2; i < s.length; i += 1) {
234
+ const c = s[i];
235
+ if ((c < '0' || c > '9') && c !== ';')
236
+ return false;
237
+ }
238
+ return true;
239
+ }
240
+ /**
241
+ * Length of the trailing run of `text` that could still complete into a
242
+ * win32-input-mode record, or 0. Longest match, mirroring the other families.
243
+ */
244
+ export function trailingWin32PrefixLength(text) {
245
+ const max = Math.min(text.length, 32);
246
+ for (let k = max; k >= 1; k -= 1) {
247
+ if (isWin32Prefix(text.slice(text.length - k)))
248
+ return k;
249
+ }
250
+ return 0;
251
+ }
@@ -128,7 +128,9 @@ const todo = shape({
128
128
  });
129
129
  const subagent = shape({
130
130
  label: string, description: string,
131
- // Old team archives predate tier; renderers already treat absence as main.
131
+ // OPTIONAL IN BOTH DIRECTIONS: sessions written since main-agent parity
132
+ // have no tier at all, and older archives may carry `main` or `fast` from
133
+ // the removed per-child fast dispatch. Renderers treat absence as main.
132
134
  tier: optional(enumeration('main', 'fast')),
133
135
  phase: enumeration('queued', 'starting', 'thinking', 'tool', 'waiting', 'done', 'failed', 'aborted'),
134
136
  startedAt: optional(number), endedAt: optional(number), turns: number, toolCalls: number,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Per-child fast review (main-agent parity).
3
+ *
4
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
5
+ * (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
6
+ *
7
+ * THE LEAD HAS A FAST REVIEWER; A CHILD NOW HAS ONE TOO. The requirement is
8
+ * that "when the main agent pulls up subagents, all subagents are as capable as
9
+ * the main agent", and the fast/slow cooperation is part of what the main agent
10
+ * has: a cheap second model periodically critiques the run and steers a
11
+ * `<fast_review>` block into the conversation. A child runs the lead's own
12
+ * model, and this module gives that run the same second opinion.
13
+ *
14
+ * WHAT IS SHARED, AND WHAT IS PER CHILD:
15
+ * - SHARED: the tier resolution (`resolveFastTier` on the LIVE config), the
16
+ * fail-fast review transport (`FastWiring.completeViaFastRegistry` - one
17
+ * lazy registry for the whole session, the same adapter-state reuse
18
+ * argument `TeamRuntime` makes about the lead's `ProviderRegistry`), and
19
+ * the `fast.*` policy keys. A `/fast off` mid-dispatch therefore stops
20
+ * child reviews at the same instant it stops the lead's.
21
+ * - PER CHILD: one `FastReviewer` instance, its own turn-frame ring, its own
22
+ * session budget, its own goal (the spec's description).
23
+ *
24
+ * THE REVIEWER'S PROTOCOL IS UNCHANGED, AND THAT IS THE POINT. `FastReviewer`
25
+ * steers from exactly one place (the `tool_execution_end` that brings the batch
26
+ * to zero), never steers into a requested abort, and proves delivery at the
27
+ * next `turn_start`. The two guards need host-side facts, and a child supplies
28
+ * them differently than the lead:
29
+ *
30
+ * - `userSteerCount` is ALWAYS ZERO: no human can steer a child. That makes
31
+ * the reviewer's `clearAllQueues` provably safe by construction (D-21).
32
+ * - `isAbortRequested` comes from the HANDLE-level abort flag that
33
+ * `TeamRuntime` sets BEFORE calling `agent.abort()`. Reading the agent
34
+ * alone is not enough: an Esc lands as `abortAll()`, and without the flag
35
+ * the reviewer would race the abort it was told about.
36
+ * - `emit` and `notify` LOG INSTEAD OF RENDERING. A child has no transcript
37
+ * card and no status chip; the team panel row and the dispatch report are
38
+ * where a child's internals belong, and review spend is bounded by the
39
+ * same `fast.reviewMaxPerSession` budget the lead obeys.
40
+ */
41
+ import type { AgentEvent, AssistantMessage, LLMRequest } from '@aragon-agent/core';
42
+ import type { ModelRole } from '../config/model-profiles.js';
43
+ import type { CliConfig } from '../config/schema.js';
44
+ /**
45
+ * The slice of a child `Agent` the reviewer needs beyond what
46
+ * `SubagentAgentLike` already carries. Core's `Agent` satisfies this with its
47
+ * own `steer` / `clearAllQueues`; a test stub satisfies it the same way.
48
+ */
49
+ export interface ChildReviewerAgent {
50
+ subscribe(listener: (event: AgentEvent) => void): () => void;
51
+ steer(text: string): void;
52
+ clearAllQueues(): void;
53
+ }
54
+ /** Everything one child reviewer needs to know about its child. */
55
+ export interface ChildReviewerRequest {
56
+ label: string;
57
+ /** The spec's description: the goal the critique judges the run against. */
58
+ goal: string;
59
+ agent: ChildReviewerAgent;
60
+ /** A live phase, not a terminal one - the lead's `isRunning` equivalent. */
61
+ isRunning: () => boolean;
62
+ /** Set by the HANDLE-level abort, synchronously before `agent.abort()`. */
63
+ isAbortRequested: () => boolean;
64
+ }
65
+ /** Everything the reviewer needs from the session, all of it shared. */
66
+ export interface ChildReviewerDeps {
67
+ /** Read LIVE on every call - a mid-dispatch `/fast off` must reach children. */
68
+ getConfig: () => CliConfig;
69
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
70
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
71
+ /** `FastWiring.completeViaFastRegistry` - the shared fail-fast transport. */
72
+ complete: (providerId: string, request: LLMRequest) => Promise<AssistantMessage>;
73
+ /** `FastWiring.available()` - registered AND enabled AND resolving. */
74
+ available: () => boolean;
75
+ }
76
+ export interface ChildReviewerHandle {
77
+ dispose(): void;
78
+ }
79
+ /**
80
+ * What the controller hands `subagent.ts`, so a child's reviewer is built from
81
+ * the lead's own wiring rather than a second tier resolver.
82
+ *
83
+ * `active` AND `create` ARE SEPARATE ON PURPOSE: the child's system prompt
84
+ * must be composed BEFORE the agent exists, and `create` needs the agent. The
85
+ * two must answer the same question at the same instant, so both derive from
86
+ * `FastWiring.available() && config.fast.review` and the caller reads `active`
87
+ * once per child.
88
+ */
89
+ export interface ChildReviewProvider {
90
+ /** Whether per-child review is live RIGHT NOW (tier live AND `fast.review`). */
91
+ active: () => boolean;
92
+ /** The resolved fast model id, for the child's `<fast_tier>` prompt block. */
93
+ modelId: () => string;
94
+ /** Build one reviewer for a child; `undefined` when not active. */
95
+ create: (req: ChildReviewerRequest) => ChildReviewerHandle | undefined;
96
+ }
97
+ /**
98
+ * Build one reviewer for one child, or `undefined` when the session has no
99
+ * live review to give it.
100
+ *
101
+ * `undefined` RATHER THAN A NO-OP REVIEWER, for the same reason
102
+ * `createChildContextManager` declines: the caller then wires nothing at all,
103
+ * and a session with the fast tier off builds the child it always built.
104
+ */
105
+ export declare function createChildReviewer(req: ChildReviewerRequest, deps: ChildReviewerDeps): ChildReviewerHandle | undefined;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Per-child fast review (main-agent parity).
3
+ *
4
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
5
+ * (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
6
+ *
7
+ * THE LEAD HAS A FAST REVIEWER; A CHILD NOW HAS ONE TOO. The requirement is
8
+ * that "when the main agent pulls up subagents, all subagents are as capable as
9
+ * the main agent", and the fast/slow cooperation is part of what the main agent
10
+ * has: a cheap second model periodically critiques the run and steers a
11
+ * `<fast_review>` block into the conversation. A child runs the lead's own
12
+ * model, and this module gives that run the same second opinion.
13
+ *
14
+ * WHAT IS SHARED, AND WHAT IS PER CHILD:
15
+ * - SHARED: the tier resolution (`resolveFastTier` on the LIVE config), the
16
+ * fail-fast review transport (`FastWiring.completeViaFastRegistry` - one
17
+ * lazy registry for the whole session, the same adapter-state reuse
18
+ * argument `TeamRuntime` makes about the lead's `ProviderRegistry`), and
19
+ * the `fast.*` policy keys. A `/fast off` mid-dispatch therefore stops
20
+ * child reviews at the same instant it stops the lead's.
21
+ * - PER CHILD: one `FastReviewer` instance, its own turn-frame ring, its own
22
+ * session budget, its own goal (the spec's description).
23
+ *
24
+ * THE REVIEWER'S PROTOCOL IS UNCHANGED, AND THAT IS THE POINT. `FastReviewer`
25
+ * steers from exactly one place (the `tool_execution_end` that brings the batch
26
+ * to zero), never steers into a requested abort, and proves delivery at the
27
+ * next `turn_start`. The two guards need host-side facts, and a child supplies
28
+ * them differently than the lead:
29
+ *
30
+ * - `userSteerCount` is ALWAYS ZERO: no human can steer a child. That makes
31
+ * the reviewer's `clearAllQueues` provably safe by construction (D-21).
32
+ * - `isAbortRequested` comes from the HANDLE-level abort flag that
33
+ * `TeamRuntime` sets BEFORE calling `agent.abort()`. Reading the agent
34
+ * alone is not enough: an Esc lands as `abortAll()`, and without the flag
35
+ * the reviewer would race the abort it was told about.
36
+ * - `emit` and `notify` LOG INSTEAD OF RENDERING. A child has no transcript
37
+ * card and no status chip; the team panel row and the dispatch report are
38
+ * where a child's internals belong, and review spend is bounded by the
39
+ * same `fast.reviewMaxPerSession` budget the lead obeys.
40
+ */
41
+ import { getLogger } from '../logging/logger.js';
42
+ import { resolveFastTier } from '../fast/resolve.js';
43
+ import { FastReviewer } from '../fast/reviewer.js';
44
+ /**
45
+ * A snapshot for the reviewer's self-disable announcement. The lead's
46
+ * `FastWiring.snapshot()` builds the same shape from its own reviewer; a child
47
+ * has no wiring, so this builds it from the reviewer the module itself holds.
48
+ *
49
+ * `pricingUnknown` is FALSE because the child reviewer never prices anything:
50
+ * it logs counts, not currency, and an unpriced model id therefore cannot lie
51
+ * here the way it can on the lead's status line (C-11).
52
+ */
53
+ function buildSnapshot(reviewer, tier) {
54
+ return {
55
+ live: tier.ok,
56
+ selfDisabled: reviewer?.isSelfDisabled() ?? false,
57
+ model: tier.ok ? tier.ref.modelId : '',
58
+ sameAsMain: tier.ok ? tier.sameAsMain : false,
59
+ reviews: reviewer?.reviewCount() ?? 0,
60
+ reviewBudget: reviewer?.budgetLimit() ?? 0,
61
+ budgetReached: reviewer?.isBudgetReached() ?? false,
62
+ usage: reviewer?.sessionUsage() ?? { inputTokens: 0, outputTokens: 0 },
63
+ pricingUnknown: false,
64
+ inFlight: reviewer?.isReviewInFlight() ?? false,
65
+ };
66
+ }
67
+ /**
68
+ * Build one reviewer for one child, or `undefined` when the session has no
69
+ * live review to give it.
70
+ *
71
+ * `undefined` RATHER THAN A NO-OP REVIEWER, for the same reason
72
+ * `createChildContextManager` declines: the caller then wires nothing at all,
73
+ * and a session with the fast tier off builds the child it always built.
74
+ */
75
+ export function createChildReviewer(req, deps) {
76
+ // GATED ON THE SAME THREE FACTS THE LEAD'S REVIEWER IS: the wiring is live
77
+ // (`available()`), the policy allows reviews (`fast.review`), and the tier
78
+ // resolves against the LIVE config. Re-checked, never snapshotted, so a
79
+ // mid-dispatch `/fast off` or a settings-screen model edit reaches the next
80
+ // child even when it cannot stop one already running.
81
+ const tier = resolveFastTier(deps.getConfig(), deps.hasKey);
82
+ if (!deps.available() || !deps.getConfig().fast.review || !tier.ok)
83
+ return undefined;
84
+ const log = getLogger().child('fast');
85
+ let reviewer = null;
86
+ const built = new FastReviewer({
87
+ subscribe: (listener) => req.agent.subscribe(listener),
88
+ complete: deps.complete,
89
+ steer: (text) => req.agent.steer(text),
90
+ isRunning: req.isRunning,
91
+ isAbortRequested: req.isAbortRequested,
92
+ userSteerCount: () => 0,
93
+ clearAllQueues: () => req.agent.clearAllQueues(),
94
+ getTier: () => resolveFastTier(deps.getConfig(), deps.hasKey),
95
+ getConfig: () => deps.getConfig().fast,
96
+ available: () => deps.available() && resolveFastTier(deps.getConfig(), deps.hasKey).ok,
97
+ getApiKey: (providerId) => deps.getApiKey(providerId, 'fast'),
98
+ emit: (event) => {
99
+ log.info('child_review', { label: req.label, type: event.type });
100
+ },
101
+ snapshot: () => buildSnapshot(reviewer, tier),
102
+ // A CHILD HAS NO TRANSCRIPT. The lead's warnings ride `notify` into a card
103
+ // the user reads; the same text from a background worker would claim the
104
+ // user's attention for an agent they cannot see. It is logged instead.
105
+ notify: (level, text) => {
106
+ log.warn('child_review_notice', { label: req.label, level, text });
107
+ },
108
+ });
109
+ reviewer = built;
110
+ built.setGoal(req.goal);
111
+ log.info('child_review_attached', {
112
+ label: req.label,
113
+ model: tier.ref.modelId,
114
+ reviewEveryTurns: deps.getConfig().fast.reviewEveryTurns,
115
+ });
116
+ return { dispose: () => built.dispose() };
117
+ }