@aragon-agent/cli 0.6.16 → 0.6.18

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 (79) hide show
  1. package/CHANGELOG.md +107 -51
  2. package/README.md +91 -48
  3. package/dist/agent/controller.js +52 -47
  4. package/dist/agent/headless.js +7 -0
  5. package/dist/agent/reducer.js +0 -4
  6. package/dist/cli.js +0 -13
  7. package/dist/commands/builtins.js +21 -20
  8. package/dist/commands/index-build.d.ts +39 -0
  9. package/dist/commands/index-build.js +61 -0
  10. package/dist/compaction/child.d.ts +10 -8
  11. package/dist/compaction/child.js +23 -25
  12. package/dist/compaction/limits.d.ts +0 -2
  13. package/dist/compaction/limits.js +0 -2
  14. package/dist/config/cli-commands.d.ts +2 -2
  15. package/dist/config/cli-commands.js +19 -15
  16. package/dist/config/load.d.ts +0 -17
  17. package/dist/config/load.js +0 -6
  18. package/dist/config/schema.d.ts +41 -9
  19. package/dist/config/schema.js +18 -11
  20. package/dist/exec/events.d.ts +16 -9
  21. package/dist/exec/events.js +2 -2
  22. package/dist/exec/runner.js +17 -1
  23. package/dist/fast/limits.d.ts +2 -2
  24. package/dist/fast/limits.js +2 -2
  25. package/dist/fast/prompt.d.ts +10 -11
  26. package/dist/fast/prompt.js +19 -20
  27. package/dist/fast/reviewer.d.ts +1 -1
  28. package/dist/fast/reviewer.js +1 -1
  29. package/dist/fast/types.d.ts +0 -2
  30. package/dist/fast/wiring.d.ts +18 -34
  31. package/dist/fast/wiring.js +20 -51
  32. package/dist/input/csiu-keys.js +13 -2
  33. package/dist/input/limits.d.ts +15 -0
  34. package/dist/input/limits.js +15 -0
  35. package/dist/input/win32-input-mode.js +7 -1
  36. package/dist/logging/install.d.ts +9 -0
  37. package/dist/logging/install.js +11 -0
  38. package/dist/session/validate-session.js +3 -1
  39. package/dist/team/child-reviewer.d.ts +105 -0
  40. package/dist/team/child-reviewer.js +117 -0
  41. package/dist/team/human-queue.d.ts +11 -0
  42. package/dist/team/human-queue.js +18 -1
  43. package/dist/team/limits.d.ts +40 -0
  44. package/dist/team/limits.js +41 -0
  45. package/dist/team/normalize.d.ts +7 -26
  46. package/dist/team/normalize.js +9 -22
  47. package/dist/team/overseer.d.ts +157 -0
  48. package/dist/team/overseer.js +348 -0
  49. package/dist/team/prompt.d.ts +0 -11
  50. package/dist/team/prompt.js +3 -12
  51. package/dist/team/report.d.ts +4 -19
  52. package/dist/team/report.js +27 -28
  53. package/dist/team/runtime.d.ts +76 -16
  54. package/dist/team/runtime.js +410 -57
  55. package/dist/team/subagent.d.ts +48 -19
  56. package/dist/team/subagent.js +88 -31
  57. package/dist/team/task-tool.d.ts +6 -26
  58. package/dist/team/task-tool.js +17 -48
  59. package/dist/team/types.d.ts +65 -41
  60. package/dist/ui/App.d.ts +1 -1
  61. package/dist/ui/App.js +64 -4
  62. package/dist/ui/BottomStatusRow.js +2 -1
  63. package/dist/ui/Header.js +13 -2
  64. package/dist/ui/PromptInput.js +11 -1
  65. package/dist/ui/StatusBar.js +2 -1
  66. package/dist/ui/TeamPanel.js +1 -1
  67. package/dist/ui/entries/TeamCard.js +1 -6
  68. package/dist/ui/layout/status-detail-layout.js +12 -2
  69. package/dist/ui/layout/status-layout.d.ts +4 -2
  70. package/dist/ui/layout/status-layout.js +24 -1
  71. package/dist/ui/overlays/HelpOverlay.js +2 -2
  72. package/package.json +2 -2
  73. package/skills/project-indexer/SKILL.md +163 -0
  74. package/skills/project-indexer/templates/cleancode-overrides.md +46 -0
  75. package/skills/project-indexer/templates/cleancode-template.md +94 -0
  76. package/skills/project-indexer/templates/config-template.md +69 -0
  77. package/skills/project-indexer/templates/index-template.md +129 -0
  78. package/dist/ui/ModeChip.d.ts +0 -18
  79. package/dist/ui/ModeChip.js +0 -24
@@ -7,8 +7,8 @@
7
7
  * THIS CLASS EXISTS FOR A BUDGET REASON, NOT AN AESTHETIC ONE (C-12 / RV-5).
8
8
  * `CLAUDE.md` caps a source file at 1000 lines and `controller.ts` was at 960
9
9
  * before this feature, which handed it nine new responsibilities. `FastWiring`
10
- * owns the flag pair, the tier cache, the reviewer's lifetime and the
11
- * `resolveTier` closure; `AgentController` keeps one field and four thin
10
+ * owns the flag pair, the tier cache, the reviewer's lifetime and the shared
11
+ * fail-fast review transport; `AgentController` keeps one field and four thin
12
12
  * forwarders. It is still CLI-local and still holds no `ui/` import.
13
13
  *
14
14
  * TWO FLAGS AND ONE LIVE PREDICATE (§3.3), which is the shape every optional
@@ -64,7 +64,6 @@ export function offFastStatus() {
64
64
  // `budgetReached` is CARRIED and callers must never re-derive it.
65
65
  reviewBudget: 0,
66
66
  budgetReached: false,
67
- delegated: 0,
68
67
  usage: { inputTokens: 0, outputTokens: 0 },
69
68
  pricingUnknown: false,
70
69
  inFlight: false,
@@ -86,7 +85,6 @@ export class FastWiring {
86
85
  tier;
87
86
  /** Warned once per TRANSITION, so a broken tier does not shout every turn. */
88
87
  lastReportedReason = null;
89
- delegated = 0;
90
88
  constructor(deps) {
91
89
  this.deps = deps;
92
90
  this.tier = resolveFastTier(deps.getConfig(), deps.hasKey);
@@ -168,26 +166,15 @@ export class FastWiring {
168
166
  * §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
169
167
  *
170
168
  * `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
171
- * it in would look tidier and would break something else: `delegationAvailable()`
172
- * is `available() && config.fast.delegate === true`, so three consecutive
173
- * REVIEW failures would silently switch DELEGATION off as well - turning a
174
- * diagnostic signal into a behaviour change for every user. The two facts are
175
- * reported separately on `FastSnapshot` instead (`live` / `selfDisabled`).
169
+ * it in would look tidier and would break something else: per-child reviewers
170
+ * gate on `available()` too, so three consecutive REVIEW failures would
171
+ * silently stop every child's reviews as well - turning a diagnostic signal
172
+ * into a behaviour change for every dispatch. The two facts are reported
173
+ * separately on `FastSnapshot` instead (`live` / `selfDisabled`).
176
174
  */
177
175
  available() {
178
176
  return this.registered && this.enabled && this.tier.ok;
179
177
  }
180
- /**
181
- * Whether `model: "fast"` on `task` is honoured right now.
182
- *
183
- * Evaluated AT DISPATCH TIME, never snapshot (§3.4 / RV-3). `fast.delegate:
184
- * false` and "the tier stopped resolving twenty minutes ago" are the same
185
- * observable to the model - a downgrade, counted and reported - which is the
186
- * honest reading of both.
187
- */
188
- delegationAvailable() {
189
- return this.available() && this.deps.getConfig().fast.delegate === true;
190
- }
191
178
  setEnabled(enabled) {
192
179
  this.enabled = enabled;
193
180
  this.deps.onPromptChanged();
@@ -233,40 +220,23 @@ export class FastWiring {
233
220
  return describeFastTierProblem(this.tier, fastProviderOf(this.deps.getConfig()));
234
221
  }
235
222
  // -----------------------------------------------------------------------
236
- // Tier resolution for subagents (§3.4)
223
+ // The tier's transport, shared with per-child reviewers (§3.4)
237
224
  // -----------------------------------------------------------------------
238
- /**
239
- * The `ModelRef` and thinking level one subagent tier runs on.
240
- *
241
- * `'fast'` FALLS BACK TO MAIN when delegation is not available, so the two
242
- * guards agree BY CONSTRUCTION: the normalizer can never produce a
243
- * `tier: 'fast'` spec this factory would refuse, and this factory can never
244
- * build a child against a `ModelRef` the tier no longer resolves (RV-3).
245
- */
246
- resolveTier(tier) {
247
- const config = this.deps.getConfig();
248
- if (tier === 'fast' && this.delegationAvailable() && this.tier.ok) {
249
- return { ref: this.tier.ref, thinkingLevel: this.tier.thinkingLevel, role: 'fast' };
250
- }
251
- return {
252
- ref: {
253
- providerId: config.provider,
254
- modelId: config.model,
255
- ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
256
- },
257
- thinkingLevel: config.thinkingLevel,
258
- role: 'main',
259
- };
260
- }
261
- /** The resolved fast `ModelRef`, or `null` - for per-tier cost lookup (§3.6). */
225
+ /** The resolved fast `ModelRef`, or `null` - for cost lookups (§3.6). */
262
226
  fastRef() {
263
227
  return this.tier.ok ? this.tier.ref : null;
264
228
  }
265
- /** Count fast-tier children so `/fast status` can answer "is this saving me
266
- * anything?" — the aggregate on the status bar cannot. */
267
- noteDelegated(count) {
268
- if (count > 0)
269
- this.delegated += count;
229
+ /**
230
+ * The fail-fast review transport, exposed so PER-CHILD reviewers share ONE
231
+ * registry instead of building one each - the same adapter-state reuse
232
+ * argument `TeamRuntime` makes about the lead's `ProviderRegistry`.
233
+ *
234
+ * Lazy: a session that never reviews allocates nothing (I-2). The retry
235
+ * policy is `FAST_RETRY_POLICY`, deliberately not the user's `retry` config
236
+ * - see `getFastRegistry()` for the reason.
237
+ */
238
+ completeViaFastRegistry(providerId, request) {
239
+ return this.getFastRegistry().complete(providerId, request);
270
240
  }
271
241
  // -----------------------------------------------------------------------
272
242
  // Reviewer lifetime
@@ -302,7 +272,6 @@ export class FastWiring {
302
272
  reviews: this.reviewer.reviewCount(),
303
273
  reviewBudget: this.reviewer.budgetLimit(),
304
274
  budgetReached: this.reviewer.isBudgetReached(),
305
- delegated: this.delegated,
306
275
  usage: this.reviewer.sessionUsage(),
307
276
  // UNKNOWN PRICING IS NOT ZERO PRICING (C-11 / RV-4). The fast tier is
308
277
  // precisely where an unrecognised model id is likely, and rendering it as
@@ -24,7 +24,7 @@
24
24
  * legacy bytes, with the modified-Enter family becoming `ENTER_NEWLINE_FRAME`.
25
25
  * Pure: no node/React imports.
26
26
  */
27
- import { ENTER_NEWLINE_FRAME } from './limits.js';
27
+ import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME } from './limits.js';
28
28
  /** Enable / disable the two CSI-u report modes (posix side). */
29
29
  export const CSI_U_ENABLE = '\x1b[>1u\x1b[>4;2m';
30
30
  export const CSI_U_DISABLE = '\x1b[<u\x1b[>4m';
@@ -111,8 +111,19 @@ export function translateCsiUKey(key) {
111
111
  return m === 0 ? '\r' : ENTER_NEWLINE_FRAME;
112
112
  if (key.code === 27)
113
113
  return alt ? '\x1b\x1b' : '\x1b';
114
- if (key.code === 9)
114
+ if (key.code === 9) {
115
+ // A modified Tab codepoint (modifyOtherKeys shape) mirrors the VK_TAB
116
+ // decision in win32-input-mode.ts: ctrl (without shift/alt) is the
117
+ // project-indexer trigger frame, shift stays Shift+Tab.
118
+ if (ctrl && !shift && !alt)
119
+ return INDEX_KEY_FRAME;
115
120
  return shift ? '\x1b[Z' : '\t';
121
+ }
122
+ // Ctrl+I under kitty disambiguate arrives as codepoint 105 with the ctrl
123
+ // modifier; the generic printable branch below would fold it back onto the
124
+ // legacy Tab byte, which is exactly the ambiguity the frame exists to break.
125
+ if (key.code === 105 && ctrl && !shift && !alt)
126
+ return INDEX_KEY_FRAME;
116
127
  if (key.code === 127) {
117
128
  // POST-normalisation bytes, same rationale as VK_BACK in win32-input-mode.
118
129
  if (alt)
@@ -64,6 +64,21 @@ export declare const PASTE_CLOSE = "\0]";
64
64
  * no payload, so one constant replaces the OPEN/CLOSE pair.
65
65
  */
66
66
  export declare const ENTER_NEWLINE_FRAME = "\0n";
67
+ /**
68
+ * The inline frame that carries ONE "Ctrl+I was pressed" intent from the stdin
69
+ * filter to `App` (project-indexer Ctrl+I trigger).
70
+ *
71
+ * WHY A FRAME AND NOT `key.ctrl && input === 'i'`. Legacy terminals encode
72
+ * Ctrl+I as the same byte as Tab (0x09), so by the time the stream reaches Ink
73
+ * the two are indistinguishable. Under win32-input-mode (`?9001h`) and kitty
74
+ * CSI-u the modifiers ARE known at translation time, and both translators emit
75
+ * this frame instead of the legacy Tab byte. Same NUL-delimiter guarantee as
76
+ * `ENTER_NEWLINE_FRAME`: no terminal delivers NUL for a key and pasted text is
77
+ * stripped of NUL, so a payload can never forge it. Terminals without keyboard
78
+ * enhancement keep the legacy collapse — Ctrl+I there is Tab, documented in the
79
+ * README keybindings table.
80
+ */
81
+ export declare const INDEX_KEY_FRAME = "\0i";
67
82
  /**
68
83
  * The one channel from the stdin filter to `App`'s notice dispatch (P1-3 / I-15).
69
84
  *
@@ -64,6 +64,21 @@ export const PASTE_CLOSE = '\u0000]';
64
64
  * no payload, so one constant replaces the OPEN/CLOSE pair.
65
65
  */
66
66
  export const ENTER_NEWLINE_FRAME = '\u0000n';
67
+ /**
68
+ * The inline frame that carries ONE "Ctrl+I was pressed" intent from the stdin
69
+ * filter to `App` (project-indexer Ctrl+I trigger).
70
+ *
71
+ * WHY A FRAME AND NOT `key.ctrl && input === 'i'`. Legacy terminals encode
72
+ * Ctrl+I as the same byte as Tab (0x09), so by the time the stream reaches Ink
73
+ * the two are indistinguishable. Under win32-input-mode (`?9001h`) and kitty
74
+ * CSI-u the modifiers ARE known at translation time, and both translators emit
75
+ * this frame instead of the legacy Tab byte. Same NUL-delimiter guarantee as
76
+ * `ENTER_NEWLINE_FRAME`: no terminal delivers NUL for a key and pasted text is
77
+ * stripped of NUL, so a payload can never forge it. Terminals without keyboard
78
+ * enhancement keep the legacy collapse — Ctrl+I there is Tab, documented in the
79
+ * README keybindings table.
80
+ */
81
+ export const INDEX_KEY_FRAME = '\u0000i';
67
82
  /** Human-readable byte size for the refusal toast (`4.1 MB`, `812.0 KB`, `7 B`). */
68
83
  export function formatPasteSize(bytes) {
69
84
  if (bytes >= 1024 * 1024)
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Pure: no node/React imports, callable from any test environment.
22
22
  */
23
- import { ENTER_NEWLINE_FRAME } from './limits.js';
23
+ import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME } from './limits.js';
24
24
  /** Enable / disable win32-input-mode (private mode 9001). */
25
25
  export const WIN32_INPUT_ENABLE = '\x1b[?9001h';
26
26
  export const WIN32_INPUT_DISABLE = '\x1b[?9001l';
@@ -190,6 +190,12 @@ export function translateWin32Key(record) {
190
190
  // A modified Enter NEVER submits -- same rule as `ENTER_SEQUENCES`.
191
191
  return m === 0 ? '\r' : ENTER_NEWLINE_FRAME;
192
192
  case VK_TAB:
193
+ // Ctrl+I is the project-indexer trigger, and the ONLY place the two are
194
+ // tell-apart is right here: legacy encoding collapses it to the Tab
195
+ // byte. Shift keeps ESC [ Z (Shift+Tab) so the mode toggle stays intact,
196
+ // and Ctrl+Shift+I stays on that Shift branch.
197
+ if (ctrl && !shift && !alt)
198
+ return INDEX_KEY_FRAME;
193
199
  return shift ? '\x1b[Z' : '\t';
194
200
  case VK_ESCAPE:
195
201
  return alt ? '\x1b\x1b' : '\x1b';
@@ -292,6 +292,15 @@ export type TeamLogEvent = {
292
292
  subject?: string;
293
293
  body?: string;
294
294
  };
295
+ } | {
296
+ type: 'overseer';
297
+ dispatchId: string;
298
+ label?: string;
299
+ decision?: {
300
+ action?: string;
301
+ reason?: string;
302
+ };
303
+ trigger?: string;
295
304
  } | {
296
305
  type: 'dispatch_end';
297
306
  dispatchId: string;
@@ -510,6 +510,17 @@ export function attachTeamEvents(logger, controller) {
510
510
  subject: event.message?.subject,
511
511
  });
512
512
  break;
513
+ case 'overseer':
514
+ // REASON ONLY, and at debug: interventions are routine supervision,
515
+ // not failures; the report and the panel carry them to the user.
516
+ logger.debug('agent', 'team_overseer', {
517
+ dispatchId: event.dispatchId,
518
+ label: event.label,
519
+ action: event.decision?.action,
520
+ trigger: event.trigger,
521
+ reason: event.decision?.reason,
522
+ });
523
+ break;
513
524
  case 'dispatch_end': {
514
525
  const outcome = event.outcome;
515
526
  const runs = outcome?.runs ?? [];
@@ -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
+ }
@@ -33,7 +33,18 @@ export declare class TeamHumanQueue {
33
33
  private readonly confirm;
34
34
  /** The chain. Never rejects: a failed request resolves `false` (deny). */
35
35
  private tail;
36
+ /**
37
+ * Labels with a request chained but not yet answered (team-overseer I-OV3).
38
+ *
39
+ * The queue pauses the CHILD's own watchdog on enqueue; the supervisor's
40
+ * soft silence timer needs the same fact one level up, or a user who takes
41
+ * four minutes over the first dialog burns inspection budget on siblings
42
+ * that are third in the FIFO and provably fine.
43
+ */
44
+ private readonly waiting;
36
45
  constructor(confirm: (req: ConfirmRequest) => Promise<boolean>);
46
+ /** Whether `label` has a confirmation queued or displayed right now. */
47
+ isWaiting(label: string): boolean;
37
48
  /**
38
49
  * Ask the human about `req` on behalf of `label`, one at a time.
39
50
  *
@@ -27,9 +27,22 @@ export class TeamHumanQueue {
27
27
  confirm;
28
28
  /** The chain. Never rejects: a failed request resolves `false` (deny). */
29
29
  tail = Promise.resolve();
30
+ /**
31
+ * Labels with a request chained but not yet answered (team-overseer I-OV3).
32
+ *
33
+ * The queue pauses the CHILD's own watchdog on enqueue; the supervisor's
34
+ * soft silence timer needs the same fact one level up, or a user who takes
35
+ * four minutes over the first dialog burns inspection budget on siblings
36
+ * that are third in the FIFO and provably fine.
37
+ */
38
+ waiting = new Set();
30
39
  constructor(confirm) {
31
40
  this.confirm = confirm;
32
41
  }
42
+ /** Whether `label` has a confirmation queued or displayed right now. */
43
+ isWaiting(label) {
44
+ return this.waiting.has(label);
45
+ }
33
46
  /**
34
47
  * Ask the human about `req` on behalf of `label`, one at a time.
35
48
  *
@@ -41,13 +54,17 @@ export class TeamHumanQueue {
41
54
  */
42
55
  request(child, label, req, signal) {
43
56
  child?.pauseIdleWatchdog();
57
+ this.waiting.add(label);
44
58
  const queued = this.enqueue(async () => {
45
59
  if (signal?.aborted)
46
60
  return false;
47
61
  return this.confirm({ ...req, summary: `[${label}] ${req.summary}` });
48
62
  });
49
63
  const raced = signal ? Promise.race([queued, abortsToFalse(signal)]) : queued;
50
- return raced.finally(() => child?.resumeIdleWatchdog());
64
+ return raced.finally(() => {
65
+ this.waiting.delete(label);
66
+ child?.resumeIdleWatchdog();
67
+ });
51
68
  }
52
69
  /**
53
70
  * Chain `run` after everything already queued.
@@ -101,6 +101,46 @@ export declare const TEAM_LIMITS: {
101
101
  * nothing a re-dispatch by the lead would not buy better. */
102
102
  readonly maxColdStartRetries: 1;
103
103
  readonly retryBackoffMs: 2000;
104
+ /**
105
+ * Wall-clock budget of everything one inspection may carry. The digest holds
106
+ * run facts, the supervisor's own memory and the tail of the child's live
107
+ * history; unbounded, one look at a child that read five big files would
108
+ * make the "cheap" supervision call cost more than the child (the argument
109
+ * FAST_LIMITS.digestMaxChars makes, one level up).
110
+ */
111
+ readonly overseerDigestBytes: 8000;
112
+ /** Clamp on a `nudge` guidance text. Advice, not a second brief. */
113
+ readonly overseerGuidanceChars: 1200;
114
+ /** Clamp on the decision `reason` recorded in the report. */
115
+ readonly overseerReasonChars: 200;
116
+ /**
117
+ * Inspections PER CHILD per dispatch. An indecisive supervisor must stop
118
+ * burning fast-tier calls; past the budget the child simply runs under its
119
+ * hard ceilings and the supervisor goes quiet.
120
+ */
121
+ readonly overseerMaxInspectionsPerChild: 4;
122
+ /**
123
+ * Rebuilds PER CHILD per dispatch. One: a replacement that also stalls dies
124
+ * on its own watchdog rather than ping-ponging fresh children at a task the
125
+ * supervisor already failed to rescue once.
126
+ */
127
+ readonly overseerMaxReplacementsPerChild: 1;
128
+ /**
129
+ * First wall-clock check when `team.subagentTimeoutMs === 0` (the shipped
130
+ * default): the supervisor still looks even when no ceiling was configured.
131
+ */
132
+ readonly overseerDefaultCheckMs: 300000;
133
+ /** Clamp on the model-chosen `nextCheckMs`, lower bound. */
134
+ readonly overseerNextCheckMinMs: 60000;
135
+ /** Clamp on the model-chosen `nextCheckMs`, upper bound. */
136
+ readonly overseerNextCheckMaxMs: 900000;
137
+ /**
138
+ * A child's OWN idle watchdog, multiplied by this while the supervisor is
139
+ * active, so the soft silence inspection at `idleTimeoutMs` always precedes
140
+ * the hard abort. The watchdog stays as the backstop for a child that
141
+ * wedges past every inspection.
142
+ */
143
+ readonly overseerWatchdogFactor: 4;
104
144
  };
105
145
  /**
106
146
  * The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).
@@ -106,6 +106,47 @@ export const TEAM_LIMITS = {
106
106
  * nothing a re-dispatch by the lead would not buy better. */
107
107
  maxColdStartRetries: 1,
108
108
  retryBackoffMs: 2_000,
109
+ // --- The dispatch supervisor (team-overseer) ---
110
+ /**
111
+ * Wall-clock budget of everything one inspection may carry. The digest holds
112
+ * run facts, the supervisor's own memory and the tail of the child's live
113
+ * history; unbounded, one look at a child that read five big files would
114
+ * make the "cheap" supervision call cost more than the child (the argument
115
+ * FAST_LIMITS.digestMaxChars makes, one level up).
116
+ */
117
+ overseerDigestBytes: 8_000,
118
+ /** Clamp on a `nudge` guidance text. Advice, not a second brief. */
119
+ overseerGuidanceChars: 1_200,
120
+ /** Clamp on the decision `reason` recorded in the report. */
121
+ overseerReasonChars: 200,
122
+ /**
123
+ * Inspections PER CHILD per dispatch. An indecisive supervisor must stop
124
+ * burning fast-tier calls; past the budget the child simply runs under its
125
+ * hard ceilings and the supervisor goes quiet.
126
+ */
127
+ overseerMaxInspectionsPerChild: 4,
128
+ /**
129
+ * Rebuilds PER CHILD per dispatch. One: a replacement that also stalls dies
130
+ * on its own watchdog rather than ping-ponging fresh children at a task the
131
+ * supervisor already failed to rescue once.
132
+ */
133
+ overseerMaxReplacementsPerChild: 1,
134
+ /**
135
+ * First wall-clock check when `team.subagentTimeoutMs === 0` (the shipped
136
+ * default): the supervisor still looks even when no ceiling was configured.
137
+ */
138
+ overseerDefaultCheckMs: 300_000,
139
+ /** Clamp on the model-chosen `nextCheckMs`, lower bound. */
140
+ overseerNextCheckMinMs: 60_000,
141
+ /** Clamp on the model-chosen `nextCheckMs`, upper bound. */
142
+ overseerNextCheckMaxMs: 900_000,
143
+ /**
144
+ * A child's OWN idle watchdog, multiplied by this while the supervisor is
145
+ * active, so the soft silence inspection at `idleTimeoutMs` always precedes
146
+ * the hard abort. The watchdog stays as the backstop for a child that
147
+ * wedges past every inspection.
148
+ */
149
+ overseerWatchdogFactor: 4,
109
150
  };
110
151
  /**
111
152
  * The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).