@aragon-agent/cli 0.6.17 → 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 (43) hide show
  1. package/CHANGELOG.md +107 -51
  2. package/README.md +48 -2
  3. package/dist/agent/controller.js +13 -0
  4. package/dist/agent/headless.js +7 -0
  5. package/dist/commands/builtins.js +18 -3
  6. package/dist/commands/index-build.d.ts +39 -0
  7. package/dist/commands/index-build.js +61 -0
  8. package/dist/config/cli-commands.d.ts +1 -1
  9. package/dist/config/cli-commands.js +3 -0
  10. package/dist/config/schema.d.ts +24 -4
  11. package/dist/config/schema.js +4 -0
  12. package/dist/exec/events.d.ts +4 -1
  13. package/dist/exec/runner.js +13 -0
  14. package/dist/input/csiu-keys.js +13 -2
  15. package/dist/input/limits.d.ts +15 -0
  16. package/dist/input/limits.js +15 -0
  17. package/dist/input/win32-input-mode.js +7 -1
  18. package/dist/logging/install.d.ts +9 -0
  19. package/dist/logging/install.js +11 -0
  20. package/dist/team/human-queue.d.ts +11 -0
  21. package/dist/team/human-queue.js +18 -1
  22. package/dist/team/limits.d.ts +40 -0
  23. package/dist/team/limits.js +41 -0
  24. package/dist/team/overseer.d.ts +157 -0
  25. package/dist/team/overseer.js +348 -0
  26. package/dist/team/report.js +26 -1
  27. package/dist/team/runtime.d.ts +71 -6
  28. package/dist/team/runtime.js +380 -23
  29. package/dist/team/subagent.d.ts +30 -1
  30. package/dist/team/subagent.js +8 -1
  31. package/dist/team/task-tool.js +4 -1
  32. package/dist/team/types.d.ts +63 -0
  33. package/dist/ui/App.d.ts +1 -1
  34. package/dist/ui/App.js +61 -0
  35. package/dist/ui/Header.js +13 -2
  36. package/dist/ui/PromptInput.js +11 -1
  37. package/dist/ui/overlays/HelpOverlay.js +1 -0
  38. package/package.json +2 -2
  39. package/skills/project-indexer/SKILL.md +163 -0
  40. package/skills/project-indexer/templates/cleancode-overrides.md +46 -0
  41. package/skills/project-indexer/templates/cleancode-template.md +94 -0
  42. package/skills/project-indexer/templates/config-template.md +69 -0
  43. package/skills/project-indexer/templates/index-template.md +129 -0
@@ -0,0 +1,348 @@
1
+ /**
2
+ * The dispatch supervisor (team-overseer).
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
+ * A TIME CEILING ON A CHILD IS AN INSPECTION TRIGGER, NOT A DEATH SENTENCE.
8
+ * When a child stalls (event silence) or simply runs long (the wall clock),
9
+ * the supervisor looks at it through ONE bounded fast-tier call and decides:
10
+ * keep waiting, steer a nudge in to help it recover, rebuild it with an
11
+ * amended brief, or abandon it with a recorded reason. Only the supervisor
12
+ * "decides"; the RUNTIME applies (see `runtime.ts`), because every child
13
+ * lifecycle change must funnel through the worker loop that awaits it
14
+ * (I-OV1).
15
+ *
16
+ * THE TRANSPORT IS THE REVIEWER'S, DELIBERATELY. One `complete` call, no
17
+ * tools, thinking off, temperature 0, output capped at
18
+ * `FAST_LIMITS.reviewOutputTokens` and wall clock at
19
+ * `FAST_LIMITS.reviewTimeoutMs` — a supervision call that could itself hang
20
+ * would need a second supervisor, which is the regress this design refuses.
21
+ *
22
+ * REPAIR, NEVER REJECT (the `normalizeSubagentSpecs` rule). An unparseable
23
+ * or out-of-budget decision becomes `wait` with the default interval: the
24
+ * failure mode of a supervisor must be "looked and let it run", never
25
+ * "looked and killed it" (I-OV2).
26
+ */
27
+ import { resolveFastTier } from '../fast/resolve.js';
28
+ import { assistantText } from '../fast/review-call.js';
29
+ import { FAST_LIMITS } from '../fast/limits.js';
30
+ import { truncateBytes } from './report.js';
31
+ import { TEAM_LIMITS } from './limits.js';
32
+ /** How many trailing history messages the digest carries. */
33
+ const DIGEST_TAIL_MESSAGES = 8;
34
+ /** Per-message text clamp inside the digest. */
35
+ const DIGEST_MSG_CHARS = 400;
36
+ function messageText(message) {
37
+ if (message.role === 'assistant') {
38
+ const parts = [];
39
+ for (const block of message.content) {
40
+ if (block.type === 'text')
41
+ parts.push(block.text);
42
+ if (block.type === 'tool_call')
43
+ parts.push(`(tool_call ${block.toolName})`);
44
+ }
45
+ return parts.join(' ').trim();
46
+ }
47
+ if (typeof message.content === 'string')
48
+ return message.content;
49
+ return message.content
50
+ .filter((p) => p.type === 'text')
51
+ .map((p) => (p.type === 'text' ? p.text : ''))
52
+ .join(' ')
53
+ .trim();
54
+ }
55
+ function clampChars(text, max) {
56
+ return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 3))}...`;
57
+ }
58
+ /**
59
+ * Build the bounded text one inspection reads.
60
+ *
61
+ * FACTS FIRST, HISTORY SECOND: the byte clamp (`overseerDigestBytes`, via the
62
+ * same `truncateBytes` the report uses so a CJK tail cannot be split mid
63
+ * sequence) bites the tail, and the tail is the least decisive part — a
64
+ * supervisor that cannot see the phase, the retry state and its own memory
65
+ * is worse than one missing the fifth message back.
66
+ */
67
+ export function buildChildDigest(input) {
68
+ const { run, childAlive, trigger, messages, memory, elapsedSec } = input;
69
+ const lines = [];
70
+ lines.push(`child: ${run.label} "${run.description}"`);
71
+ lines.push(`trigger: ${trigger === 'silence' ? 'event silence (suspected stall)' : trigger === 'clock' ? 'wall-clock check' : 'post-mortem (child already ended)'}`);
72
+ lines.push(`elapsed: ${Math.round(elapsedSec)}s, alive: ${childAlive ? 'yes' : 'no'}`);
73
+ lines.push(`phase: ${run.phase}, turns: ${run.turns}, toolCalls: ${run.toolCalls}` +
74
+ (run.lastTool ? `, lastTool: ${run.lastTool}` : ''));
75
+ if (run.retry)
76
+ lines.push(`api-retry: attempt ${run.retry.attempt}/${run.retry.maxRetries} (legitimate backoff, not a stall)`);
77
+ if (run.error)
78
+ lines.push(`error: ${clampChars(run.error, DIGEST_MSG_CHARS)}`);
79
+ if (run.filesTouched.length > 0)
80
+ lines.push(`files: ${run.filesTouched.join(', ')}`);
81
+ if (memory.length > 0) {
82
+ lines.push('previous supervisor decisions:');
83
+ for (const entry of memory) {
84
+ lines.push(` [${entry.atSec}s] ${entry.action}: ${clampChars(entry.reason, 120)}`);
85
+ }
86
+ }
87
+ const tail = messages.slice(-DIGEST_TAIL_MESSAGES);
88
+ if (tail.length > 0) {
89
+ lines.push('history tail:');
90
+ for (const message of tail) {
91
+ const text = clampChars(messageText(message), DIGEST_MSG_CHARS);
92
+ lines.push(` ${message.role}: ${text.length > 0 ? text : '(no text)'}`);
93
+ }
94
+ }
95
+ return truncateBytes(lines.join('\n'), TEAM_LIMITS.overseerDigestBytes);
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Decision normalization
99
+ // ---------------------------------------------------------------------------
100
+ const VALID_ACTIONS = new Set(['wait', 'nudge', 'replace', 'abandon']);
101
+ /** Clamp a model-chosen next check into the structural range. */
102
+ export function clampNextCheckMs(ms) {
103
+ const n = typeof ms === 'number' ? ms : Number.NaN;
104
+ if (!Number.isFinite(n) || n <= 0)
105
+ return undefined;
106
+ return Math.min(TEAM_LIMITS.overseerNextCheckMaxMs, Math.max(TEAM_LIMITS.overseerNextCheckMinMs, Math.round(n)));
107
+ }
108
+ function waitDecision(reason, nextCheckMs) {
109
+ return {
110
+ action: 'wait',
111
+ reason: clampChars(reason, TEAM_LIMITS.overseerReasonChars),
112
+ ...(nextCheckMs !== undefined ? { nextCheckMs } : {}),
113
+ };
114
+ }
115
+ /**
116
+ * Normalize a raw (JSON-parsed or garbage) decision into a safe one.
117
+ *
118
+ * `childAlive === false` PHYSICALLY REMOVES two actions: a dead child can be
119
+ * replaced or abandoned, never waited on or nudged, so those normalize to
120
+ * `abandon` rather than being rejected — a post-mortem answer the protocol
121
+ * cannot honour must still produce a decision the runtime can apply.
122
+ */
123
+ export function normalizeOverseerDecision(raw, childAlive) {
124
+ const obj = (raw && typeof raw === 'object' ? raw : {});
125
+ const action = typeof obj.action === 'string' && VALID_ACTIONS.has(obj.action)
126
+ ? obj.action
127
+ : 'wait';
128
+ const reason = typeof obj.reason === 'string' && obj.reason.trim().length > 0
129
+ ? obj.reason.trim()
130
+ : 'no reason given';
131
+ const guidance = typeof obj.guidance === 'string' && obj.guidance.trim().length > 0
132
+ ? clampChars(obj.guidance.trim(), TEAM_LIMITS.overseerGuidanceChars)
133
+ : undefined;
134
+ const nextCheckMs = clampNextCheckMs(obj.nextCheckMs);
135
+ if (!childAlive && (action === 'wait' || action === 'nudge')) {
136
+ return {
137
+ action: 'abandon',
138
+ reason: clampChars(`child already ended; supervisor answered ${action}: ${reason}`, TEAM_LIMITS.overseerReasonChars),
139
+ };
140
+ }
141
+ return {
142
+ action,
143
+ reason: clampChars(reason, TEAM_LIMITS.overseerReasonChars),
144
+ ...(guidance !== undefined && action === 'nudge' ? { guidance } : {}),
145
+ ...(action === 'wait' && nextCheckMs !== undefined ? { nextCheckMs } : {}),
146
+ };
147
+ }
148
+ /**
149
+ * Parse the model's answer text as one JSON decision object.
150
+ *
151
+ * Forgiving on PURPOSE: models wrap JSON in prose and fences. The FIRST `{`
152
+ * to the LAST `}` is the only span tried, and anything that does not parse is
153
+ * `wait` — the caller never sees a throw from parsing.
154
+ */
155
+ export function parseOverseerDecisionText(text, childAlive) {
156
+ const first = text.indexOf('{');
157
+ const last = text.lastIndexOf('}');
158
+ if (first >= 0 && last > first) {
159
+ try {
160
+ return normalizeOverseerDecision(JSON.parse(text.slice(first, last + 1)), childAlive);
161
+ }
162
+ catch {
163
+ // Fall through to the safe default.
164
+ }
165
+ }
166
+ return waitDecision('decision unparseable; defaulting to wait');
167
+ }
168
+ const OVERSEER_SYSTEM_PROMPT = [
169
+ 'You are the supervisor of a team of subagents. You are shown ONE subagent',
170
+ 'that crossed a time trigger (event silence or a wall-clock check) and must',
171
+ 'decide what to do with it. Answer with ONE JSON object and nothing else:',
172
+ '{"action": "wait" | "nudge" | "replace" | "abandon", "reason": "<= 200 chars",',
173
+ ' "guidance": "<= 1200 chars, nudge only", "nextCheckMs": <ms, wait only>}.',
174
+ 'wait: the child looks healthy or is legitimately blocked (API backoff,',
175
+ ' waiting on a teammate or a human confirmation) - keep waiting, pick the',
176
+ ' next check delay.',
177
+ 'nudge: the child is stuck in a way a hint can fix (wrong path, retrying a',
178
+ ' failing command, circling) - write the hint as guidance.',
179
+ 'replace: the child cannot recover - a fresh child will be built with your',
180
+ ' reason and guidance prepended to its brief.',
181
+ 'abandon: the task is not worth more attempts - the child is stopped and',
182
+ ' your reason is reported to the lead.',
183
+ 'A nudge is only delivered after the child finishes its current tool call;',
184
+ 'a long-running tool is not a stall. Prefer the cheapest action that works.',
185
+ ].join('\n');
186
+ function buildInspectionRequest(params) {
187
+ return {
188
+ model: params.modelId,
189
+ ...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
190
+ apiKey: params.apiKey,
191
+ systemPrompt: OVERSEER_SYSTEM_PROMPT,
192
+ messages: [{ role: 'user', content: params.digest, timestamp: Date.now() }],
193
+ maxTokens: FAST_LIMITS.reviewOutputTokens,
194
+ thinkingLevel: 'off',
195
+ temperature: 0,
196
+ signal: params.signal,
197
+ };
198
+ }
199
+ /**
200
+ * The fast-tier-backed provider, mirroring `createChildReviewer`'s shape so
201
+ * the controller wires it the same way: shared transport, live predicates,
202
+ * fail-soft (any failure resolves to `wait` — I-OV2).
203
+ */
204
+ export function createFastOverseerProvider(deps) {
205
+ const tier = () => resolveFastTier(deps.getConfig(), deps.hasKey);
206
+ return {
207
+ active: () => deps.available() && tier().ok,
208
+ async inspect(req) {
209
+ const resolved = tier();
210
+ if (!deps.available() || !resolved.ok) {
211
+ return waitDecision('fast tier unavailable; defaulting to wait');
212
+ }
213
+ const apiKey = deps.getApiKey(resolved.ref.providerId, 'fast');
214
+ if (apiKey === undefined) {
215
+ return waitDecision('no fast-tier key; defaulting to wait');
216
+ }
217
+ const controller = new AbortController();
218
+ const timer = setTimeout(() => controller.abort(), FAST_LIMITS.reviewTimeoutMs);
219
+ try {
220
+ const message = await deps.complete(resolved.ref.providerId, buildInspectionRequest({
221
+ digest: req.digest,
222
+ modelId: resolved.ref.modelId,
223
+ ...(resolved.ref.baseUrl ? { baseUrl: resolved.ref.baseUrl } : {}),
224
+ apiKey,
225
+ signal: controller.signal,
226
+ }));
227
+ return parseOverseerDecisionText(assistantText(message), true);
228
+ }
229
+ catch {
230
+ // INCLUDING a timeout abort: supervision failing is wait, never kill.
231
+ return waitDecision('inspection call failed; defaulting to wait');
232
+ }
233
+ finally {
234
+ clearTimeout(timer);
235
+ }
236
+ },
237
+ };
238
+ }
239
+ export const OVERSEER_DEFAULT_NEXT_CHECK_MS = TEAM_LIMITS.overseerDefaultCheckMs;
240
+ /**
241
+ * Per-dispatch supervisor state: memory, budgets and the single-flight guard.
242
+ *
243
+ * THE CLASS NEVER TOUCHES A CHILD (I-OV1). It looks and decides; `runtime.ts`
244
+ * steers, flags, aborts and rebuilds, because only the worker loop can apply
245
+ * a lifecycle change without orphaning the replacement.
246
+ */
247
+ export class TeamOverseer {
248
+ provider;
249
+ options;
250
+ memory = new Map();
251
+ inspections = new Map();
252
+ replacements = new Map();
253
+ inFlight = new Set();
254
+ constructor(provider, options) {
255
+ this.provider = provider;
256
+ this.options = options;
257
+ }
258
+ /** Never throws: a provider bug must not take the dispatch down. */
259
+ isActive() {
260
+ try {
261
+ return this.provider.active();
262
+ }
263
+ catch {
264
+ return false;
265
+ }
266
+ }
267
+ inspectionsExhausted(label) {
268
+ return (this.inspections.get(label) ?? 0) >= TEAM_LIMITS.overseerMaxInspectionsPerChild;
269
+ }
270
+ replacementsLeft(label) {
271
+ return (this.replacements.get(label) ?? 0) < TEAM_LIMITS.overseerMaxReplacementsPerChild;
272
+ }
273
+ /** Called by the runtime when it APPLIES a replace, not when it merely reads one. */
274
+ noteReplacement(label) {
275
+ this.replacements.set(label, (this.replacements.get(label) ?? 0) + 1);
276
+ }
277
+ /**
278
+ * ONE inspection. `null` means QUIET — not active, no budget left, or one
279
+ * already in flight for this child — and the caller then does nothing,
280
+ * which is exactly what quiet should do.
281
+ */
282
+ async inspect(req) {
283
+ const label = req.run.label;
284
+ if (!this.isActive() || this.inspectionsExhausted(label) || this.inFlight.has(label)) {
285
+ return null;
286
+ }
287
+ this.inFlight.add(label);
288
+ const now = this.options.now?.() ?? Date.now();
289
+ const digest = buildChildDigest({
290
+ run: req.run,
291
+ childAlive: req.childAlive,
292
+ trigger: req.trigger,
293
+ messages: req.messages,
294
+ memory: this.memory.get(label) ?? [],
295
+ elapsedSec: Math.max(0, (now - (req.run.startedAt ?? now)) / 1000),
296
+ });
297
+ let decision;
298
+ try {
299
+ decision = await this.provider.inspect({
300
+ label,
301
+ goal: req.run.description,
302
+ digest,
303
+ });
304
+ }
305
+ catch {
306
+ decision = waitDecision('inspection threw; defaulting to wait');
307
+ }
308
+ finally {
309
+ this.inFlight.delete(label);
310
+ }
311
+ this.inspections.set(label, (this.inspections.get(label) ?? 0) + 1);
312
+ const entries = this.memory.get(label) ?? [];
313
+ entries.push({
314
+ atSec: Math.max(0, Math.round((now - this.options.startedAt) / 1000)),
315
+ action: decision.action,
316
+ reason: decision.reason,
317
+ });
318
+ this.memory.set(label, entries);
319
+ return decision;
320
+ }
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // Replacement brief
324
+ // ---------------------------------------------------------------------------
325
+ /** Headroom the amended brief keeps over the original prompt's clamp. */
326
+ const REPLACEMENT_NOTE_CHARS = 800;
327
+ /**
328
+ * Prepend the supervisor's note to a spec's prompt, bounding the TOTAL to
329
+ * `TEAM_LIMITS.promptChars` so the replacement fits exactly what a
330
+ * model-supplied spec would have been allowed to carry.
331
+ *
332
+ * THE NOTE TELLS THE FRESH CHILD WHAT THE DEAD ONE DID (`filesTouched` is
333
+ * best-effort, the same caveat the report's conflict warning carries): a
334
+ * replacement that rewrites a half-written file blind is not a recovery.
335
+ */
336
+ export function buildReplacementPrompt(spec, decision, previousRun) {
337
+ const files = previousRun.filesTouched.length > 0
338
+ ? ` It already wrote to: ${previousRun.filesTouched.join(', ')}.`
339
+ : '';
340
+ const guidance = decision.action === 'replace' && decision.guidance !== undefined
341
+ ? ` Guidance: ${decision.guidance}`
342
+ : '';
343
+ const note = clampChars(`[supervisor note] A previous attempt at this task ran ${previousRun.turns} turns ` +
344
+ `and was replaced. Reason: ${decision.reason}.${files}${guidance} ` +
345
+ `Verify rather than assume prior work is complete.\n\n`, REPLACEMENT_NOTE_CHARS);
346
+ const originalBudget = Math.max(0, TEAM_LIMITS.promptChars - note.length);
347
+ return `${note}${spec.prompt.slice(0, originalBudget)}`;
348
+ }
@@ -117,6 +117,11 @@ function buildSection(run) {
117
117
  // the only place the lead learns that a child's first attempt died before it
118
118
  // did anything (F-4).
119
119
  const retried = run.retries ? `, retried ${run.retries}x` : '';
120
+ // Same head-line position as `retried Nx`, for the same reason: the ONLY
121
+ // place the lead learns that a supervisor looked at this child (N times) or
122
+ // rebuilt it (team-overseer §3.7).
123
+ const overseen = run.interventions ? `, overseer ${run.interventions}` : '';
124
+ const replaced = run.replacements ? `, replaced ${run.replacements}x` : '';
120
125
  // `compacted N` in the same position `retried Nx` uses, and for the same
121
126
  // reason (context-auto-compaction-hardening §3.4.4 / W3): this is the ONLY
122
127
  // place the lead learns that a child's own history was summarized mid-task, so
@@ -125,7 +130,7 @@ function buildSection(run) {
125
130
  const compacted = run.compactions ? `, compacted ${run.compactions}` : '';
126
131
  const head = [
127
132
  `### ${run.label} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
128
- `${run.turns} turns, ${run.toolCalls} tools${retried}${compacted}`,
133
+ `${run.turns} turns, ${run.toolCalls} tools${retried}${compacted}${overseen}${replaced}`,
129
134
  ];
130
135
  if (run.filesTouched.length > 0)
131
136
  head.push(`files: ${run.filesTouched.join(', ')}`);
@@ -137,6 +142,23 @@ function buildSection(run) {
137
142
  const summary = (run.summary ?? '').trim();
138
143
  return { head, summary: summary.length > 0 ? summary : '(no summary)' };
139
144
  }
145
+ /**
146
+ * The supervisor's ledger (team-overseer §3.7): one line per APPLIED decision,
147
+ * so the lead can see who was nudged or replaced, when, and why. Budgeted by
148
+ * the same `reportMaxBytes` ladder as everything else - it renders as a block,
149
+ * so trimming it means trimming the blocks, which the ladder already does.
150
+ */
151
+ function buildOverseer(outcome) {
152
+ const list = outcome.interventions ?? [];
153
+ if (list.length === 0)
154
+ return [];
155
+ const lines = ['### Supervisor'];
156
+ for (const item of list) {
157
+ const at = Math.max(0, Math.round((item.at - outcome.startedAt) / 1000));
158
+ lines.push(`[${item.trigger} ${at}s] ${item.label}: ${item.action} - ${item.reason}`);
159
+ }
160
+ return lines;
161
+ }
140
162
  function buildLeadMail(outcome) {
141
163
  if (outcome.leadMail.length === 0)
142
164
  return [];
@@ -162,6 +184,7 @@ export function buildDispatchReport(outcome, options = {}) {
162
184
  const header = buildHeader(outcome, options);
163
185
  const sections = outcome.runs.map(buildSection);
164
186
  const mail = buildLeadMail(outcome);
187
+ const overseer = buildOverseer(outcome);
165
188
  const summaries = sections.map((s) => truncateBytes(s.summary, TEAM_LIMITS.summaryMaxBytes));
166
189
  const trimmed = summaries.map((s, i) => s.length < sections[i].summary.length);
167
190
  const render = (bodies) => {
@@ -174,6 +197,8 @@ export function buildDispatchReport(outcome, options = {}) {
174
197
  }
175
198
  blocks.push(lines.join('\n'));
176
199
  });
200
+ if (overseer.length > 0)
201
+ blocks.push(overseer.join('\n'));
177
202
  if (mail.length > 0)
178
203
  blocks.push(mail.join('\n'));
179
204
  return blocks.join('\n\n');
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * TeamRuntime — dispatch scheduling, budgets, the `TeamEvent` stream, abort and
3
- * disposal (team-subagents §3.2 / §3.5 / §3.8).
3
+ * disposal (team-subagents §3.2 / §3.5 / §3.8; team-overseer §3.4).
4
4
  *
5
5
  * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
6
  *
@@ -16,6 +16,16 @@
16
16
  * worker loops, awaited with `Promise.allSettled`. No worker abstraction, no
17
17
  * queue class, no event-loop trickery — every path is observable from the event
18
18
  * stream.
19
+ *
20
+ * THE DISPATCH SUPERVISOR (team-overseer) — a time ceiling is an inspection
21
+ * trigger, not a death sentence. When a supervisor is live for the dispatch,
22
+ * two SOFT timers per child (wall clock and event silence) fire an inspection;
23
+ * the supervisor decides wait / nudge / replace / abandon; THIS module applies.
24
+ * Lifecycle changes are funnelled exclusively through `runOne`, the only place
25
+ * a child's `prompt()` is awaited (I-OV1): the supervisor may steer, flag and
26
+ * abort, but a replacement is built by the worker loop or it is built for
27
+ * nobody. Without a supervisor, every path below is byte for byte the
28
+ * pre-feature behaviour.
19
29
  */
20
30
  import type { ModelRole } from '../config/model-profiles.js';
21
31
  import type { ProviderRegistry, SkillRegistry, ToolPolicyDecision, ToolPolicyVerdict } from '@aragon-agent/core';
@@ -23,6 +33,7 @@ import type { AgentMode } from '../agent/agent-mode.js';
23
33
  import type { CliConfig } from '../config/schema.js';
24
34
  import type { ToolPermission } from '../exec/permission.js';
25
35
  import type { TeamHumanQueue } from './human-queue.js';
36
+ import { type OverseerProvider } from './overseer.js';
26
37
  import { type SubagentAgentFactory, type SubagentDeps } from './subagent.js';
27
38
  import type { DispatchOutcome, SubagentSpec, TeamEventListener, TeamSnapshot } from './types.js';
28
39
  export interface TeamRuntimeDeps {
@@ -67,10 +78,17 @@ export interface TeamRuntimeDeps {
67
78
  * Passed straight through to `SubagentDeps`; absent means children get none,
68
79
  * which is the pre-hardening behaviour exactly. `AgentController` supplies it
69
80
  * from `CompactionWiring.childFactory()`, and the closure returns `undefined`
70
- * when compaction is unregistered or `compaction.subagents` is false - which is
71
- * what lets `subagent.ts` spread nothing at all.
81
+ * when compaction is unregistered or `compaction.subagents` is false - which
82
+ * is what lets `subagent.ts` spread nothing at all.
72
83
  */
73
84
  contextManagerFor?: SubagentDeps['contextManagerFor'];
85
+ /**
86
+ * The dispatch supervisor (team-overseer). ABSENT means the session has no
87
+ * supervision, and every path below is the pre-feature behaviour exactly.
88
+ * `active()` is read once per dispatch AND once per inspection (inside
89
+ * `TeamOverseer`), so a mid-dispatch `/fast off` stops the next look.
90
+ */
91
+ overseer?: OverseerProvider;
74
92
  }
75
93
  export declare class TeamRuntime {
76
94
  private readonly deps;
@@ -94,6 +112,16 @@ export declare class TeamRuntime {
94
112
  * quietly not for the others.
95
113
  */
96
114
  private readonly pendingBackoffs;
115
+ /**
116
+ * The dispatch supervisor, LIVE for the current dispatch only (team-overseer).
117
+ * Null when the provider is absent/disabled/off, and reset to null at every
118
+ * dispatch end so nothing supervision-shaped leaks across dispatches.
119
+ */
120
+ private overseer;
121
+ /** The soft timers, same lifetime as `overseer`. */
122
+ private supervision;
123
+ /** Applied interventions for the in-flight dispatch's outcome. */
124
+ private interventions;
97
125
  constructor(deps: TeamRuntimeDeps);
98
126
  subscribe(listener: TeamEventListener): () => void;
99
127
  /**
@@ -109,18 +137,55 @@ export declare class TeamRuntime {
109
137
  private worker;
110
138
  /**
111
139
  * Run one child to a terminal phase, replacing it once if its very first
112
- * request died on a transport error before it did anything at all (F-4).
140
+ * request died on a transport error before it did anything at all (F-4), and
141
+ * replacing it when the SUPERVISOR decides to (team-overseer §3.4).
142
+ *
143
+ * ONE LOOP, ALL LIFECYCLE: the cold-start replacement, the supervisor's
144
+ * replace and the post-mortem replace all swap `handles[index]` and continue
145
+ * THIS loop, because the await below is the only place a child is driven and
146
+ * a handle swapped anywhere else would never be (I-OV1).
113
147
  */
114
148
  private runOne;
115
149
  /**
116
- * Swap a cold-started child for a fresh one, carrying only what must survive.
150
+ * One post-mortem inspection, and possibly ONE replacement driven by a
151
+ * recursive `runOne` — the worker that called us has already moved on to the
152
+ * next index, so nobody else will drive the fresh child (I-OV1 holds here
153
+ * too, one frame deeper). Recursion depth is bounded by the replacement
154
+ * budget, which is 1.
155
+ */
156
+ private maybePostMortem;
157
+ /**
158
+ * Swap one child for a fresh one, carrying only what must survive.
117
159
  *
118
160
  * `this.live` IS `handles`, so the single assignment below also makes
119
161
  * `abortAll()` abort the NEW child and makes the outcome carry the new run.
120
162
  * Anyone tempted to "clean up" that aliasing into a defensive copy must update
121
163
  * both places.
164
+ *
165
+ * `verdict === null` is the COLD-START retry (F-4): identical spec, `retries`
166
+ * carried. A verdict is the SUPERVISOR's replace: an amended brief
167
+ * (`buildReplacementPrompt`), `replacements` carried, and the supervisor's
168
+ * budget spent HERE, at the moment the rebuild is real.
169
+ */
170
+ private replaceChild;
171
+ /**
172
+ * Whether a child's silence is a legitimate wait rather than a stall:
173
+ * `team_wait` (the comm tool already pauses the child's own watchdog for
174
+ * exactly this wait) or a confirmation queued on the one human slot.
175
+ */
176
+ private isBlocked;
177
+ /**
178
+ * One trigger handled: look, then apply. Fired from a supervision timer and
179
+ * therefore async-and-forgotten; nothing here may throw.
180
+ *
181
+ * QUIET (`decision === null` — provider gone, budget spent) CLEARS the
182
+ * child's timers: supervision that has nothing left to say should stop
183
+ * waking up, and the child's own hard ceilings take over.
122
184
  */
123
- private replaceForRetry;
185
+ private inspectAndApply;
186
+ private applyDecision;
187
+ /** Record one applied decision on the outcome, the run and the event stream. */
188
+ private recordIntervention;
124
189
  /**
125
190
  * The final phase of an attempt, in a FIXED ORDER: aborted, then timed out,
126
191
  * then error, then no-summary, then done.