@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
@@ -17,32 +17,18 @@
17
17
  * whether an optional install step succeeded.
18
18
  *
19
19
  * NEVER THROWS. Zero survivors is the caller's single hard failure.
20
+ *
21
+ * EVERY CHILD RUNS THE LEAD'S MODEL (main-agent parity): the former
22
+ * `model: "fast"` per-child tier was removed, so a spec that still carries a
23
+ * `model` key simply ignores it - there is no tier to downgrade TO, and the
24
+ * honest reading of a stale key is silence, not a report line about a
25
+ * capability that no longer exists.
20
26
  */
21
27
  import type { SubagentSpec } from './types.js';
22
28
  export interface NormalizedSpecs {
23
29
  specs: SubagentSpec[];
24
30
  /** How many entries the model asked for, before dropping and capping. */
25
31
  requested: number;
26
- /**
27
- * Specs that asked for `model:"fast"` and were downgraded to `main`
28
- * (fast-model-tier §3.4 / R-6).
29
- *
30
- * ADDITIVE: `NormalizedSpecs` GAINS a field, it does not change one, so every
31
- * existing caller and test compiles unchanged.
32
- */
33
- downgraded: number;
34
- }
35
- /** Options for the fast tier. Optional for the reason `normalizeSubagentSpecs`
36
- * documents below (RV-14). */
37
- export interface NormalizeOptions {
38
- /**
39
- * §3.3's LIVE predicate, evaluated at DISPATCH time (RV-3).
40
- *
41
- * `fast.delegate: false` and "the tier stopped resolving twenty minutes ago"
42
- * are the same observable to the model — a downgrade, counted and reported —
43
- * which is the honest reading of both.
44
- */
45
- fastAvailable?: boolean;
46
32
  }
47
33
  /**
48
34
  * Repair and bound a raw `subagents` array.
@@ -55,10 +41,5 @@ export interface NormalizeOptions {
55
41
  * @param max The resolved `team.maxSubagents`; further clamped to
56
42
  * `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
57
43
  * cannot raise the requirement's ceiling (R-g).
58
- * @param opts Fast-tier availability. OPTIONAL, DEFAULTING TO `false` (RV-14):
59
- * this function and `NormalizedSpecs` are both exported and both
60
- * have existing callers and tests, so a required third parameter
61
- * would be a breaking edit that buys nothing — and an omitted one
62
- * now means exactly what a pre-feature build meant.
63
44
  */
64
- export declare function normalizeSubagentSpecs(raw: unknown, max: number, opts?: NormalizeOptions): NormalizedSpecs;
45
+ export declare function normalizeSubagentSpecs(raw: unknown, max: number): NormalizedSpecs;
@@ -17,6 +17,12 @@
17
17
  * whether an optional install step succeeded.
18
18
  *
19
19
  * NEVER THROWS. Zero survivors is the caller's single hard failure.
20
+ *
21
+ * EVERY CHILD RUNS THE LEAD'S MODEL (main-agent parity): the former
22
+ * `model: "fast"` per-child tier was removed, so a spec that still carries a
23
+ * `model` key simply ignores it - there is no tier to downgrade TO, and the
24
+ * honest reading of a stale key is silence, not a report line about a
25
+ * capability that no longer exists.
20
26
  */
21
27
  import { TEAM_LIMITS } from './limits.js';
22
28
  function asString(value) {
@@ -76,21 +82,13 @@ function dedupeLabel(label, taken) {
76
82
  * @param max The resolved `team.maxSubagents`; further clamped to
77
83
  * `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
78
84
  * cannot raise the requirement's ceiling (R-g).
79
- * @param opts Fast-tier availability. OPTIONAL, DEFAULTING TO `false` (RV-14):
80
- * this function and `NormalizedSpecs` are both exported and both
81
- * have existing callers and tests, so a required third parameter
82
- * would be a breaking edit that buys nothing — and an omitted one
83
- * now means exactly what a pre-feature build meant.
84
85
  */
85
- export function normalizeSubagentSpecs(raw, max, opts = {}) {
86
+ export function normalizeSubagentSpecs(raw, max) {
86
87
  if (!Array.isArray(raw))
87
- return { specs: [], requested: 0, downgraded: 0 };
88
+ return { specs: [], requested: 0 };
88
89
  const requested = raw.length;
89
- const fastAvailable = opts.fastAvailable === true;
90
90
  const kept = [];
91
91
  const taken = new Set();
92
- /** Parallel to `kept`: did entry `i` ask for the fast tier? */
93
- const wantedFast = [];
94
92
  for (let i = 0; i < raw.length; i += 1) {
95
93
  const item = raw[i];
96
94
  if (!item || typeof item !== 'object')
@@ -102,25 +100,14 @@ export function normalizeSubagentSpecs(raw, max, opts = {}) {
102
100
  const prompt = clamp(asString(src.prompt), TEAM_LIMITS.promptChars);
103
101
  if (prompt.length === 0)
104
102
  continue;
105
- // REPAIR, NEVER REJECT — this file's own contract. `'fast'` without
106
- // availability becomes `'main'` AND IS COUNTED; anything else (a typo, a
107
- // model id, `undefined`) is simply `'main'` and is NOT counted, because the
108
- // model did not ask for the fast tier and has nothing to be told about.
109
- const wantsFast = src.model === 'fast';
110
- wantedFast.push(wantsFast);
111
103
  kept.push({
112
104
  label: dedupeLabel(slugLabel(src.label, kept.length), taken),
113
105
  description,
114
106
  prompt,
115
107
  readOnly: src.readOnly === true,
116
- tier: wantsFast && fastAvailable ? 'fast' : 'main',
117
108
  });
118
109
  }
119
110
  const ceiling = Math.max(1, Math.min(Math.floor(max) || 1, TEAM_LIMITS.hardMaxSubagents));
120
111
  const specs = kept.slice(0, ceiling);
121
- // COUNTED OVER THE SURVIVORS, not over everything the model wrote: a spec the
122
- // cap dropped never ran at all, so reporting it as "ran on the main model"
123
- // would be a second, different lie in a line that exists to stop the first.
124
- const downgraded = specs.filter((spec, i) => spec.tier === 'main' && wantedFast[i] === true).length;
125
- return { specs, requested, downgraded };
112
+ return { specs, requested };
126
113
  }
@@ -18,17 +18,6 @@ export { TEAM_BLOCK_VERSION };
18
18
  export interface TeamBlockParams {
19
19
  maxSubagents: number;
20
20
  maxConcurrent: number;
21
- /**
22
- * Whether `model:"fast"` is honoured on `task` right now (fast-model-tier
23
- * §3.8).
24
- *
25
- * ONE SENTENCE, AND ONLY WHEN IT IS TRUE. `<fast_tier>` carries the full
26
- * guidance; this is the cross-reference that stops a model reading
27
- * `<team_mode>` in isolation and concluding every child costs the same. With
28
- * it absent the block is BYTE-IDENTICAL to the pre-feature output, which is
29
- * what `--no-fast` and every existing snapshot depend on.
30
- */
31
- fastDelegation?: boolean;
32
21
  }
33
22
  /**
34
23
  * The lead's `<team_mode>` block. Under 1200 characters on purpose: it is paid
@@ -44,19 +44,10 @@ export function buildTeamBlock(params) {
44
44
  '',
45
45
  'Each subagent starts with no memory of this conversation, so its prompt must',
46
46
  'carry everything it needs: the goal, the files or areas it owns, and what to',
47
- 'report back. Give each one a disjoint set of files to write. Subagents cannot',
48
- 'dispatch further subagents and cannot ask the user anything.',
47
+ 'report back. Give each one a disjoint set of files to write. Every subagent',
48
+ 'runs on your own model. Subagents cannot dispatch further subagents and',
49
+ 'cannot ask the user anything.',
49
50
  '',
50
- // THE TRAILING BLANK IS INSIDE THE CONDITIONAL, not outside it. An
51
- // unconditional `''` here would add a line to EVERY team-enabled prompt and
52
- // silently break the byte-identity `--no-fast` depends on (I-2).
53
- ...(params.fastDelegation
54
- ? [
55
- 'A subagent doing mechanical, high-volume work can run on the cheaper fast',
56
- 'model - see <fast_tier>.',
57
- '',
58
- ]
59
- : []),
60
51
  'You get one combined report when they all finish. Read it before deciding what',
61
52
  'to do next, and tell the user what the team found in your own words.',
62
53
  '</team_mode>',
@@ -23,26 +23,11 @@
23
23
  import type { ModelCost } from '@aragon-agent/core';
24
24
  import type { DispatchOutcome, SubagentRun } from './types.js';
25
25
  export interface ReportOptions {
26
- /** The lead's model cost table, so the header can state real spend (R-5). */
27
- cost?: ModelCost;
28
- /**
29
- * The FAST model's cost table (fast-model-tier §3.4 / R-7).
30
- *
31
- * A SECOND TABLE, not a second number derived from the first. Summing
32
- * fast-tier tokens at the lead's price over-reports a Haiku child under a
33
- * Sonnet lead by roughly an order of magnitude, and the whole justification
34
- * for delegating to a cheaper model is the figure on this line.
35
- */
36
- fastCost?: ModelCost;
37
- /**
38
- * The static price table has never heard of the fast model (C-11 / RV-4).
26
+ /** The lead's model cost table, so the header can state real spend (R-5).
39
27
  *
40
- * UNKNOWN PRICING IS NOT ZERO PRICING. `buildRuntimeModel` hands back
41
- * `cost: { input: 0, output: 0 }` for an unrecognised id, and the fast tier is
42
- * precisely where an unrecognised id is LIKELY — so rendering it as `$0.00`
43
- * would make the feature look free while it is spending money.
44
- */
45
- fastPricingUnknown?: boolean;
28
+ * THE ONE TABLE, because every child runs the lead's own model
29
+ * (main-agent parity): one tier, one price, one honest total. */
30
+ cost?: ModelCost;
46
31
  }
47
32
  /**
48
33
  * Cut `text` to at most `maxBytes` WITHOUT splitting a UTF-8 sequence.
@@ -106,28 +106,6 @@ function buildHeader(outcome, options) {
106
106
  `(${counts}) in ${formatDuration(durationMs)}.`);
107
107
  lines.push(`Tokens: in ${formatTokens(outcome.usage.inputTokens)}, out ` +
108
108
  `${formatTokens(outcome.usage.outputTokens)}. Cost: ${formatCost(computeCost(outcome.usage, options.cost))}.`);
109
- // A SECOND LINE, NOT A COMBINED TOTAL (fast-model-tier §3.4). Two tiers with
110
- // two price tables have two answers, and folding them into one figure is the
111
- // misattribution R-7 is about. Present only when a fast child actually ran, so
112
- // an ordinary dispatch's header is byte-identical.
113
- const fastUsage = outcome.fastUsage;
114
- if (fastUsage) {
115
- // `unknown` rather than a currency amount when the model has no price table
116
- // (RV-4): a lower bound rendered as a total is the same class of lie as
117
- // pricing a Haiku child at Sonnet rates.
118
- const fastCostLabel = options.fastPricingUnknown
119
- ? 'unknown (no price table)'
120
- : formatCost(computeCost(fastUsage, options.fastCost));
121
- lines.push(`Fast tier: in ${formatTokens(fastUsage.inputTokens)}, out ` +
122
- `${formatTokens(fastUsage.outputTokens)}. Cost: ${fastCostLabel}.`);
123
- }
124
- // THE LINE THAT KEEPS A DOWNGRADE FROM BEING SILENT (R-6). Without it the
125
- // model asked for a cheap child, got an expensive one, and has no way to learn
126
- // that its cost model is wrong.
127
- if (outcome.downgraded && outcome.downgraded > 0) {
128
- const n = outcome.downgraded;
129
- lines.push(`${n} ${n === 1 ? 'subagent' : 'subagents'} ran on the main model (fast tier off).`);
130
- }
131
109
  for (const conflict of findFileConflicts(outcome.runs)) {
132
110
  const who = conflict.labels.map((l) => `"${l}"`).join(' and ');
133
111
  lines.push(`WARNING: agents ${who} both wrote ${conflict.path} - review before trusting either.`);
@@ -139,10 +117,6 @@ function buildSection(run) {
139
117
  // the only place the lead learns that a child's first attempt died before it
140
118
  // did anything (F-4).
141
119
  const retried = run.retries ? `, retried ${run.retries}x` : '';
142
- // `[fast]` after the LABEL, on the head line, which is never trimmed. Six
143
- // bytes, and it is the only place the lead learns which of its children were
144
- // cheap — the fact the whole second cost line is about.
145
- const tier = run.tier === 'fast' ? ' [fast]' : '';
146
120
  // `compacted N` in the same position `retried Nx` uses, and for the same
147
121
  // reason (context-auto-compaction-hardening §3.4.4 / W3): this is the ONLY
148
122
  // place the lead learns that a child's own history was summarized mid-task, so
@@ -150,7 +124,7 @@ function buildSection(run) {
150
124
  // rather than an unexplained one. Absent when it never happened.
151
125
  const compacted = run.compactions ? `, compacted ${run.compactions}` : '';
152
126
  const head = [
153
- `### ${run.label}${tier} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
127
+ `### ${run.label} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
154
128
  `${run.turns} turns, ${run.toolCalls} tools${retried}${compacted}`,
155
129
  ];
156
130
  if (run.filesTouched.length > 0)
@@ -56,11 +56,11 @@ export interface TeamRuntimeDeps {
56
56
  /** Test seam (§8.1): a stub factory keeps the whole scheduler off the network. */
57
57
  agentFactory?: SubagentAgentFactory;
58
58
  /**
59
- * Per-tier model resolution (fast-model-tier §3.4). Passed straight through to
60
- * `SubagentDeps`; absent means every child runs on the session's model, which
61
- * is the pre-feature behaviour.
59
+ * Per-child fast review (main-agent parity), passed straight through to
60
+ * `SubagentDeps`. ABSENT means the session has no fast wiring and children
61
+ * are built exactly as they were before the reviewers existed.
62
62
  */
63
- resolveTier?: SubagentDeps['resolveTier'];
63
+ review?: SubagentDeps['review'];
64
64
  /**
65
65
  * Per-child context compaction (context-auto-compaction-hardening §3.4 / W3).
66
66
  *
@@ -104,12 +104,7 @@ export declare class TeamRuntime {
104
104
  isBusy(): boolean;
105
105
  /** The live roster, for `/team` status. `null` when nothing is dispatching. */
106
106
  snapshot(): TeamSnapshot | null;
107
- dispatch(specs: SubagentSpec[], requested: number, signal?: AbortSignal,
108
- /** Carried through from the normalizer so the report can state it (§3.4 /
109
- * R-6). Optional, so every existing caller compiles unchanged. */
110
- extra?: {
111
- downgraded?: number;
112
- }): Promise<DispatchOutcome>;
107
+ dispatch(specs: SubagentSpec[], requested: number, signal?: AbortSignal): Promise<DispatchOutcome>;
113
108
  /** One slot of the pool: take the next index until the list is exhausted. */
114
109
  private worker;
115
110
  /**
@@ -65,10 +65,7 @@ export class TeamRuntime {
65
65
  // -------------------------------------------------------------------------
66
66
  // Dispatch
67
67
  // -------------------------------------------------------------------------
68
- async dispatch(specs, requested, signal,
69
- /** Carried through from the normalizer so the report can state it (§3.4 /
70
- * R-6). Optional, so every existing caller compiles unchanged. */
71
- extra = {}) {
68
+ async dispatch(specs, requested, signal) {
72
69
  this.counter += 1;
73
70
  const dispatchId = `d${this.counter}`;
74
71
  const config = this.deps.getConfig();
@@ -131,7 +128,7 @@ export class TeamRuntime {
131
128
  confirmTools: config.confirmTools,
132
129
  ...(this.deps.humanQueue ? { humanQueue: this.deps.humanQueue } : {}),
133
130
  agentFactory: this.deps.agentFactory ?? defaultSubagentFactory,
134
- ...(this.deps.resolveTier ? { resolveTier: this.deps.resolveTier } : {}),
131
+ ...(this.deps.review ? { review: this.deps.review } : {}),
135
132
  // Site two of two, and the omission is silent in the same way the policy
136
133
  // one above is: declaring it on `TeamRuntimeDeps` and forgetting this line
137
134
  // type-checks and forwards nothing, so every child would quietly run with
@@ -147,11 +144,7 @@ export class TeamRuntime {
147
144
  // show the child its brief twice.
148
145
  const makeHandle = (spec) => createSubagent(spec, subDeps, {
149
146
  onUpdate: (run) => this.publish(dispatchId, run),
150
- // `tier` rides along so `App.tsx` can pick the RIGHT cost table for this
151
- // event rather than always using the lead's (§3.6 / R-7). Taken from the
152
- // SPEC, which the normalizer already downgraded when the tier was
153
- // unavailable, so it names the model that actually billed.
154
- onUsage: (label, usage) => this.emit({ type: 'usage', dispatchId, label, usage, tier: spec.tier }),
147
+ onUsage: (label, usage) => this.emit({ type: 'usage', dispatchId, label, usage }),
155
148
  });
156
149
  // ASSIGNMENT, not a declaration: a `const` here would shadow the `let` above
157
150
  // the bus, `canSend` would see the empty array forever, and every peer would
@@ -179,7 +172,15 @@ export class TeamRuntime {
179
172
  // so the override only bites because `task` listens to `ctx.signal` — and a
180
173
  // second, independent timer means a signal that never arrives still ends the
181
174
  // dispatch.
182
- this.dispatchTimer = setTimeout(() => this.abortAll(), team.dispatchTimeoutMs);
175
+ //
176
+ // `0` MEANS NO CEILING (main-agent parity): the dispatch lasts as long as
177
+ // its slowest child and only an Esc ends it early. The lead's idle
178
+ // watchdog is paused for the whole dispatch either way, exactly as it is
179
+ // for the lead's own long turns.
180
+ this.dispatchTimer =
181
+ team.dispatchTimeoutMs > 0
182
+ ? setTimeout(() => this.abortAll(), team.dispatchTimeoutMs)
183
+ : null;
183
184
  const runCtx = {
184
185
  dispatchId,
185
186
  handles,
@@ -195,24 +196,13 @@ export class TeamRuntime {
195
196
  signal?.removeEventListener('abort', onAbort);
196
197
  for (const handle of handles)
197
198
  handle.unsubscribe();
198
- // SUMMED PER TIER (fast-model-tier §3.4 / R-7). One total priced at the
199
- // lead's table would over-report a Haiku child under a Sonnet lead by
200
- // roughly an order of magnitude — and the whole justification for delegating
201
- // to a cheaper model is a number in the report this feeds.
202
- // `main` IS THE COMPLEMENT OF `fast`, not an equality test. `SubagentRun.tier`
203
- // is populated by `createSubagent` from the spec, but a hand-built run in a
204
- // test — or a run restored from a session file written before this field
205
- // existed — has it `undefined`, and `=== 'main'` would silently drop that
206
- // child's spend from the total. Under-reporting is the one failure this
207
- // split exists to prevent, so the default direction has to be "counted".
208
- const sumTier = (tier) => handles
209
- .filter((h) => (tier === 'fast' ? h.run.tier === 'fast' : h.run.tier !== 'fast'))
210
- .reduce((acc, h) => ({
199
+ // ONE TOTAL, AT THE LEAD'S TABLE. Every child runs the lead's own model
200
+ // (main-agent parity), so there is exactly one tier to sum and one price to
201
+ // sum it at; a split would be two answers to one question.
202
+ const usage = handles.reduce((acc, h) => ({
211
203
  inputTokens: acc.inputTokens + h.run.usage.inputTokens,
212
204
  outputTokens: acc.outputTokens + h.run.usage.outputTokens,
213
205
  }), { inputTokens: 0, outputTokens: 0 });
214
- const fastUsage = sumTier('fast');
215
- const ranFast = handles.some((h) => h.run.tier === 'fast');
216
206
  const outcome = {
217
207
  dispatchId,
218
208
  runs: handles.map((h) => ({ ...h.run, filesTouched: [...h.run.filesTouched] })),
@@ -221,11 +211,7 @@ export class TeamRuntime {
221
211
  endedAt: Date.now(),
222
212
  aborted: this.aborted,
223
213
  leadMail: bus.leadMail(),
224
- usage: sumTier('main'),
225
- // ABSENT when no fast child ran, which is what keeps a default session's
226
- // outcome shape byte-identical to the pre-feature one.
227
- ...(ranFast ? { fastUsage } : {}),
228
- ...(extra.downgraded ? { downgraded: extra.downgraded } : {}),
214
+ usage,
229
215
  };
230
216
  this.live = [];
231
217
  this.bus = null;
@@ -262,14 +248,23 @@ export class TeamRuntime {
262
248
  for (let attempt = 0;; attempt += 1) {
263
249
  const handle = handles[index];
264
250
  const { run, agent } = handle;
251
+ // ABORTS GO THROUGH THE HANDLE (never `agent.abort()` directly): the
252
+ // handle sets the synchronous `abortRequested` flag the child's fast
253
+ // reviewer reads as its never-steer-into-a-requested-abort fact.
265
254
  run.phase = 'starting';
266
255
  run.startedAt = Date.now();
267
256
  this.publish(dispatchId, run);
257
+ // `subagentTimeoutMs: 0` MEANS NO CEILING (main-agent parity): no timer
258
+ // is armed and `timedOut` can never become true. A wedged child is still
259
+ // ended by its OWN idle watchdog, which is the exact ceiling the lead
260
+ // runs under.
268
261
  let timedOut = false;
269
- const timer = setTimeout(() => {
270
- timedOut = true;
271
- agent.abort();
272
- }, subagentTimeoutMs);
262
+ const timer = subagentTimeoutMs > 0
263
+ ? setTimeout(() => {
264
+ timedOut = true;
265
+ handle.abort();
266
+ }, subagentTimeoutMs)
267
+ : null;
273
268
  try {
274
269
  await agent.prompt(handle.spec.prompt);
275
270
  }
@@ -281,7 +276,8 @@ export class TeamRuntime {
281
276
  run.error = err instanceof Error ? err.message : String(err);
282
277
  }
283
278
  finally {
284
- clearTimeout(timer);
279
+ if (timer)
280
+ clearTimeout(timer);
285
281
  }
286
282
  // THE TWO ABORT EXCLUSIONS THAT MAKE THE PREDICATE SAFE. Every abort in
287
283
  // this system reaches the provider as an aborted fetch, and `core` marks
@@ -319,7 +315,7 @@ export class TeamRuntime {
319
315
  const retries = (handle.run.retries ?? 0) + 1;
320
316
  handle.unsubscribe();
321
317
  try {
322
- handle.agent.abort();
318
+ handle.abort();
323
319
  }
324
320
  catch {
325
321
  // Already gone; this must not stop the retry.
@@ -346,7 +342,7 @@ export class TeamRuntime {
346
342
  if (this.aborted) {
347
343
  run.phase = 'aborted';
348
344
  }
349
- else if (timedOut) {
345
+ else if (timedOut && subagentTimeoutMs > 0) {
350
346
  run.phase = 'failed';
351
347
  run.error = run.error ?? `subagent exceeded ${Math.round(subagentTimeoutMs / 1000)}s`;
352
348
  }
@@ -398,7 +394,7 @@ export class TeamRuntime {
398
394
  this.aborted = true;
399
395
  for (const handle of this.live) {
400
396
  try {
401
- handle.agent.abort();
397
+ handle.abort();
402
398
  }
403
399
  catch {
404
400
  // A child that is already gone must not stop the others being aborted.
@@ -11,12 +11,12 @@
11
11
  * omission has a reason that is a correctness boundary rather than caution.
12
12
  */
13
13
  import type { ModelRole } from '../config/model-profiles.js';
14
- import { type AgentConfig, type AgentEvent, type AgentTool, type Message, type ModelRef, type ProviderRegistry, type SkillRegistry, type ThinkingLevel, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
14
+ import { type AgentConfig, type AgentEvent, type AgentTool, type Message, type ProviderRegistry, type SkillRegistry, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
15
15
  import type { AgentMode } from '../agent/agent-mode.js';
16
16
  import type { ChildContextManagerProvider } from '../compaction/child.js';
17
17
  import type { CliConfig } from '../config/schema.js';
18
18
  import type { ToolPermission } from '../exec/permission.js';
19
- import type { FastTierName } from '../fast/types.js';
19
+ import type { ChildReviewProvider } from './child-reviewer.js';
20
20
  import { TeamBus } from './bus.js';
21
21
  import type { TeamHumanQueue, WatchdogPausable } from './human-queue.js';
22
22
  import type { SubagentRun, SubagentSpec } from './types.js';
@@ -31,6 +31,10 @@ export interface SubagentAgentLike extends WatchdogPausable {
31
31
  prompt(text: string): Promise<void>;
32
32
  abort(): void;
33
33
  subscribe(listener: (event: AgentEvent) => void): () => void;
34
+ /** Steering surface for the child's own fast reviewer (core `Agent.steer`). */
35
+ steer(text: string): void;
36
+ /** Queued-block hygiene on abort, again mirroring the lead (D-21). */
37
+ clearAllQueues(): void;
34
38
  /**
35
39
  * The child's live history, for its own context manager
36
40
  * (context-auto-compaction-hardening §3.4.3 / W3).
@@ -95,23 +99,11 @@ export interface SubagentDeps {
95
99
  humanQueue?: TeamHumanQueue;
96
100
  agentFactory: SubagentAgentFactory;
97
101
  /**
98
- * Which model and thinking level a tier runs on (fast-model-tier §3.4).
99
- *
100
- * OPTIONAL, defaulting to the session's own model, so every existing caller
101
- * and test compiles unchanged and a `--no-fast` session builds children
102
- * exactly as it always did.
103
- *
104
- * `resolveTier('fast')` RETURNS THE MAIN TIER when the fast tier is not
105
- * available, which is what makes the two guards agree BY CONSTRUCTION: the
106
- * normalizer can never produce a `tier: 'fast'` spec this factory would
107
- * refuse, and this factory can never build a child against a `ModelRef` the
108
- * tier stopped resolving twenty minutes ago (RV-3 / R-14).
102
+ * Per-child fast review (main-agent parity): the controller's shared
103
+ * provider, built on the lead's `FastWiring`. ABSENT means the session
104
+ * has no fast wiring, and the child is then exactly what it always was.
109
105
  */
110
- resolveTier?: (tier: FastTierName) => {
111
- ref: ModelRef;
112
- thinkingLevel: ThinkingLevel;
113
- role?: ModelRole;
114
- };
106
+ review?: ChildReviewProvider;
115
107
  /**
116
108
  * Build this child its own context manager
117
109
  * (context-auto-compaction-hardening §3.4.3 / W3).
@@ -134,6 +126,14 @@ export interface SubagentHandle {
134
126
  run: SubagentRun;
135
127
  agent: SubagentAgentLike;
136
128
  tools: AgentTool[];
129
+ /**
130
+ * THE HANDLE-LEVEL ABORT, and the only one `TeamRuntime` may call. It sets a
131
+ * synchronous `abortRequested` flag BEFORE `agent.abort()`, which is what the
132
+ * child's fast reviewer reads as its guard-1 fact: an Esc or a dispatch stop
133
+ * must be visible to the reviewer in the same tick it happens, or the
134
+ * reviewer steers a `<fast_review>` block into a run that is already dying.
135
+ */
136
+ abort: () => void;
137
137
  unsubscribe: () => void;
138
138
  }
139
139
  /**