@aragon-agent/cli 0.5.2 → 0.5.4

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 (87) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +193 -3
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +98 -7
  12. package/dist/commands/builtins.d.ts +2 -2
  13. package/dist/commands/builtins.js +133 -3
  14. package/dist/config/app-paths.d.ts +17 -0
  15. package/dist/config/app-paths.js +21 -0
  16. package/dist/config/cli-commands.d.ts +26 -2
  17. package/dist/config/cli-commands.js +83 -6
  18. package/dist/config/env.d.ts +3 -2
  19. package/dist/config/env.js +44 -6
  20. package/dist/config/history-commands.d.ts +20 -0
  21. package/dist/config/history-commands.js +71 -0
  22. package/dist/config/load.d.ts +13 -0
  23. package/dist/config/load.js +107 -6
  24. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  25. package/dist/config/migrate-state-out-of-config.js +85 -0
  26. package/dist/config/prompt-history.d.ts +86 -0
  27. package/dist/config/prompt-history.js +332 -0
  28. package/dist/config/schema.d.ts +176 -18
  29. package/dist/config/schema.js +162 -8
  30. package/dist/config/store.js +40 -4
  31. package/dist/config/ui-state.d.ts +52 -0
  32. package/dist/config/ui-state.js +149 -0
  33. package/dist/logging/install.d.ts +85 -0
  34. package/dist/logging/install.js +94 -0
  35. package/dist/logging/logger.d.ts +1 -1
  36. package/dist/session/persist.d.ts +16 -0
  37. package/dist/session/persist.js +19 -1
  38. package/dist/team/activity.d.ts +67 -0
  39. package/dist/team/activity.js +130 -0
  40. package/dist/team/bus.d.ts +190 -0
  41. package/dist/team/bus.js +366 -0
  42. package/dist/team/comm-tools.d.ts +61 -0
  43. package/dist/team/comm-tools.js +221 -0
  44. package/dist/team/human-queue.d.ts +55 -0
  45. package/dist/team/human-queue.js +72 -0
  46. package/dist/team/limits.d.ts +123 -0
  47. package/dist/team/limits.js +128 -0
  48. package/dist/team/normalize.d.ts +39 -0
  49. package/dist/team/normalize.js +106 -0
  50. package/dist/team/panel-rows.d.ts +48 -0
  51. package/dist/team/panel-rows.js +75 -0
  52. package/dist/team/prompt.d.ts +49 -0
  53. package/dist/team/prompt.js +87 -0
  54. package/dist/team/report.d.ts +58 -0
  55. package/dist/team/report.js +205 -0
  56. package/dist/team/retry.d.ts +48 -0
  57. package/dist/team/retry.js +56 -0
  58. package/dist/team/runtime.d.ts +122 -0
  59. package/dist/team/runtime.js +406 -0
  60. package/dist/team/subagent.d.ts +90 -0
  61. package/dist/team/subagent.js +294 -0
  62. package/dist/team/task-tool.d.ts +47 -0
  63. package/dist/team/task-tool.js +118 -0
  64. package/dist/team/types.d.ts +172 -0
  65. package/dist/team/types.js +13 -0
  66. package/dist/tools/index.d.ts +48 -2
  67. package/dist/tools/index.js +26 -1
  68. package/dist/ui/App.d.ts +22 -1
  69. package/dist/ui/App.js +144 -18
  70. package/dist/ui/StatusBar.d.ts +21 -0
  71. package/dist/ui/StatusBar.js +5 -2
  72. package/dist/ui/TeamPanel.d.ts +57 -0
  73. package/dist/ui/TeamPanel.js +134 -0
  74. package/dist/ui/Transcript.js +10 -0
  75. package/dist/ui/entries/TeamCard.d.ts +31 -0
  76. package/dist/ui/entries/TeamCard.js +39 -0
  77. package/dist/ui/glyphs.d.ts +10 -0
  78. package/dist/ui/glyphs.js +6 -0
  79. package/dist/ui/layout/AppShell.d.ts +11 -1
  80. package/dist/ui/layout/AppShell.js +3 -3
  81. package/dist/ui/overlays/HelpOverlay.js +13 -0
  82. package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
  83. package/dist/ui/overlays/SettingsScreen.js +38 -1
  84. package/dist/ui/transcript-text.js +20 -0
  85. package/dist/ui/use-startup-notices.d.ts +1 -1
  86. package/dist/ui/use-startup-notices.js +2 -1
  87. package/package.json +2 -2
@@ -0,0 +1,55 @@
1
+ /**
2
+ * TeamHumanQueue — serializes subagent confirmations onto the ONE human slot
3
+ * (team-subagents §3.11 / I-4).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
+ *
7
+ * `App.tsx` holds `humanRequest` as a SINGLE state slot and `resolveHuman`
8
+ * broadcasts one response to every pending entry, so two children raising a
9
+ * `--confirm` dialog at the same instant would double-resolve it. This queue is
10
+ * a plain promise chain that admits one request at a time and prefixes the
11
+ * summary with the child's label, so the user can always tell WHICH agent is
12
+ * asking.
13
+ *
14
+ * THE WATCHDOG IS PAUSED ON ENQUEUE, NOT ON DISPLAY (D-18 / P0-4). This is
15
+ * invariant I-3 one level down, and it is the second-most likely way to ship
16
+ * this feature broken. A child sitting third in the FIFO is emitting nothing,
17
+ * and its own `idleTimeout` (210 s by default) is armed: a user who takes four
18
+ * minutes over the first dialog would otherwise lose children 2 and 3 to their
19
+ * own watchdogs, each printing `[Agent] idle watchdog fired` over the TUI and
20
+ * each landing in the report as a failure that has nothing to do with the work.
21
+ * `controller.ts` records fixing exactly this for the LEAD's `--confirm` gate;
22
+ * repeating it for children would regress a fix this codebase already paid for.
23
+ * The wait starts when the promise is chained, which is where the time actually
24
+ * goes — hence enqueue, not display.
25
+ */
26
+ import type { ConfirmRequest } from '../tools/index.js';
27
+ /** The slice of `Agent` this queue needs. Structural, so tests can stub it. */
28
+ export interface WatchdogPausable {
29
+ pauseIdleWatchdog(): void;
30
+ resumeIdleWatchdog(): void;
31
+ }
32
+ export declare class TeamHumanQueue {
33
+ private readonly confirm;
34
+ /** The chain. Never rejects: a failed request resolves `false` (deny). */
35
+ private tail;
36
+ constructor(confirm: (req: ConfirmRequest) => Promise<boolean>);
37
+ /**
38
+ * Ask the human about `req` on behalf of `label`, one at a time.
39
+ *
40
+ * @param child Paused for the whole wait, queue time included. May be `null`
41
+ * in the window before the child's `Agent` exists.
42
+ * @param signal The child's `ctx.signal`. An abort resolves `false` so the
43
+ * child is never left owing an answer to a dialog nobody is
44
+ * going to reach.
45
+ */
46
+ request(child: WatchdogPausable | null, label: string, req: ConfirmRequest, signal?: AbortSignal): Promise<boolean>;
47
+ /**
48
+ * Chain `run` after everything already queued.
49
+ *
50
+ * The `catch` is what keeps the chain alive: an unhandled rejection here would
51
+ * poison `this.tail` and every LATER request would reject without ever being
52
+ * shown, which presents as "the second file write silently did nothing".
53
+ */
54
+ private enqueue;
55
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * TeamHumanQueue — serializes subagent confirmations onto the ONE human slot
3
+ * (team-subagents §3.11 / I-4).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
+ *
7
+ * `App.tsx` holds `humanRequest` as a SINGLE state slot and `resolveHuman`
8
+ * broadcasts one response to every pending entry, so two children raising a
9
+ * `--confirm` dialog at the same instant would double-resolve it. This queue is
10
+ * a plain promise chain that admits one request at a time and prefixes the
11
+ * summary with the child's label, so the user can always tell WHICH agent is
12
+ * asking.
13
+ *
14
+ * THE WATCHDOG IS PAUSED ON ENQUEUE, NOT ON DISPLAY (D-18 / P0-4). This is
15
+ * invariant I-3 one level down, and it is the second-most likely way to ship
16
+ * this feature broken. A child sitting third in the FIFO is emitting nothing,
17
+ * and its own `idleTimeout` (210 s by default) is armed: a user who takes four
18
+ * minutes over the first dialog would otherwise lose children 2 and 3 to their
19
+ * own watchdogs, each printing `[Agent] idle watchdog fired` over the TUI and
20
+ * each landing in the report as a failure that has nothing to do with the work.
21
+ * `controller.ts` records fixing exactly this for the LEAD's `--confirm` gate;
22
+ * repeating it for children would regress a fix this codebase already paid for.
23
+ * The wait starts when the promise is chained, which is where the time actually
24
+ * goes — hence enqueue, not display.
25
+ */
26
+ export class TeamHumanQueue {
27
+ confirm;
28
+ /** The chain. Never rejects: a failed request resolves `false` (deny). */
29
+ tail = Promise.resolve();
30
+ constructor(confirm) {
31
+ this.confirm = confirm;
32
+ }
33
+ /**
34
+ * Ask the human about `req` on behalf of `label`, one at a time.
35
+ *
36
+ * @param child Paused for the whole wait, queue time included. May be `null`
37
+ * in the window before the child's `Agent` exists.
38
+ * @param signal The child's `ctx.signal`. An abort resolves `false` so the
39
+ * child is never left owing an answer to a dialog nobody is
40
+ * going to reach.
41
+ */
42
+ request(child, label, req, signal) {
43
+ child?.pauseIdleWatchdog();
44
+ const queued = this.enqueue(async () => {
45
+ if (signal?.aborted)
46
+ return false;
47
+ return this.confirm({ ...req, summary: `[${label}] ${req.summary}` });
48
+ });
49
+ const raced = signal ? Promise.race([queued, abortsToFalse(signal)]) : queued;
50
+ return raced.finally(() => child?.resumeIdleWatchdog());
51
+ }
52
+ /**
53
+ * Chain `run` after everything already queued.
54
+ *
55
+ * The `catch` is what keeps the chain alive: an unhandled rejection here would
56
+ * poison `this.tail` and every LATER request would reject without ever being
57
+ * shown, which presents as "the second file write silently did nothing".
58
+ */
59
+ enqueue(run) {
60
+ const next = this.tail.then(run, run).catch(() => false);
61
+ this.tail = next;
62
+ return next;
63
+ }
64
+ }
65
+ /** Resolve `false` when `signal` aborts, with the listener cleaned up after. */
66
+ function abortsToFalse(signal) {
67
+ if (signal.aborted)
68
+ return Promise.resolve(false);
69
+ return new Promise((resolve) => {
70
+ signal.addEventListener('abort', () => resolve(false), { once: true });
71
+ });
72
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * TEAM_LIMITS — the single authority on every bound this subsystem enforces
3
+ * (team-subagents §3.5 / §3.6).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
7
+ *
8
+ * TWO KINDS OF NUMBER LIVE HERE AND THEY ARE NOT INTERCHANGEABLE.
9
+ *
10
+ * - The `TEAM_LIMITS` entries below are STRUCTURAL: they describe what the
11
+ * report format, the message channel and the panel can physically carry, and
12
+ * a user has no business tuning them. They are constants.
13
+ * - The six `team.*` config keys (`config/schema.ts`) are POLICY: how wide to
14
+ * fan out, how long to wait. They are clamped, persisted and user-facing.
15
+ *
16
+ * `hardMaxSubagents` sits on the structural side ON PURPOSE even though it
17
+ * bounds a config key: the requirement caps the fan-out at 10 (R-g), so a
18
+ * hand-edited `config.json` must not be able to raise it. It is applied twice —
19
+ * once in `clampTeamConfig` and once in `normalizeSubagentSpecs` — because a
20
+ * clamp that only runs on the read path leaves a bad value on disk.
21
+ *
22
+ * BYTES, NOT CHARACTERS, for every report budget (D-8 / I-9). `ToolExecutor`
23
+ * truncates combined tool text at 100 000 BYTES; a 24 000-character CJK report
24
+ * is 72 000 bytes, so a character-denominated cap would let the executor chop
25
+ * the report mid-section and take the failure count with it.
26
+ */
27
+ export declare const TEAM_LIMITS: {
28
+ /**
29
+ * The requirement's ceiling (R-g). Enforced in `clampTeamConfig` AND in
30
+ * `normalizeSubagentSpecs`, so neither a hand-edited file nor a model asking
31
+ * for thirty can exceed it.
32
+ */
33
+ readonly hardMaxSubagents: 10;
34
+ /** `SubagentSpec.description`, clamped (never rejected). */
35
+ readonly descriptionChars: 60;
36
+ /** `SubagentSpec.prompt`, clamped. The child cannot see the conversation, so
37
+ * this has to be generous enough to carry a whole brief. */
38
+ readonly promptChars: 8000;
39
+ /** Slugified `SubagentSpec.label`. Long enough to be meaningful in the panel,
40
+ * short enough that five of them fit one row of an 80-column terminal. */
41
+ readonly labelChars: 12;
42
+ readonly reportMaxBytes: 48000;
43
+ readonly summaryMaxBytes: 6000;
44
+ /**
45
+ * Floor a summary is trimmed TO, never below. A section trimmed to nothing is
46
+ * indistinguishable from a child that reported nothing, which is exactly the
47
+ * distinction the report exists to preserve.
48
+ */
49
+ readonly summaryFloorBytes: 400;
50
+ /** Deduped `write_file` / `edit_file` paths recorded per child (§3.4). */
51
+ readonly filesTouchedMax: 30;
52
+ /** `team_send` calls one child may spend on a whole dispatch. */
53
+ readonly messagesPerAgent: 6;
54
+ /** `team_send` calls the whole dispatch may spend. */
55
+ readonly messagesPerDispatch: 24;
56
+ /** Minimum gap between one child's sends. */
57
+ readonly minSendIntervalMs: 15000;
58
+ /** Quota units a `to: "all"` broadcast costs. */
59
+ readonly broadcastCost: 2;
60
+ readonly subjectChars: 80;
61
+ readonly bodyChars: 800;
62
+ /** `team_wait` timeout clamp, in seconds. */
63
+ readonly waitMinSeconds: 5;
64
+ readonly waitMaxSeconds: 120;
65
+ readonly waitDefaultSeconds: 30;
66
+ /**
67
+ * `agent_update` coalescing window (§5.2). Five children streaming tokens must
68
+ * not push five React renders per token; phase transitions always pass.
69
+ */
70
+ readonly agentUpdateThrottleMs: 120;
71
+ /** Agent rows the live panel renders before collapsing to `+N more` (§6.1). */
72
+ readonly panelMaxRows: 5;
73
+ /** Below this many terminal rows the panel collapses to its header line. */
74
+ readonly panelCollapseRows: 20;
75
+ /** Below this many columns the status-bar readout degrades to `[3]` (D-20). */
76
+ readonly statusCompactCols: 100;
77
+ /**
78
+ * STORAGE ceiling for a prose activity tail (P0-1). The most a `SubagentRun`
79
+ * may carry, NOT the width of the column: `TeamPanel.activityBudget(cols)`
80
+ * derives the display budget from the terminal and is always <= this.
81
+ */
82
+ readonly activityChars: 64;
83
+ /**
84
+ * Floor under `activityBudget`. Below roughly 46 columns the roster has bigger
85
+ * problems than an elided activity line, and a column narrower than this shows
86
+ * nothing but an ellipsis - at which point the phase word is more informative.
87
+ */
88
+ readonly activityMinChars: 12;
89
+ /** Rolling text buffer kept per child. The only per-token allocation. */
90
+ readonly activityTailChars: 160;
91
+ /** Per picked tool argument (P1-4). A path or a command longer than this is
92
+ * elided on capture, so a run can never carry more than a few hundred bytes
93
+ * of argument however large the real call was. */
94
+ readonly activityArgChars: 120;
95
+ /** At or above this many columns the activity line carries a full path or
96
+ * command; below it, a basename or a program name. Mirrors `statusCompactCols`.
97
+ * This chooses WHICH FACT to show; `activityBudget` chooses how much of it. */
98
+ readonly activityWideCols: 100;
99
+ /** Retries of a child that produced NOTHING. One, and not configurable: a
100
+ * second retry doubles the worst-case latency of a failing dispatch and buys
101
+ * nothing a re-dispatch by the lead would not buy better. */
102
+ readonly maxColdStartRetries: 1;
103
+ readonly retryBackoffMs: 2000;
104
+ };
105
+ /**
106
+ * The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).
107
+ *
108
+ * They are not in the lead's registered tool list, so evaluating them against
109
+ * the lead's `allowed-tools` ceiling would refuse them and dead-end the channel
110
+ * — the same dead end `SKILL_TOOL_FLOOR` exists to prevent one level up.
111
+ *
112
+ * Deliberately NOT added to `HOST_TOOL_NAMES` (P2-5). That constant answers
113
+ * "can a skill's `allowed-tools` declaration ever take effect?", and these two
114
+ * are never registered on a lead — so a skill naming them genuinely IS
115
+ * unenforceable, and `aragon skills doctor` reporting it as such is correct
116
+ * rather than a gap.
117
+ */
118
+ export declare const TEAM_SUBAGENT_TOOL_NAMES: ReadonlySet<string>;
119
+ /**
120
+ * Bumped whenever the wording of either prompt block changes, so a behaviour
121
+ * report can be tied to a block revision with one grep.
122
+ */
123
+ export declare const TEAM_BLOCK_VERSION = "v1-2026-07";
@@ -0,0 +1,128 @@
1
+ /**
2
+ * TEAM_LIMITS — the single authority on every bound this subsystem enforces
3
+ * (team-subagents §3.5 / §3.6).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
7
+ *
8
+ * TWO KINDS OF NUMBER LIVE HERE AND THEY ARE NOT INTERCHANGEABLE.
9
+ *
10
+ * - The `TEAM_LIMITS` entries below are STRUCTURAL: they describe what the
11
+ * report format, the message channel and the panel can physically carry, and
12
+ * a user has no business tuning them. They are constants.
13
+ * - The six `team.*` config keys (`config/schema.ts`) are POLICY: how wide to
14
+ * fan out, how long to wait. They are clamped, persisted and user-facing.
15
+ *
16
+ * `hardMaxSubagents` sits on the structural side ON PURPOSE even though it
17
+ * bounds a config key: the requirement caps the fan-out at 10 (R-g), so a
18
+ * hand-edited `config.json` must not be able to raise it. It is applied twice —
19
+ * once in `clampTeamConfig` and once in `normalizeSubagentSpecs` — because a
20
+ * clamp that only runs on the read path leaves a bad value on disk.
21
+ *
22
+ * BYTES, NOT CHARACTERS, for every report budget (D-8 / I-9). `ToolExecutor`
23
+ * truncates combined tool text at 100 000 BYTES; a 24 000-character CJK report
24
+ * is 72 000 bytes, so a character-denominated cap would let the executor chop
25
+ * the report mid-section and take the failure count with it.
26
+ */
27
+ export const TEAM_LIMITS = {
28
+ /**
29
+ * The requirement's ceiling (R-g). Enforced in `clampTeamConfig` AND in
30
+ * `normalizeSubagentSpecs`, so neither a hand-edited file nor a model asking
31
+ * for thirty can exceed it.
32
+ */
33
+ hardMaxSubagents: 10,
34
+ // --- Spec normalization (§3.3.1) ---
35
+ /** `SubagentSpec.description`, clamped (never rejected). */
36
+ descriptionChars: 60,
37
+ /** `SubagentSpec.prompt`, clamped. The child cannot see the conversation, so
38
+ * this has to be generous enough to carry a whole brief. */
39
+ promptChars: 8000,
40
+ /** Slugified `SubagentSpec.label`. Long enough to be meaningful in the panel,
41
+ * short enough that five of them fit one row of an 80-column terminal. */
42
+ labelChars: 12,
43
+ // --- Report budgeting (§3.7). BYTES. ---
44
+ reportMaxBytes: 48_000,
45
+ summaryMaxBytes: 6_000,
46
+ /**
47
+ * Floor a summary is trimmed TO, never below. A section trimmed to nothing is
48
+ * indistinguishable from a child that reported nothing, which is exactly the
49
+ * distinction the report exists to preserve.
50
+ */
51
+ summaryFloorBytes: 400,
52
+ /** Deduped `write_file` / `edit_file` paths recorded per child (§3.4). */
53
+ filesTouchedMax: 30,
54
+ // --- The team bus (§3.6). The "bu neng tai pin fan" clause, in numbers. ---
55
+ /** `team_send` calls one child may spend on a whole dispatch. */
56
+ messagesPerAgent: 6,
57
+ /** `team_send` calls the whole dispatch may spend. */
58
+ messagesPerDispatch: 24,
59
+ /** Minimum gap between one child's sends. */
60
+ minSendIntervalMs: 15_000,
61
+ /** Quota units a `to: "all"` broadcast costs. */
62
+ broadcastCost: 2,
63
+ subjectChars: 80,
64
+ bodyChars: 800,
65
+ /** `team_wait` timeout clamp, in seconds. */
66
+ waitMinSeconds: 5,
67
+ waitMaxSeconds: 120,
68
+ waitDefaultSeconds: 30,
69
+ /**
70
+ * `agent_update` coalescing window (§5.2). Five children streaming tokens must
71
+ * not push five React renders per token; phase transitions always pass.
72
+ */
73
+ agentUpdateThrottleMs: 120,
74
+ /** Agent rows the live panel renders before collapsing to `+N more` (§6.1). */
75
+ panelMaxRows: 5,
76
+ /** Below this many terminal rows the panel collapses to its header line. */
77
+ panelCollapseRows: 20,
78
+ /** Below this many columns the status-bar readout degrades to `[3]` (D-20). */
79
+ statusCompactCols: 100,
80
+ // --- Live activity (team-live-activity F-1) ---
81
+ /**
82
+ * STORAGE ceiling for a prose activity tail (P0-1). The most a `SubagentRun`
83
+ * may carry, NOT the width of the column: `TeamPanel.activityBudget(cols)`
84
+ * derives the display budget from the terminal and is always <= this.
85
+ */
86
+ activityChars: 64,
87
+ /**
88
+ * Floor under `activityBudget`. Below roughly 46 columns the roster has bigger
89
+ * problems than an elided activity line, and a column narrower than this shows
90
+ * nothing but an ellipsis - at which point the phase word is more informative.
91
+ */
92
+ activityMinChars: 12,
93
+ /** Rolling text buffer kept per child. The only per-token allocation. */
94
+ activityTailChars: 160,
95
+ /** Per picked tool argument (P1-4). A path or a command longer than this is
96
+ * elided on capture, so a run can never carry more than a few hundred bytes
97
+ * of argument however large the real call was. */
98
+ activityArgChars: 120,
99
+ /** At or above this many columns the activity line carries a full path or
100
+ * command; below it, a basename or a program name. Mirrors `statusCompactCols`.
101
+ * This chooses WHICH FACT to show; `activityBudget` chooses how much of it. */
102
+ activityWideCols: 100,
103
+ // --- Cold-start retry (team-live-activity F-4) ---
104
+ /** Retries of a child that produced NOTHING. One, and not configurable: a
105
+ * second retry doubles the worst-case latency of a failing dispatch and buys
106
+ * nothing a re-dispatch by the lead would not buy better. */
107
+ maxColdStartRetries: 1,
108
+ retryBackoffMs: 2_000,
109
+ };
110
+ /**
111
+ * The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).
112
+ *
113
+ * They are not in the lead's registered tool list, so evaluating them against
114
+ * the lead's `allowed-tools` ceiling would refuse them and dead-end the channel
115
+ * — the same dead end `SKILL_TOOL_FLOOR` exists to prevent one level up.
116
+ *
117
+ * Deliberately NOT added to `HOST_TOOL_NAMES` (P2-5). That constant answers
118
+ * "can a skill's `allowed-tools` declaration ever take effect?", and these two
119
+ * are never registered on a lead — so a skill naming them genuinely IS
120
+ * unenforceable, and `aragon skills doctor` reporting it as such is correct
121
+ * rather than a gap.
122
+ */
123
+ export const TEAM_SUBAGENT_TOOL_NAMES = new Set(['team_send', 'team_wait']);
124
+ /**
125
+ * Bumped whenever the wording of either prompt block changes, so a behaviour
126
+ * report can be tied to a block revision with one grep.
127
+ */
128
+ export const TEAM_BLOCK_VERSION = 'v1-2026-07';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `normalizeSubagentSpecs` — pure repair of whatever the model put in the `task`
3
+ * call (team-subagents §3.3.1).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
+ *
7
+ * MODELLED DIRECTLY ON `human-input.ts::normalizeQuestions`, INCLUDING THE
8
+ * REASON IT EXISTS. The JSON Schema declares SHAPE, not POLICY: `type` and
9
+ * `required` stay in the schema because a `subagents` value that is not an array
10
+ * has nothing for a normalizer to repair. Every BOUND lives in the tool's
11
+ * `description` (which is what the model reads) and is enforced here (which is
12
+ * deterministic and unit-tested). Putting `maxItems: 10` in the schema instead
13
+ * would behave differently depending on whether `ajv` — an OPTIONAL dependency
14
+ * of `@aragon-agent/core` — happened to install: present, the executor rejects an
15
+ * eleven-spec call outright and these rules are dead code; absent, the same call
16
+ * sails straight through to here. One design, two behaviours, selected by
17
+ * whether an optional install step succeeded.
18
+ *
19
+ * NEVER THROWS. Zero survivors is the caller's single hard failure.
20
+ */
21
+ import type { SubagentSpec } from './types.js';
22
+ export interface NormalizedSpecs {
23
+ specs: SubagentSpec[];
24
+ /** How many entries the model asked for, before dropping and capping. */
25
+ requested: number;
26
+ }
27
+ /**
28
+ * Repair and bound a raw `subagents` array.
29
+ *
30
+ * DROP FIRST, CAP SECOND. Capping first would let a blank entry consume a slot
31
+ * the model meant for real work — the fan-out would silently be one narrower
32
+ * than the model believed, and nothing would say so.
33
+ *
34
+ * @param raw Whatever arrived in `params.subagents`.
35
+ * @param max The resolved `team.maxSubagents`; further clamped to
36
+ * `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
37
+ * cannot raise the requirement's ceiling (R-g).
38
+ */
39
+ export declare function normalizeSubagentSpecs(raw: unknown, max: number): NormalizedSpecs;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `normalizeSubagentSpecs` — pure repair of whatever the model put in the `task`
3
+ * call (team-subagents §3.3.1).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
+ *
7
+ * MODELLED DIRECTLY ON `human-input.ts::normalizeQuestions`, INCLUDING THE
8
+ * REASON IT EXISTS. The JSON Schema declares SHAPE, not POLICY: `type` and
9
+ * `required` stay in the schema because a `subagents` value that is not an array
10
+ * has nothing for a normalizer to repair. Every BOUND lives in the tool's
11
+ * `description` (which is what the model reads) and is enforced here (which is
12
+ * deterministic and unit-tested). Putting `maxItems: 10` in the schema instead
13
+ * would behave differently depending on whether `ajv` — an OPTIONAL dependency
14
+ * of `@aragon-agent/core` — happened to install: present, the executor rejects an
15
+ * eleven-spec call outright and these rules are dead code; absent, the same call
16
+ * sails straight through to here. One design, two behaviours, selected by
17
+ * whether an optional install step succeeded.
18
+ *
19
+ * NEVER THROWS. Zero survivors is the caller's single hard failure.
20
+ */
21
+ import { TEAM_LIMITS } from './limits.js';
22
+ function asString(value) {
23
+ return typeof value === 'string' ? value : '';
24
+ }
25
+ function clamp(text, maxChars) {
26
+ const trimmed = text.trim();
27
+ return trimmed.length > maxChars ? trimmed.slice(0, maxChars) : trimmed;
28
+ }
29
+ /**
30
+ * Slugify a label to `[a-z0-9-]{1,12}`. An empty result falls back to the
31
+ * positional default so a child is never nameless in the panel.
32
+ */
33
+ function slugLabel(raw, index) {
34
+ const slug = asString(raw)
35
+ .trim()
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9-]+/g, '-')
38
+ .replace(/^-+|-+$/g, '')
39
+ .slice(0, TEAM_LIMITS.labelChars);
40
+ return slug.length > 0 ? slug : `a${index + 1}`;
41
+ }
42
+ /**
43
+ * Make `label` unique within the dispatch, case-insensitively, by appending
44
+ * `-2`, `-3`, ...
45
+ *
46
+ * Two agents the user cannot tell apart in the panel is worse than an ugly
47
+ * label, and the bus addresses children BY label — a duplicate would make
48
+ * `team_send` ambiguous, which is a correctness problem rather than a cosmetic
49
+ * one.
50
+ */
51
+ function dedupeLabel(label, taken) {
52
+ const base = label.toLowerCase();
53
+ if (!taken.has(base)) {
54
+ taken.add(base);
55
+ return label;
56
+ }
57
+ for (let n = 2; n < 100; n += 1) {
58
+ const suffix = `-${n}`;
59
+ const candidate = `${label.slice(0, TEAM_LIMITS.labelChars - suffix.length)}${suffix}`;
60
+ if (!taken.has(candidate.toLowerCase())) {
61
+ taken.add(candidate.toLowerCase());
62
+ return candidate;
63
+ }
64
+ }
65
+ taken.add(base);
66
+ return label;
67
+ }
68
+ /**
69
+ * Repair and bound a raw `subagents` array.
70
+ *
71
+ * DROP FIRST, CAP SECOND. Capping first would let a blank entry consume a slot
72
+ * the model meant for real work — the fan-out would silently be one narrower
73
+ * than the model believed, and nothing would say so.
74
+ *
75
+ * @param raw Whatever arrived in `params.subagents`.
76
+ * @param max The resolved `team.maxSubagents`; further clamped to
77
+ * `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
78
+ * cannot raise the requirement's ceiling (R-g).
79
+ */
80
+ export function normalizeSubagentSpecs(raw, max) {
81
+ if (!Array.isArray(raw))
82
+ return { specs: [], requested: 0 };
83
+ const requested = raw.length;
84
+ const kept = [];
85
+ const taken = new Set();
86
+ for (let i = 0; i < raw.length; i += 1) {
87
+ const item = raw[i];
88
+ if (!item || typeof item !== 'object')
89
+ continue;
90
+ const src = item;
91
+ const description = clamp(asString(src.description), TEAM_LIMITS.descriptionChars);
92
+ if (description.length === 0)
93
+ continue;
94
+ const prompt = clamp(asString(src.prompt), TEAM_LIMITS.promptChars);
95
+ if (prompt.length === 0)
96
+ continue;
97
+ kept.push({
98
+ label: dedupeLabel(slugLabel(src.label, kept.length), taken),
99
+ description,
100
+ prompt,
101
+ readOnly: src.readOnly === true,
102
+ });
103
+ }
104
+ const ceiling = Math.max(1, Math.min(Math.floor(max) || 1, TEAM_LIMITS.hardMaxSubagents));
105
+ return { specs: kept.slice(0, ceiling), requested };
106
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `selectPanelRows` - which children the live roster shows (team-live-activity
3
+ * F-2).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
+ *
7
+ * THE DEFECT THIS REPLACES. `TeamPanel` took the first `panelMaxRows` runs by
8
+ * ARRAY INDEX, and the slot pool hands out indices from a shared cursor in
9
+ * order - so at `maxSubagents: 8, maxConcurrent: 3` the first five rows are the
10
+ * five that finished first and everything still working sits behind `+3 more`.
11
+ * The one thing the requirement asks the panel for - show that subagents are
12
+ * running - is the one thing it stopped doing at the fan-out widths the
13
+ * requirement explicitly permits.
14
+ *
15
+ * Pure: a function of a roster snapshot and a row cap, with no clock, no
16
+ * terminal and no `Agent` anywhere near it.
17
+ */
18
+ import type { SubagentRun } from './types.js';
19
+ export interface PanelRows {
20
+ visible: SubagentRun[];
21
+ hiddenTotal: number;
22
+ hiddenRunning: number;
23
+ }
24
+ /**
25
+ * Rank the roster so that what is VISIBLE is what is HAPPENING.
26
+ *
27
+ * Ordering inside each group is chosen for ROW STABILITY, which is the real
28
+ * usability constraint here: the panel re-renders every 200 ms and a row that
29
+ * changes position on each tick is unreadable.
30
+ * - running: by `startedAt` ascending. `startedAt` changes exactly once in a
31
+ * run's life outside of its first assignment - on a cold-start retry (F-4),
32
+ * which re-stamps it (P2-4). A retried child therefore moves to the end of
33
+ * the running group, once, at the moment it visibly restarts. That is the
34
+ * correct reading of the ordering rule rather than an exception to it: the
35
+ * group is ordered by "how long has this been going", and a retry resets
36
+ * exactly that.
37
+ * - queued: original order; that IS the order the pool will start them in.
38
+ * - settled: most recently ended first, so the newest result is the one that
39
+ * stays on screen as others complete.
40
+ * A run changes position at most twice in its life - queued -> running ->
41
+ * settled - and each move carries real information.
42
+ *
43
+ * The final tiebreak is the original index. `Array.prototype.sort` is stable in
44
+ * every Node this package supports, so the tiebreak is not load-bearing; it is
45
+ * here so the ordering is total and the test can assert it without depending on
46
+ * that guarantee.
47
+ */
48
+ export declare function selectPanelRows(runs: SubagentRun[], max: number): PanelRows;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `selectPanelRows` - which children the live roster shows (team-live-activity
3
+ * F-2).
4
+ *
5
+ * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
+ *
7
+ * THE DEFECT THIS REPLACES. `TeamPanel` took the first `panelMaxRows` runs by
8
+ * ARRAY INDEX, and the slot pool hands out indices from a shared cursor in
9
+ * order - so at `maxSubagents: 8, maxConcurrent: 3` the first five rows are the
10
+ * five that finished first and everything still working sits behind `+3 more`.
11
+ * The one thing the requirement asks the panel for - show that subagents are
12
+ * running - is the one thing it stopped doing at the fan-out widths the
13
+ * requirement explicitly permits.
14
+ *
15
+ * Pure: a function of a roster snapshot and a row cap, with no clock, no
16
+ * terminal and no `Agent` anywhere near it.
17
+ */
18
+ const RUNNING = new Set(['starting', 'thinking', 'tool', 'waiting']);
19
+ const SETTLED = new Set(['done', 'failed', 'aborted']);
20
+ /** 0 running, 1 queued, 2 settled. */
21
+ function group(run) {
22
+ if (RUNNING.has(run.phase))
23
+ return 0;
24
+ if (SETTLED.has(run.phase))
25
+ return 2;
26
+ return 1;
27
+ }
28
+ /**
29
+ * Rank the roster so that what is VISIBLE is what is HAPPENING.
30
+ *
31
+ * Ordering inside each group is chosen for ROW STABILITY, which is the real
32
+ * usability constraint here: the panel re-renders every 200 ms and a row that
33
+ * changes position on each tick is unreadable.
34
+ * - running: by `startedAt` ascending. `startedAt` changes exactly once in a
35
+ * run's life outside of its first assignment - on a cold-start retry (F-4),
36
+ * which re-stamps it (P2-4). A retried child therefore moves to the end of
37
+ * the running group, once, at the moment it visibly restarts. That is the
38
+ * correct reading of the ordering rule rather than an exception to it: the
39
+ * group is ordered by "how long has this been going", and a retry resets
40
+ * exactly that.
41
+ * - queued: original order; that IS the order the pool will start them in.
42
+ * - settled: most recently ended first, so the newest result is the one that
43
+ * stays on screen as others complete.
44
+ * A run changes position at most twice in its life - queued -> running ->
45
+ * settled - and each move carries real information.
46
+ *
47
+ * The final tiebreak is the original index. `Array.prototype.sort` is stable in
48
+ * every Node this package supports, so the tiebreak is not load-bearing; it is
49
+ * here so the ordering is total and the test can assert it without depending on
50
+ * that guarantee.
51
+ */
52
+ export function selectPanelRows(runs, max) {
53
+ const ranked = runs
54
+ .map((run, index) => ({ run, index }))
55
+ .sort((a, b) => {
56
+ const ga = group(a.run);
57
+ const gb = group(b.run);
58
+ if (ga !== gb)
59
+ return ga - gb;
60
+ if (ga === 0)
61
+ return (a.run.startedAt ?? 0) - (b.run.startedAt ?? 0) || a.index - b.index;
62
+ if (ga === 2)
63
+ return (b.run.endedAt ?? 0) - (a.run.endedAt ?? 0) || a.index - b.index;
64
+ return a.index - b.index;
65
+ })
66
+ .map((e) => e.run);
67
+ const limit = Math.max(0, max);
68
+ const visible = ranked.slice(0, limit);
69
+ const hidden = ranked.slice(limit);
70
+ return {
71
+ visible,
72
+ hiddenTotal: hidden.length,
73
+ hiddenRunning: hidden.filter((r) => RUNNING.has(r.phase)).length,
74
+ };
75
+ }