@aragon-agent/cli 0.6.40 → 0.6.42

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 (38) hide show
  1. package/CHANGELOG.md +2632 -2589
  2. package/README.md +3720 -3706
  3. package/dist/agent/controller.d.ts +36 -5
  4. package/dist/agent/controller.js +103 -21
  5. package/dist/agent/system-prompt.d.ts +19 -0
  6. package/dist/agent/system-prompt.js +5 -0
  7. package/dist/commands/builtins.js +16 -182
  8. package/dist/commands/subagent-command.d.ts +21 -16
  9. package/dist/commands/subagent-command.js +22 -225
  10. package/dist/commands/team-command.d.ts +49 -0
  11. package/dist/commands/team-command.js +259 -0
  12. package/dist/config/model-kits.d.ts +2 -1
  13. package/dist/config/store.js +3 -2
  14. package/dist/team/dispatch-gate.d.ts +86 -0
  15. package/dist/team/dispatch-gate.js +75 -0
  16. package/dist/team/forced-mode.d.ts +116 -0
  17. package/dist/team/forced-mode.js +156 -0
  18. package/dist/team/forced-prompt.d.ts +48 -0
  19. package/dist/team/forced-prompt.js +84 -0
  20. package/dist/team/limits.d.ts +40 -0
  21. package/dist/team/limits.js +48 -0
  22. package/dist/team/model.d.ts +5 -5
  23. package/dist/team/model.js +5 -5
  24. package/dist/ui/App.js +116 -10
  25. package/dist/ui/StatusBar.d.ts +14 -0
  26. package/dist/ui/StatusBar.js +1 -0
  27. package/dist/ui/layout/status-layout.d.ts +17 -0
  28. package/dist/ui/layout/status-layout.js +9 -0
  29. package/dist/ui/overlays/HelpOverlay.js +8 -4
  30. package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
  31. package/dist/ui/overlays/SettingsScreen.js +17 -7
  32. package/dist/ui/overlays/TeamFanoutPage.d.ts +17 -1
  33. package/dist/ui/overlays/TeamFanoutPage.js +23 -11
  34. package/dist/ui/overlays/kit-settings-state.js +3 -2
  35. package/dist/ui/overlays/model-profile-state.d.ts +1 -1
  36. package/dist/ui/overlays/team-fanout-state.d.ts +46 -13
  37. package/dist/ui/overlays/team-fanout-state.js +78 -20
  38. package/package.json +81 -81
@@ -25,6 +25,7 @@ import type { ToolPermission } from '../exec/permission.js';
25
25
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
26
26
  import type { FilePatch } from '../tools/patch.js';
27
27
  import type { SubagentModelResolution } from '../team/model.js';
28
+ import type { ForcedTeamState } from '../team/forced-mode.js';
28
29
  import type { TeamEvent, TeamSnapshot } from '../team/types.js';
29
30
  import { ProcSupervisor } from '../proc/supervisor.js';
30
31
  import type { ProcEventListener, ServiceSnapshot } from '../proc/types.js';
@@ -295,8 +296,8 @@ export declare class AgentController {
295
296
  /** Static: whether this session could ever render a question overlay. */
296
297
  private readonly planInteractive;
297
298
  private readonly teamRegistered;
298
- private teamEnabled;
299
299
  private readonly teamRuntime;
300
+ private forcedTeam;
300
301
  private readonly todoRegistered;
301
302
  private todoEnabled;
302
303
  private readonly todos;
@@ -704,17 +705,47 @@ export declare class AgentController {
704
705
  withHumanWait<T>(fn: () => Promise<T>): Promise<T>;
705
706
  /** Whether `task` is in `this.tools`. Fixed at construction; see the fields. */
706
707
  isTeamRegistered(): boolean;
707
- /** Whether `task` will actually dispatch. Flipped live by `/team on|off`. */
708
+ /**
709
+ * Whether `task` will actually dispatch - a DERIVED value.
710
+ *
711
+ * One truth (`config.team.enabled`) plus one immutable snapshot
712
+ * (`teamRegistered`). The previous version read a private copy that `/team
713
+ * on|off` hand-synchronised, which worked only while that command was the sole
714
+ * writer; the settings screen writes the config directly, so the copy would
715
+ * have kept saying `on` (P1-4 / AC-25).
716
+ */
708
717
  isTeamEnabled(): boolean;
709
718
  /**
710
719
  * Flip the live switch and re-render the prompt.
711
720
  *
721
+ * THE NAME IS KEPT (three call sites plus the settings path) but the body is
722
+ * one write: `setTeamConfig()` already clamps, bumps the revision and rebuilds,
723
+ * so there is no second place where a half-applied state can exist.
724
+ *
712
725
  * Turning it ON in a session that started with `--no-team` deliberately does
713
726
  * NOT register the tool (D-17): the array is immutable, and advertising an
714
727
  * unregistered tool is the dead end P0-2 is about. `/team` reports that
715
728
  * honestly by checking `isTeamRegistered()` first.
716
729
  */
717
730
  setTeamEnabled(enabled: boolean): void;
731
+ /**
732
+ * Arm forced team mode for this session (team-command-forced-dispatch §3.5).
733
+ *
734
+ * Returns the count actually adopted. The clamp is `[1, team.maxSubagents]`:
735
+ * `0`, a negative and a fraction are refused by the parser upstream (`-n 0` is
736
+ * a syntax error, not a clamp), so this is the boundary check of a value that
737
+ * crossed a module line - which is the one place `CLAUDE.md` asks for it, not a
738
+ * second validation of the same input.
739
+ *
740
+ * THE COUNT IS FROZEN for the rest of the session: `/team -n 2 <brief>` then a
741
+ * plain message keeps two children per turn, so the user's one expressed intent
742
+ * does not silently widen back to four.
743
+ */
744
+ armForcedTeam(count: number): number;
745
+ /** Disarm. IDEMPOTENT: disarming an unarmed session rewrites nothing. */
746
+ disarmForcedTeam(): void;
747
+ /** The armed state, read at render time by the chip and by `/team`'s status. */
748
+ getForcedTeam(): ForcedTeamState | null;
718
749
  getTeamConfig(): TeamConfig;
719
750
  /**
720
751
  * Adopt a new team config for this session and re-render the prompt (the block
@@ -728,8 +759,8 @@ export declare class AgentController {
728
759
  * One authority for the `task` report's disclosure line and its price
729
760
  * table: both read through here, so the header can never name one model
730
761
  * while pricing another. Re-resolved on every call for the reason
731
- * `resolveFastTier` records - `/fast off`, a settings save and `/team model`
732
- * all rewrite the inputs mid-session.
762
+ * `resolveFastTier` records - `/fast off`, a settings save and the `Subagent
763
+ * model` row all rewrite the inputs mid-session.
733
764
  */
734
765
  subagentModelResolution(): SubagentModelResolution;
735
766
  /** Refused mid-dispatch by `/team`; half a dispatch under two ceilings is not
@@ -771,7 +802,7 @@ export declare class AgentController {
771
802
  * THIS EXISTS BECAUSE `persistConfig` DOES NOT TOUCH THE RUNTIME (P1-2 /
772
803
  * AC-38). `App` reads `controller.getConfig()` on every render, and that
773
804
  * returns THIS object; the App's `persistConfig` only writes the file. So
774
- * `/todo panel off` must call both, exactly as `/team max` does — with only
805
+ * `/todo panel off` must call both, exactly as settings' `Team` page does — with
775
806
  * the persist call the command reports success and changes nothing until the
776
807
  * next launch.
777
808
  *
@@ -21,6 +21,9 @@ import { createFastOverseerProvider } from '../team/overseer.js';
21
21
  import { createTaskTool } from '../team/task-tool.js';
22
22
  import { resolveSubagentModel } from '../team/model.js';
23
23
  import { buildTeamBlock } from '../team/prompt.js';
24
+ // THE THIRD LAYER's prompt half (team-command-forced-dispatch §3.6): spliced
25
+ // after `<team_mode>` and before the subagent block, only while armed.
26
+ import { buildForcedTeamBlock } from '../team/forced-prompt.js';
24
27
  import { buildFastBlock } from '../fast/prompt.js';
25
28
  import { ProcSupervisor } from '../proc/supervisor.js';
26
29
  import { buildBackgroundJobsBlock, buildBackgroundServicesBlock } from '../proc/prompt.js';
@@ -172,18 +175,32 @@ export class AgentController {
172
175
  // immutable for the life of the session (see the note in the constructor),
173
176
  // so `/team on` in a session started with `--no-team` CANNOT add it.
174
177
  //
175
- // `teamEnabled` is live. `/team off` flips it, which makes `task` refuse
176
- // through its own guard and drops `<team_mode>` from the next prompt rebuild
177
- // — `rebuildSystemPrompt()` IS a live path, unlike the tool array.
178
+ // The LIVE switch is NOT a second field (team-command-forced-dispatch §3.5 /
179
+ // P1-4). It used to be one — `teamEnabled`, flipped by `/team on|off` and
180
+ // hand-synchronised with `config.team.enabled` — which was a second truth
181
+ // waiting to separate. The moment the settings screen grew a `Team mode` row,
182
+ // `applyModelSettingsSnapshot()` replaced `this.config` WITHOUT touching that
183
+ // copy, and the screen would have said `on` while `task` still answered
184
+ // TEAM_OFF_REFUSAL. `isTeamEnabled()` is now one derivation over
185
+ // `config.team.enabled`, and `setTeamEnabled()` is one write to it.
178
186
  //
179
- // Conflating them is P0-2: the prompt would advertise a tool that was never
180
- // registered, every call would come back "unknown tool", and the model would
181
- // have no way to discover why (D-17 / R-18).
187
+ // Conflating the two flags was P0-2 in the original feature: the prompt would
188
+ // advertise a tool that was never registered, every call would come back
189
+ // "unknown tool", and the model would have no way to discover why (D-17 / R-18).
182
190
  // -----------------------------------------------------------------------
183
191
  teamRegistered;
184
- teamEnabled;
185
192
  teamRuntime;
186
193
  // -----------------------------------------------------------------------
194
+ // Forced team mode (team-command-forced-dispatch §3.5 / D-4).
195
+ //
196
+ // SESSION STATE, NEVER PERSISTED: `null` means "not armed". `/team <brief>`
197
+ // and `/team on` set it, `/team off` clears it, and the process exiting is the
198
+ // third way it ends. It is not a `config.json` key on purpose — a persisted
199
+ // "always force a fan-out" would be inherited by `aragon exec` and by every
200
+ // launch after one experiment, at four children a turn (D-4 / AC-24).
201
+ // -----------------------------------------------------------------------
202
+ forcedTeam = null;
203
+ // -----------------------------------------------------------------------
187
204
  // Todo planning (todo-plan-execution §3.6a) — THE SAME TWO FLAGS, FOR THE
188
205
  // SAME REASON.
189
206
  //
@@ -426,8 +443,10 @@ export class AgentController {
426
443
  const fastTier = resolveFastTier(config, (id, role) => this.hasApiKey(id, role));
427
444
  const fastRegistered = fastTier.ok;
428
445
  // --- Team mode ---------------------------------------------------------
446
+ // ONE FLAG HERE (team-command-forced-dispatch P1-4): this is the IMMUTABLE
447
+ // snapshot of "is `task` in this.tools at all". The live switch is
448
+ // `config.team.enabled`, read through `isTeamEnabled()` — no second copy.
429
449
  this.teamRegistered = config.team.enabled;
430
- this.teamEnabled = config.team.enabled;
431
450
  this.teamRuntime = this.teamRegistered
432
451
  ? new TeamRuntime({
433
452
  getConfig: () => this.config,
@@ -711,7 +730,10 @@ export class AgentController {
711
730
  createTaskTool({
712
731
  runtime: this.teamRuntime,
713
732
  // Read LIVE: `/team off` flips the flag, it cannot unregister.
714
- isTeamEnabled: () => this.teamEnabled,
733
+ // THROUGH THE METHOD, not the config: one derivation, so the
734
+ // task tool, `<team_mode>` and the settings screen can never
735
+ // disagree about whether this session may dispatch (P1-4).
736
+ isTeamEnabled: () => this.isTeamEnabled(),
715
737
  maxSubagents: () => this.config.team.maxSubagents,
716
738
  hasApiKey: () => this.hasApiKey(),
717
739
  activeProvider: () => this.config.provider,
@@ -1056,16 +1078,31 @@ export class AgentController {
1056
1078
  skillTriageBlock: triageBlock,
1057
1079
  agentMode: this.effectiveMode,
1058
1080
  planInteractive: this.planInteractive,
1059
- // BOTH flags, and both are required (§4.5). `teamRegistered` false means
1060
- // there is no `task` tool to advertise; `teamEnabled` false means the user
1061
- // turned it off for this session. Advertising in either case would
1062
- // describe a capability the model does not have.
1063
- teamBlock: this.teamRegistered && this.teamEnabled
1081
+ // BOTH halves, and both are required (§4.5). `teamRegistered` false means
1082
+ // there is no `task` tool to advertise; `config.team.enabled` false means
1083
+ // the user turned the capability off (in settings — the only writer).
1084
+ // Advertising in either case would describe a capability the model does not
1085
+ // have. Read through the field rather than `this.isTeamEnabled()` so the
1086
+ // sentence and the code agree, as the two flags always have here (P1-4).
1087
+ teamBlock: this.teamRegistered && this.config.team.enabled
1064
1088
  ? buildTeamBlock({
1065
1089
  maxSubagents: config.team.maxSubagents,
1066
1090
  maxConcurrent: config.team.maxConcurrent,
1067
1091
  })
1068
1092
  : '',
1093
+ // THE THIRD LAYER (team-command-forced-dispatch §3.6): "you MUST dispatch"
1094
+ // on top of `<team_mode>`'s "you may". THREE conditions and all three are
1095
+ // load-bearing - armed for this session, the tool actually registered, and
1096
+ // the capability still on. The last two are `<team_mode>`'s own pair, so a
1097
+ // session can never be told to use a tool the prompt above it does not
1098
+ // mention. Empty everywhere else, which is what keeps I-2 (byte-identical
1099
+ // prompt) true for every session that never types `/team`.
1100
+ forcedTeamBlock: this.forcedTeam && this.teamRegistered && this.config.team.enabled
1101
+ ? buildForcedTeamBlock({
1102
+ count: this.forcedTeam.count,
1103
+ planMode: this.effectiveMode === 'plan',
1104
+ })
1105
+ : '',
1069
1106
  // BOTH FLAGS, exactly as team mode uses both, and for the identical
1070
1107
  // reason: `todoRegistered` false means there is no `todo_write` to
1071
1108
  // advertise, `todoEnabled` false means the user turned it off for this
@@ -1723,22 +1760,67 @@ export class AgentController {
1723
1760
  isTeamRegistered() {
1724
1761
  return this.teamRegistered;
1725
1762
  }
1726
- /** Whether `task` will actually dispatch. Flipped live by `/team on|off`. */
1763
+ /**
1764
+ * Whether `task` will actually dispatch - a DERIVED value.
1765
+ *
1766
+ * One truth (`config.team.enabled`) plus one immutable snapshot
1767
+ * (`teamRegistered`). The previous version read a private copy that `/team
1768
+ * on|off` hand-synchronised, which worked only while that command was the sole
1769
+ * writer; the settings screen writes the config directly, so the copy would
1770
+ * have kept saying `on` (P1-4 / AC-25).
1771
+ */
1727
1772
  isTeamEnabled() {
1728
- return this.teamEnabled;
1773
+ return this.teamRegistered && this.config.team.enabled;
1729
1774
  }
1730
1775
  /**
1731
1776
  * Flip the live switch and re-render the prompt.
1732
1777
  *
1778
+ * THE NAME IS KEPT (three call sites plus the settings path) but the body is
1779
+ * one write: `setTeamConfig()` already clamps, bumps the revision and rebuilds,
1780
+ * so there is no second place where a half-applied state can exist.
1781
+ *
1733
1782
  * Turning it ON in a session that started with `--no-team` deliberately does
1734
1783
  * NOT register the tool (D-17): the array is immutable, and advertising an
1735
1784
  * unregistered tool is the dead end P0-2 is about. `/team` reports that
1736
1785
  * honestly by checking `isTeamRegistered()` first.
1737
1786
  */
1738
1787
  setTeamEnabled(enabled) {
1739
- this.advanceSettingsRevision();
1740
- this.teamEnabled = enabled;
1788
+ this.setTeamConfig({ enabled });
1789
+ }
1790
+ /**
1791
+ * Arm forced team mode for this session (team-command-forced-dispatch §3.5).
1792
+ *
1793
+ * Returns the count actually adopted. The clamp is `[1, team.maxSubagents]`:
1794
+ * `0`, a negative and a fraction are refused by the parser upstream (`-n 0` is
1795
+ * a syntax error, not a clamp), so this is the boundary check of a value that
1796
+ * crossed a module line - which is the one place `CLAUDE.md` asks for it, not a
1797
+ * second validation of the same input.
1798
+ *
1799
+ * THE COUNT IS FROZEN for the rest of the session: `/team -n 2 <brief>` then a
1800
+ * plain message keeps two children per turn, so the user's one expressed intent
1801
+ * does not silently widen back to four.
1802
+ */
1803
+ armForcedTeam(count) {
1804
+ const ceiling = Math.max(1, this.config.team.maxSubagents);
1805
+ const adopted = Math.max(1, Math.min(Math.floor(count), ceiling));
1806
+ this.forcedTeam = { count: adopted };
1807
+ // RE-RENDER: the `<forced_team_mode>` block has to be in the very next
1808
+ // request, and `rebuildSystemPrompt()` is the live path that does it (it is
1809
+ // also why the command body prints a notice: this produces no AgentEvent, so
1810
+ // without one React would not repaint the `Team N` chip - P1-6).
1741
1811
  this.rebuildSystemPrompt();
1812
+ return adopted;
1813
+ }
1814
+ /** Disarm. IDEMPOTENT: disarming an unarmed session rewrites nothing. */
1815
+ disarmForcedTeam() {
1816
+ if (!this.forcedTeam)
1817
+ return;
1818
+ this.forcedTeam = null;
1819
+ this.rebuildSystemPrompt();
1820
+ }
1821
+ /** The armed state, read at render time by the chip and by `/team`'s status. */
1822
+ getForcedTeam() {
1823
+ return this.forcedTeam;
1742
1824
  }
1743
1825
  getTeamConfig() {
1744
1826
  return this.config.team;
@@ -1761,8 +1843,8 @@ export class AgentController {
1761
1843
  * One authority for the `task` report's disclosure line and its price
1762
1844
  * table: both read through here, so the header can never name one model
1763
1845
  * while pricing another. Re-resolved on every call for the reason
1764
- * `resolveFastTier` records - `/fast off`, a settings save and `/team model`
1765
- * all rewrite the inputs mid-session.
1846
+ * `resolveFastTier` records - `/fast off`, a settings save and the `Subagent
1847
+ * model` row all rewrite the inputs mid-session.
1766
1848
  */
1767
1849
  subagentModelResolution() {
1768
1850
  return resolveSubagentModel(this.config, (id, role) => this.hasApiKey(id, role));
@@ -1847,7 +1929,7 @@ export class AgentController {
1847
1929
  * THIS EXISTS BECAUSE `persistConfig` DOES NOT TOUCH THE RUNTIME (P1-2 /
1848
1930
  * AC-38). `App` reads `controller.getConfig()` on every render, and that
1849
1931
  * returns THIS object; the App's `persistConfig` only writes the file. So
1850
- * `/todo panel off` must call both, exactly as `/team max` does — with only
1932
+ * `/todo panel off` must call both, exactly as settings' `Team` page does — with
1851
1933
  * the persist call the command reports success and changes nothing until the
1852
1934
  * next launch.
1853
1935
  *
@@ -56,6 +56,25 @@ export interface SystemPromptParams {
56
56
  * produce the pre-team prompt BYTE FOR BYTE.
57
57
  */
58
58
  teamBlock?: string;
59
+ /**
60
+ * The rendered `<forced_team_mode>` block while the session is ARMED, or `''`
61
+ * (team-command-forced-dispatch §3.6 / I-2).
62
+ *
63
+ * A SEPARATE PARAMETER FROM `teamBlock`, not a sentence appended to it: that
64
+ * block says the lead MAY dispatch, this one says it MUST, and merging them
65
+ * would make the obligation inseparable from the permission - so an ordinary
66
+ * team session would start inheriting the forced tone, and switching `/team
67
+ * off` could only be done by rebuilding the team block itself.
68
+ *
69
+ * SPLICED AFTER `teamBlock` (it is the tightening of it) and BEFORE
70
+ * `subagentBlock` (a child must never read it: children have no `task` tool, so
71
+ * the block would order a call that cannot succeed - the same reason
72
+ * `system-prompt.ts:68` gives for keeping the two role blocks apart).
73
+ *
74
+ * Absent or `''` keeps the prompt BYTE-IDENTICAL to the pre-feature output,
75
+ * which is what I-2 asserts for every session that never types `/team`.
76
+ */
77
+ forcedTeamBlock?: string;
59
78
  /**
60
79
  * The rendered `<subagent_role>` block for a CHILD, or `''`.
61
80
  *
@@ -39,6 +39,7 @@ export function buildSystemPrompt(params) {
39
39
  // keeps this block's absence byte-identical to the pre-feature prompt (I-T1).
40
40
  const skillTriageBlock = params.skillTriageBlock ?? '';
41
41
  const teamBlock = params.teamBlock ?? '';
42
+ const forcedTeamBlock = params.forcedTeamBlock ?? '';
42
43
  const subagentBlock = params.subagentBlock ?? '';
43
44
  const todoBlock = params.todoBlock ?? '';
44
45
  const fastBlock = params.fastBlock ?? '';
@@ -66,6 +67,10 @@ export function buildSystemPrompt(params) {
66
67
  ...(skillTriageBlock ? ['', skillTriageBlock] : []),
67
68
  ...(planBlock ? ['', planBlock] : []),
68
69
  ...(teamBlock ? ['', teamBlock] : []),
70
+ // IMMEDIATELY AFTER `<team_mode>`: "can" then "must". BEFORE the subagent
71
+ // block, because a child reading an order to dispatch would be ordered to use
72
+ // a tool it does not have (§3.6).
73
+ ...(forcedTeamBlock ? ['', forcedTeamBlock] : []),
69
74
  ...(subagentBlock ? ['', subagentBlock] : []),
70
75
  ...(todoBlock ? ['', todoBlock] : []),
71
76
  ...(fastBlock ? ['', fastBlock] : []),
@@ -21,13 +21,15 @@ import { applyRestoredSession } from '../session/startup-session.js';
21
21
  import { runSessionCommand } from './session-command.js';
22
22
  import { runBranchCommand } from './branch-command.js';
23
23
  import { getLogger } from '../logging/logger.js';
24
- import { resolveFastTier } from '../fast/resolve.js';
25
- import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
26
24
  import { registerQueueCommand } from './queue-command.js';
27
25
  import { registerPromptCommands } from '../prompts/command.js';
28
26
  import { registerKitCommand } from './kit-command.js';
29
27
  import { registerAgentsCommand } from './agents-command.js';
30
28
  import { registerSubagentCommand } from './subagent-command.js';
29
+ // `/team` (team-command-forced-dispatch §3.10). The BODY is `commands/team-command.ts`
30
+ // and this pair of lines is the whole registration; the `team` entry that used to
31
+ // live in `COMMANDS` above was a CONFIG command and now lives in `Settings > Team`.
32
+ import { registerTeamCommand } from './team-command.js';
31
33
  // THE DRAWER AND THE HEADER LABEL (tui-shortcut-drawer-and-task-summary §4.7):
32
34
  // two single-purpose commands in their own modules, registered from here. Both are
33
35
  // UNCONDITIONAL, even with `taskSummary.enabled=false`, because they are the only
@@ -108,66 +110,6 @@ function formatUpdateStatus(port, override) {
108
110
  lines.push(` next ${next ? new Date(next).toLocaleTimeString() : 'not scheduled'}`);
109
111
  return lines.join('\n');
110
112
  }
111
- /**
112
- * `/team model [main|fast|<profile-name>]` (subagent-model §3.6 / 5.2).
113
- *
114
- * The user-owned switch's command-line surface. `main` and `fast` are verbs
115
- * compared case-insensitively; a NAME is matched EXACTLY, case and all,
116
- * against the library - the same identity the picker shows - and a miss lists
117
- * the names that would work rather than failing silently. The summary reuses
118
- * the exact wording the settings root row renders, so the two surfaces can
119
- * never describe the same setting differently.
120
- */
121
- function runTeamModelCommand(ctx, subject) {
122
- const controller = ctx.controller;
123
- const config = controller.getConfig();
124
- const tier = resolveFastTier(config, (id, role) => controller.hasApiKey(id, role));
125
- const summary = subagentModelSummary({
126
- mode: config.team.model,
127
- modelProfileId: config.team.modelProfileId,
128
- mainConnection: `${config.provider}:${config.model}`,
129
- fastConnection: tier.ok ? `${tier.ref.providerId}:${tier.ref.modelId}` : null,
130
- profiles: config.modelProfiles,
131
- hasKey: (id) => controller.hasApiKey(id),
132
- });
133
- if (!subject) {
134
- // The status readout: mode + the connection actually in effect + the
135
- // disclosed reason when the request had to fall back (D-4).
136
- const resolution = controller.subagentModelResolution();
137
- const note = resolution.fallbackReason
138
- ? ` Unavailable (${resolution.fallbackReason}); children run on the main model.`
139
- : resolution.effective === 'main'
140
- ? ''
141
- : ` Children run on ${resolution.ref.providerId}:${resolution.ref.modelId}.`;
142
- ctx.notify('info', `Subagent model: ${summary}.${note}`);
143
- return;
144
- }
145
- if (subject.toLowerCase() === 'main' || subject.toLowerCase() === 'fast') {
146
- const mode = subject.toLowerCase();
147
- const applied = controller.setTeamConfig({ model: mode });
148
- ctx.persistConfig({ team: { model: applied.model } });
149
- if (mode === 'fast' && !tier.ok) {
150
- ctx.notify('warn', 'The fast tier is not available now; subagents will fall back to the main model.');
151
- }
152
- ctx.toast('success', `Subagent model: ${mode === 'main' ? 'same as main' : 'fast tier'}.`);
153
- return;
154
- }
155
- const entries = config.modelProfiles?.entries ?? [];
156
- const matches = entries.filter((entry) => entry.name === subject);
157
- if (matches.length === 0) {
158
- const names = entries.slice(0, 8).map((entry) => entry.name);
159
- ctx.notify('warn', `No profile named "${subject}".`
160
- + (names.length ? ` Available: ${names.join(', ')}` : ' Create one in /settings first.'));
161
- return;
162
- }
163
- const entry = matches[0];
164
- const disambiguation = matches.length > 1 ? ` (#${entry.id.slice(0, 8)})` : '';
165
- const applied = controller.setTeamConfig({ model: 'profile', modelProfileId: entry.id });
166
- const teamPatch = { model: applied.model,
167
- modelProfileId: applied.modelProfileId };
168
- ctx.persistConfig({ team: teamPatch });
169
- ctx.toast('success', `Subagent model: ${entry.name}${disambiguation} (${entry.provider}:${entry.model}).`);
170
- }
171
113
  /** Rows `/todo status` prints before it says how many it did not. */
172
114
  const ACCEPT_STATUS_ROWS = 8;
173
115
  /**
@@ -428,126 +370,6 @@ const COMMANDS = [
428
370
  ctx.applyAgentMode(target);
429
371
  },
430
372
  },
431
- {
432
- name: 'team',
433
- description: 'Team subagents: status | on | off | max <n> | overseer on|off | model [main|fast|<profile-name>]',
434
- /**
435
- * `/team on` CANNOT turn team mode on in a session that started without it,
436
- * and this command must not pretend otherwise (§4.5 / D-17 / P0-2).
437
- *
438
- * `controller.ts` builds the tool array ONCE and its own comment forbids
439
- * rebuilding it: `Agent.setTools()` mutates the live `ToolRegistry`, and
440
- * `submit_plan` flips the session mode from INSIDE a tool execution, so a
441
- * rebuild at that moment would mutate the registry mid-iteration. But
442
- * `rebuildSystemPrompt()` IS a live path — so the naive reading of `/team on`
443
- * (flip a flag, re-splice `<team_mode>`) produces a session where the model
444
- * is told it has a `task` tool that was never registered. Every call comes
445
- * back "unknown tool", and the model has no way to discover why.
446
- *
447
- * So the answer splits by what was decided at construction, and the honest
448
- * branch — persist it, say the session is unchanged — is the whole point.
449
- */
450
- run: (ctx) => {
451
- const arg = ctx.args.trim().toLowerCase();
452
- const [verb, value] = arg.split(/\s+/, 2);
453
- const controller = ctx.controller;
454
- // RAW TOKENS, KEPT BESIDE THE LOWERCASED PAIR (V-2): `model` matches a
455
- // profile NAME case-sensitively - like the picker and the library -
456
- // and the whole-args toLowerCase() above would have made
457
- // `/team model MyProfile` silently unmatchable. Only the VERB token is
458
- // compared through the lowercased pair.
459
- const rawTokens = ctx.args.trim().split(/\s+/).filter((token) => token.length > 0);
460
- // Refused mid-dispatch for the reason `/reload` already records: half a
461
- // dispatch under one ceiling and half under another produces a result
462
- // nothing can afterwards explain.
463
- if (verb && verb !== 'status' && controller.isTeamBusy()) {
464
- ctx.notify('warn', 'Cannot change team settings mid-dispatch.');
465
- return;
466
- }
467
- if (!verb || verb === 'status') {
468
- const cfg = controller.getTeamConfig();
469
- const config = controller.getConfig();
470
- const state = !controller.isTeamRegistered()
471
- ? 'off for this session (started with --no-team)'
472
- : controller.isTeamEnabled()
473
- ? 'on'
474
- : 'off';
475
- const snapshot = controller.getTeamSnapshot();
476
- const roster = snapshot
477
- ? `\nRunning: ${snapshot.runs.map((r) => `${r.label} (${r.phase})`).join(', ')}`
478
- : '';
479
- const tier = resolveFastTier(controller.getConfig(), (id, role) => controller.hasApiKey(id, role));
480
- const subagent = subagentModelSummary({
481
- mode: cfg.model,
482
- modelProfileId: cfg.modelProfileId,
483
- mainConnection: `${config.provider}:${config.model}`,
484
- fastConnection: tier.ok ? `${tier.ref.providerId}:${tier.ref.modelId}` : null,
485
- profiles: config.modelProfiles,
486
- hasKey: (id) => controller.hasApiKey(id),
487
- });
488
- ctx.notify('info', `Team mode: ${state}. Max subagents: ${cfg.maxSubagents}, ` +
489
- `concurrent: ${cfg.maxConcurrent}, supervisor: ${cfg.overseer ? 'on' : 'off'}` +
490
- `${cfg.overseer ? ' (never kills; checks the fast tier each tick)' : ' (legacy hard timeouts)'}.` +
491
- `\nSubagent model: ${subagent}` +
492
- `${roster}`);
493
- return;
494
- }
495
- if (verb === 'on' || verb === 'off') {
496
- const enabled = verb === 'on';
497
- // A PARTIAL `team` section, safe only because `updatePersistedConfig`
498
- // deep-merges it — the same contract `/skills disable` relies on.
499
- ctx.persistConfig({ team: { enabled } });
500
- if (!controller.isTeamRegistered()) {
501
- // The bottom-right cell of §4.5's table. It is honest, it is one
502
- // sentence, and it keeps AC-11 intact: a `--no-team` session never
503
- // grows a wrapper or an entry, whatever the user types afterwards.
504
- ctx.notify('info', enabled
505
- ? 'Team mode is off for this session (started with --no-team). Saved for next launch.'
506
- : 'Team mode is already off for this session. Saved for next launch.');
507
- return;
508
- }
509
- controller.setTeamEnabled(enabled);
510
- ctx.toast('success', `Team mode ${enabled ? 'on' : 'off'}.`);
511
- return;
512
- }
513
- if (verb === 'max') {
514
- const n = Number.parseInt(value ?? '', 10);
515
- if (!Number.isFinite(n) || n <= 0) {
516
- ctx.notify('warn', 'Usage: /team max <n>');
517
- return;
518
- }
519
- // Clamped by `setTeamConfig` and again on the persist path, so a value
520
- // above the requirement's ceiling can reach neither the runtime nor the
521
- // file (R-g).
522
- const applied = controller.setTeamConfig({ maxSubagents: n });
523
- ctx.persistConfig({
524
- team: { maxSubagents: applied.maxSubagents },
525
- });
526
- ctx.toast('success', `Max subagents set to ${applied.maxSubagents}.`);
527
- return;
528
- }
529
- if (verb === 'model') {
530
- runTeamModelCommand(ctx, rawTokens[1]);
531
- return;
532
- }
533
- if (verb === 'overseer') {
534
- if (value !== 'on' && value !== 'off') {
535
- ctx.notify('warn', 'Usage: /team overseer on|off');
536
- return;
537
- }
538
- // Read LIVE per dispatch by the runtime, so no rebuild is needed - the
539
- // same reason this command does not touch the tool array (its own header
540
- // comment). Clamped by `setTeamConfig` and again on the persist path.
541
- const applied = controller.setTeamConfig({ overseer: value === 'on' });
542
- ctx.persistConfig({
543
- team: { overseer: applied.overseer },
544
- });
545
- ctx.toast('success', `Dispatch supervisor ${applied.overseer ? 'on' : 'off'}.`);
546
- return;
547
- }
548
- ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>|overseer on|off|model main|fast|<profile-name>].`);
549
- },
550
- },
551
373
  {
552
374
  name: 'fast',
553
375
  description: 'Fast model tier: status | on | off | model <id> | provider <id> | same | ' +
@@ -1515,7 +1337,15 @@ export function registerBuiltinCommands(registry) {
1515
1337
  // and for the same reason: a `--no-team` session must still be able to ask the
1516
1338
  // command why it cannot force anything (I-7 / AC-15) - so registering it may
1517
1339
  // not read config, build a connection or send a request.
1340
+ // It is now a DEPRECATED ALIAS of `/team` (team-command-forced-dispatch D-12):
1341
+ // same implementation, one extra toast, no second grammar.
1518
1342
  registerSubagentCommand(registry);
1343
+ // `/team` (team-command-forced-dispatch §3.10). Registered HERE rather than from
1344
+ // `COMMANDS` because the entry that used to sit in that array was the config
1345
+ // command (`status | on | off | max n | overseer | model`) - those four verbs
1346
+ // now live in the settings screen, and this command is the ACT: arm forced mode
1347
+ // and start work. Unconditional, for the reason `/subagent` above records.
1348
+ registerTeamCommand(registry);
1519
1349
  // The keyboard drawer (`Ctrl+G` / `/keys`) and the task label (`/task`). Two
1520
1350
  // registrations, no new mechanism: `/keys` sets the panel's pre-filter and opens
1521
1351
  // the same overlay `Ctrl+G` opens, and `/task` reports what the header cell says.
@@ -1531,6 +1361,10 @@ export function registerBuiltinCommands(registry) {
1531
1361
  * correct when commands are registered dynamically (§7.1 / P1-2).
1532
1362
  */
1533
1363
  export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', 'subagent',
1364
+ // `team` is listed here because it is no longer in `COMMANDS` (it moved to
1365
+ // `commands/team-command.ts`), and this array is the documentation/tests surface
1366
+ // for every built-in name - the `flatMap` below only sees `COMMANDS`.
1367
+ 'team',
1534
1368
  'keys', 'task',
1535
1369
  ...COMMANDS.flatMap((c) => [
1536
1370
  c.name,
@@ -1,25 +1,30 @@
1
1
  /**
2
- * `/subagent` — force the next turn to fan out into a parallel team
3
- * (subagent-fanout 3.5 / 5.2).
2
+ * `/subagent`: a DEPRECATED ALIAS of `/team` (team-command-forced-dispatch
3
+ * §3.10 / D-12 / I-8).
4
4
  *
5
5
  * ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
6
6
  *
7
- * WHAT THIS COMMAND IS AND IS NOT (D-8 / D-9 / D-10). It does not dispatch: it
8
- * parses the user's intent, refuses honestly when forcing cannot work, prints
9
- * ONE line saying what will happen next, and submits a normal user turn. The
10
- * lead then calls `task` on the one dispatch path that already exists, so Esc,
11
- * the watchdog pause, the report, spend accounting and the concurrency lock all
12
- * keep working without a second implementation. The consequence the user must
13
- * be told about is that forcing is INSTRUCTION-LEVEL: the notice says "the next
14
- * turn dispatches them", never "N subagents started".
7
+ * WHY IT STILL EXISTS. It shipped, it is in five places in the README, and it is
8
+ * in the muscle memory of everyone who used the forced fan-out before `/team`
9
+ * grew its meaning. Removing the name would be a breaking change that buys
10
+ * nothing the alias does not buy for one `toast`.
15
11
  *
16
- * THE GUARD ORDER IS A CONTRACT (R-P1-10 / R-P1-11): status -> capability ->
17
- * parse. A `--no-team` session typing `/subagent -n x` is told the session has
18
- * no team capability, because a syntax hint would send them to fix the one thing
19
- * that is not wrong.
12
+ * WHY IT HAS NO BODY. `subagent-fanout-and-forced-dispatch` implemented the
13
+ * whole grammar - control words, two ceilings, four parse errors, plan-mode
14
+ * disclosure - and every one of those bytes is now `/team`'s. A second copy here
15
+ * would drift on the first refinement, and the drift would be invisible: both
16
+ * commands would keep working, one of them would quietly be the old one. The
17
+ * file therefore holds ONE delegation and one line of deprecation text, and
18
+ * `subagent-command.test.ts` asserts both (I-8).
20
19
  */
21
20
  import type { CommandContext, CommandRegistry } from './registry.js';
22
- /** The command body; `registerSubagentCommand` is the only registration site. */
21
+ /**
22
+ * Run `/team`, having first said out loud that this is the same thing.
23
+ *
24
+ * THE TOAST COMES FIRST ON PURPOSE: it is one ephemeral line above whatever
25
+ * `/team` prints, so the sequence reads "you used the old name; here is the
26
+ * answer" rather than a full answer followed by a correction nobody notices.
27
+ */
23
28
  export declare function runSubagentCommand(ctx: CommandContext): void;
24
- /** Registered from `builtins.ts` with one line, next to every other subsystem. */
29
+ /** Registered from `builtins.ts`: the name, and nothing else, lives here. */
25
30
  export declare function registerSubagentCommand(registry: CommandRegistry): void;