@aragon-agent/cli 0.6.40 → 0.6.43

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 (51) 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 +112 -33
  5. package/dist/agent/headless.js +6 -2
  6. package/dist/agent/system-prompt.d.ts +19 -0
  7. package/dist/agent/system-prompt.js +5 -0
  8. package/dist/commands/builtins.js +16 -182
  9. package/dist/commands/subagent-command.d.ts +21 -16
  10. package/dist/commands/subagent-command.js +22 -225
  11. package/dist/commands/team-command.d.ts +49 -0
  12. package/dist/commands/team-command.js +259 -0
  13. package/dist/config/model-kits.d.ts +2 -1
  14. package/dist/config/store.js +3 -2
  15. package/dist/exec/runner.js +2 -1
  16. package/dist/team/bus.d.ts +4 -0
  17. package/dist/team/bus.js +12 -2
  18. package/dist/team/comm-tools.js +3 -0
  19. package/dist/team/dispatch-gate.d.ts +86 -0
  20. package/dist/team/dispatch-gate.js +75 -0
  21. package/dist/team/forced-mode.d.ts +116 -0
  22. package/dist/team/forced-mode.js +156 -0
  23. package/dist/team/forced-prompt.d.ts +48 -0
  24. package/dist/team/forced-prompt.js +84 -0
  25. package/dist/team/limits.d.ts +41 -1
  26. package/dist/team/limits.js +49 -1
  27. package/dist/team/model.d.ts +22 -6
  28. package/dist/team/model.js +19 -5
  29. package/dist/team/overseer.d.ts +4 -0
  30. package/dist/team/overseer.js +38 -6
  31. package/dist/team/prompt.js +3 -3
  32. package/dist/team/report.d.ts +21 -0
  33. package/dist/team/report.js +40 -9
  34. package/dist/team/runtime.d.ts +3 -0
  35. package/dist/team/runtime.js +61 -5
  36. package/dist/team/subagent.js +25 -7
  37. package/dist/ui/App.js +140 -20
  38. package/dist/ui/StatusBar.d.ts +14 -0
  39. package/dist/ui/StatusBar.js +1 -0
  40. package/dist/ui/layout/status-layout.d.ts +17 -0
  41. package/dist/ui/layout/status-layout.js +9 -0
  42. package/dist/ui/overlays/HelpOverlay.js +8 -4
  43. package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
  44. package/dist/ui/overlays/SettingsScreen.js +17 -7
  45. package/dist/ui/overlays/TeamFanoutPage.d.ts +17 -1
  46. package/dist/ui/overlays/TeamFanoutPage.js +23 -11
  47. package/dist/ui/overlays/kit-settings-state.js +3 -2
  48. package/dist/ui/overlays/model-profile-state.d.ts +1 -1
  49. package/dist/ui/overlays/team-fanout-state.d.ts +46 -13
  50. package/dist/ui/overlays/team-fanout-state.js +78 -20
  51. package/package.json +81 -81
@@ -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;
@@ -1,242 +1,39 @@
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
- import { buildForcedDirective, parseSubagentArgs } from '../team/forced-dispatch.js';
22
- import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
20
+ import { runTeamCommand } from './team-command.js';
23
21
  /**
24
- * The default count when the user names none (D-11).
22
+ * Run `/team`, having first said out loud that this is the same thing.
25
23
  *
26
- * FOUR, not `maxSubagents`: `<team_mode>` tells the model two to four is usually
27
- * enough, so "force one dispatch" must not silently become the most expensive
28
- * call the session can make. The status line advertises the real ceiling so the
29
- * user who wants the widest fan-out knows which flag to add.
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.
30
27
  */
31
- const DEFAULT_FORCED_COUNT = 4;
32
- /** `off (--no-team)` / `off` / `on`: the three states the status line tells apart. */
33
- function teamState(ctx) {
34
- if (!ctx.controller.isTeamRegistered())
35
- return 'off (--no-team)';
36
- return ctx.controller.isTeamEnabled() ? 'on' : 'off';
37
- }
38
- /** The subagent model line, phrased by the same authority `/team status` uses. */
39
- function modelLine(ctx) {
40
- const config = ctx.controller.getConfig();
41
- const team = ctx.controller.getTeamConfig();
42
- const fast = config.fast;
43
- return subagentModelSummary({
44
- mode: team.model,
45
- modelProfileId: team.modelProfileId,
46
- mainConnection: `${config.provider}:${config.model}`,
47
- fastConnection: fast.enabled && fast.model
48
- ? `${fast.provider || config.provider}:${fast.model}` : null,
49
- profiles: config.modelProfiles,
50
- hasKey: (id) => ctx.controller.hasApiKey(id),
51
- });
52
- }
53
- /**
54
- * The no-argument answer: five lines, honest in every session state.
55
- *
56
- * IT RUNS BEFORE EVERY GUARD (R-P1-11). "How wide can I go?" is a question, not
57
- * a request, and a question deserves an answer even in a `--no-team` session -
58
- * which is exactly where the user is most likely to ask it.
59
- */
60
- function reportStatus(ctx) {
61
- const team = ctx.controller.getTeamConfig();
62
- const max = team.maxSubagents;
63
- // The bracket's bounds are the LIVE width, never a literal: at capacity 2 this
64
- // line has to read `<2-2>`, or the status line teaches a flag that arrives
65
- // narrowed every time.
66
- const low = Math.min(2, max);
67
- const lines = [
68
- `Forced fan-out: /subagent [-n <${low}-${max}>] [--parts a,b,c] [--read-only] <brief>`,
69
- `Capacity: ${max} per dispatch, ${team.maxConcurrent} at once `
70
- + '(team.maxSubagents / team.maxConcurrent)',
71
- `Subagent model: ${modelLine(ctx)}`,
72
- `Supervisor: ${team.overseer ? 'on' : 'off'}. Team mode: ${teamState(ctx)}.`,
73
- ];
74
- // Disclosed, never refused (R-P2-6): plan mode may remove `task` from this
75
- // turn's tool array, and a user who is told so will not read the silence as a
76
- // broken command.
77
- if (ctx.controller.getAgentMode() === 'plan') {
78
- lines.push('Plan mode is on: the task tool may be unavailable this turn.');
79
- }
80
- ctx.notify('info', lines.join('\n'));
81
- }
82
- /**
83
- * Guard 5's sentence. The provider is named because "no API key" alone leaves
84
- * the user guessing which of their configured providers is the empty one.
85
- */
86
- function reportMissingKey(ctx, team) {
87
- const config = ctx.controller.getConfig();
88
- const resolution = ctx.controller.subagentModelResolution();
89
- const provider = resolution.effective === 'main'
90
- ? config.provider : resolution.ref.providerId;
91
- const note = team.model === 'fast' && !ctx.controller.hasApiKey(undefined, 'fast')
92
- ? ' Fast tier has no key either; children will fail and the report will say so.'
93
- : '';
94
- ctx.notify('warn', `No API key for ${provider}. Set one with /settings or an environment variable.${note}`);
95
- }
96
- /** The five capability refusals, in the fixed order 3.5.3 records. */
97
- function refused(ctx, team) {
98
- if (!ctx.controller.isTeamRegistered()) {
99
- ctx.notify('warn', 'Team mode is off for this session (started with --no-team). '
100
- + 'Restart without --no-team to use /subagent.');
101
- return true;
102
- }
103
- if (!ctx.controller.isTeamEnabled()) {
104
- ctx.notify('warn', 'Team mode is off. Run /team on, then /subagent again.');
105
- return true;
106
- }
107
- // The team lock and the lead's turn are separate refusals for one reason: both
108
- // would collide with `task`'s own BUSY_REFUSAL, and the user would see a failed
109
- // tool card instead of a sentence explaining why nothing ran.
110
- if (ctx.controller.isTeamBusy()) {
111
- ctx.notify('warn', 'A team dispatch is already running. '
112
- + 'Wait for its report, or press Esc to stop it.');
113
- return true;
114
- }
115
- if (ctx.controller.isRunning()) {
116
- ctx.notify('warn', 'A turn is in progress. Press Esc to stop it, then run /subagent again.');
117
- return true;
118
- }
119
- if (!ctx.controller.hasApiKey()) {
120
- reportMissingKey(ctx, team);
121
- return true;
122
- }
123
- return false;
124
- }
125
- /** The four parse failures, each with the one sentence that fixes it. */
126
- function reportParseError(ctx, parsed) {
127
- if (parsed.error === 'count') {
128
- ctx.notify('warn', '/subagent: -n needs a number of 1 or more, e.g. -n 4.');
129
- return;
130
- }
131
- if (parsed.error === 'conflict') {
132
- ctx.notify('warn', '/subagent: use -n or --parts, not both.');
133
- return;
134
- }
135
- if (parsed.error === 'flag') {
136
- ctx.notify('warn', `/subagent: unknown option "${parsed.token ?? ''}". Try /subagent for usage.`);
137
- return;
138
- }
139
- ctx.notify('warn', '/subagent <brief> - describe the work; add -n <count> to fix the fan-out.');
140
- }
141
- /**
142
- * Narrow the parsed intent against the LIVE width.
143
- *
144
- * TWO CEILINGS, IN THIS ORDER (R-P1-3): the structural `forcedPartsMax` bounds
145
- * what one argument may carry (the parser already applied it), and the policy
146
- * `maxSubagents` bounds what this session may dispatch - which only this layer
147
- * can apply, because only it can read the live config.
148
- */
149
- function planForced(args, team) {
150
- const max = team.maxSubagents;
151
- if (args.parts.length > 0) {
152
- // A PARTS LIST STARTS WITH A CHILD PER SURVIVING PART, and `requested` is the
153
- // number the user TYPED - not the structural 8 the parser already applied.
154
- // Quoting the post-truncation count made the old notice tell a user who asked
155
- // for eleven parts that they had asked for eight (R-4: the notice and the
156
- // directive must both name the user's number).
157
- const parts = args.parts.slice(0, max);
158
- return {
159
- brief: args.brief,
160
- parts,
161
- partsAdmitted: args.parts.length,
162
- partsRequested: args.partsRequested,
163
- requested: args.partsRequested,
164
- count: parts.length,
165
- readOnly: args.readOnly,
166
- };
167
- }
168
- const requested = args.count ?? Math.min(DEFAULT_FORCED_COUNT, max);
169
- return {
170
- brief: args.brief,
171
- parts: [],
172
- partsAdmitted: 0,
173
- partsRequested: 0,
174
- requested,
175
- count: Math.max(1, Math.min(requested, max)),
176
- readOnly: args.readOnly,
177
- };
178
- }
179
- /** The one line that says what the NEXT turn will do - a plan, not a result. */
180
- function planNotice(planned, team) {
181
- const noun = planned.count === 1 ? 'subagent' : 'subagents';
182
- const head = `Forcing a fan-out of ${planned.count} ${noun}`;
183
- const followUp = `${head} (max ${team.maxSubagents}) - the next turn dispatches them. `
184
- + 'Esc stops the turn.';
185
- // TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may
186
- // carry and `team.maxSubagents` bounds what may be dispatched; naming the wrong
187
- // one sends the user to change a number that is not the reason.
188
- if (planned.parts.length > 0) {
189
- if (planned.parts.length < planned.partsAdmitted) {
190
- return `${head} (asked for ${planned.partsRequested} parts; `
191
- + `team.maxSubagents is ${team.maxSubagents}).`;
192
- }
193
- if (planned.partsAdmitted < planned.partsRequested) {
194
- return `${head} (asked for ${planned.partsRequested} parts, `
195
- + `the first ${planned.parts.length} are used).`;
196
- }
197
- return followUp;
198
- }
199
- if (planned.requested > planned.count) {
200
- return `${head} (asked for ${planned.requested}; `
201
- + `team.maxSubagents is ${team.maxSubagents}).`;
202
- }
203
- return followUp;
204
- }
205
- /** The command body; `registerSubagentCommand` is the only registration site. */
206
28
  export function runSubagentCommand(ctx) {
207
- if (ctx.args.trim().length === 0) {
208
- reportStatus(ctx);
209
- return;
210
- }
211
- const team = ctx.controller.getTeamConfig();
212
- if (refused(ctx, team))
213
- return;
214
- const parsed = parseSubagentArgs(ctx.args);
215
- if (!parsed.ok) {
216
- reportParseError(ctx, parsed);
217
- return;
218
- }
219
- const planned = planForced(parsed.args, team);
220
- const directive = buildForcedDirective({
221
- brief: planned.brief,
222
- count: planned.count,
223
- readOnly: planned.readOnly,
224
- parts: planned.parts,
225
- maxSubagents: team.maxSubagents,
226
- clamped: planned.requested > planned.count,
227
- requested: planned.requested,
228
- planMode: ctx.controller.getAgentMode() === 'plan',
229
- });
230
- ctx.notify('info', planNotice(planned, team));
231
- // `userInitiated: false`: a directive the user did not type belongs in neither
232
- // the prompt history nor the hint-fade counter (todo-plan-followthrough C-12).
233
- ctx.submit(directive, { userInitiated: false });
29
+ ctx.toast('info', '/subagent is now /team - same arguments.');
30
+ runTeamCommand(ctx);
234
31
  }
235
- /** Registered from `builtins.ts` with one line, next to every other subsystem. */
32
+ /** Registered from `builtins.ts`: the name, and nothing else, lives here. */
236
33
  export function registerSubagentCommand(registry) {
237
34
  registry.register({
238
35
  name: 'subagent',
239
- description: 'Force the next turn to fan out into a parallel team',
36
+ description: 'Deprecated alias of /team - same arguments',
240
37
  run: (ctx) => { runSubagentCommand(ctx); },
241
38
  });
242
39
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `/team`: force the session into team mode and start work, or leave it
3
+ * (team-command-forced-dispatch §3.3 / §3.4 / §5.1).
4
+ *
5
+ * ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHAT THIS COMMAND IS. `/team <brief>` parses the user's intent, ARMS a
8
+ * session-level forced mode and submits ONE normal user turn. The lead analyses
9
+ * and plans that turn and then has to call `task` - three layers say so: the
10
+ * directive it is handed, the `<forced_team_mode>` block in every later system
11
+ * prompt, and the mechanical gate in `ui/App.tsx` that nudges once if a whole
12
+ * turn went by without a dispatch. This command never dispatches itself (I-1):
13
+ * Esc, the watchdog, the report, spend accounting and the concurrency lock all
14
+ * stay on the one path that already has them.
15
+ *
16
+ * WHAT IT IS NOT ANY MORE. It used to be the CONFIG command - `status | on | off
17
+ * | max <n> | overseer on|off | model <name>`. All five verbs now live in the
18
+ * settings screen (`Settings > Team`, plus the root `Subagent model` row), and
19
+ * this command writes not one byte of `config.json` (I-4). The reason is the one
20
+ * the requirement names: a command that both changes configuration and starts
21
+ * work leaves the user unable to predict what it did. `on` and `off` survive as
22
+ * SESSION CONTROL words - a mode needs enter, leave and report - and they never
23
+ * persist; the settings screen is the only persistent writer (I-4 / D-11).
24
+ *
25
+ * THE GUARD ORDER IS A CONTRACT (§3.4, R-P1-10 / R-P1-11) and it is not
26
+ * alphabetical:
27
+ *
28
+ * status (no guard) - "what is on?" is a question, answerable even in a
29
+ * `--no-team` session, which is where users ask it.
30
+ * off (no guard) - leaving is always possible, even mid-dispatch.
31
+ * registered? - a `--no-team` session cannot grow the tool (I-7).
32
+ * enabled? - NOT a refusal: it is turned on for this session
33
+ * (D-5), because "start now" must not detour through a
34
+ * settings screen. Nothing is persisted.
35
+ * not busy / idle - two separate sentences, because both would otherwise
36
+ * surface as `task`'s own refusal card.
37
+ * key present? - names the missing provider.
38
+ * on - arm only; the next message dispatches.
39
+ *
40
+ * EVERY `armForcedTeam()` / `disarmForcedTeam()` CALL SITE BELOW IS FOLLOWED BY A
41
+ * `ctx.notify` / `ctx.toast` IN THE SAME BRANCH (P1-6 / AC-28), because arming
42
+ * itself produces no `AgentEvent` and therefore no render: without the notice the
43
+ * `Team 4` chip would wait for an unrelated state change before appearing.
44
+ */
45
+ import type { CommandContext, CommandRegistry } from './registry.js';
46
+ /** The command body; `registerTeamCommand` is the only registration site. */
47
+ export declare function runTeamCommand(ctx: CommandContext): void;
48
+ /** Registered from `builtins.ts` with one line, next to every other subsystem. */
49
+ export declare function registerTeamCommand(registry: CommandRegistry): void;
@@ -0,0 +1,259 @@
1
+ /**
2
+ * `/team`: force the session into team mode and start work, or leave it
3
+ * (team-command-forced-dispatch §3.3 / §3.4 / §5.1).
4
+ *
5
+ * ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHAT THIS COMMAND IS. `/team <brief>` parses the user's intent, ARMS a
8
+ * session-level forced mode and submits ONE normal user turn. The lead analyses
9
+ * and plans that turn and then has to call `task` - three layers say so: the
10
+ * directive it is handed, the `<forced_team_mode>` block in every later system
11
+ * prompt, and the mechanical gate in `ui/App.tsx` that nudges once if a whole
12
+ * turn went by without a dispatch. This command never dispatches itself (I-1):
13
+ * Esc, the watchdog, the report, spend accounting and the concurrency lock all
14
+ * stay on the one path that already has them.
15
+ *
16
+ * WHAT IT IS NOT ANY MORE. It used to be the CONFIG command - `status | on | off
17
+ * | max <n> | overseer on|off | model <name>`. All five verbs now live in the
18
+ * settings screen (`Settings > Team`, plus the root `Subagent model` row), and
19
+ * this command writes not one byte of `config.json` (I-4). The reason is the one
20
+ * the requirement names: a command that both changes configuration and starts
21
+ * work leaves the user unable to predict what it did. `on` and `off` survive as
22
+ * SESSION CONTROL words - a mode needs enter, leave and report - and they never
23
+ * persist; the settings screen is the only persistent writer (I-4 / D-11).
24
+ *
25
+ * THE GUARD ORDER IS A CONTRACT (§3.4, R-P1-10 / R-P1-11) and it is not
26
+ * alphabetical:
27
+ *
28
+ * status (no guard) - "what is on?" is a question, answerable even in a
29
+ * `--no-team` session, which is where users ask it.
30
+ * off (no guard) - leaving is always possible, even mid-dispatch.
31
+ * registered? - a `--no-team` session cannot grow the tool (I-7).
32
+ * enabled? - NOT a refusal: it is turned on for this session
33
+ * (D-5), because "start now" must not detour through a
34
+ * settings screen. Nothing is persisted.
35
+ * not busy / idle - two separate sentences, because both would otherwise
36
+ * surface as `task`'s own refusal card.
37
+ * key present? - names the missing provider.
38
+ * on - arm only; the next message dispatches.
39
+ *
40
+ * EVERY `armForcedTeam()` / `disarmForcedTeam()` CALL SITE BELOW IS FOLLOWED BY A
41
+ * `ctx.notify` / `ctx.toast` IN THE SAME BRANCH (P1-6 / AC-28), because arming
42
+ * itself produces no `AgentEvent` and therefore no render: without the notice the
43
+ * `Team 4` chip would wait for an unrelated state change before appearing.
44
+ */
45
+ import { buildForcedDirective } from '../team/forced-dispatch.js';
46
+ import { DEFAULT_FORCED_COUNT, planForced, planNotice, readTeamIntent, } from '../team/forced-mode.js';
47
+ import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
48
+ /**
49
+ * The subagent model line, phrased by the same authority the settings root row
50
+ * uses (`subagentModelSummary`), fed the six inputs only this layer can gather.
51
+ *
52
+ * IT IS A FUNCTION AND NOT AN INLINE CALL because the six inputs are exactly
53
+ * what a second copy gets wrong, and the settings root row feeds the same
54
+ * authority (`subagentModelSummary`) the same six - which is what keeps the two
55
+ * surfaces from describing one setting differently.
56
+ */
57
+ function modelLine(ctx) {
58
+ const config = ctx.controller.getConfig();
59
+ const team = ctx.controller.getTeamConfig();
60
+ const fast = config.fast;
61
+ return subagentModelSummary({
62
+ mode: team.model,
63
+ modelProfileId: team.modelProfileId,
64
+ mainConnection: `${config.provider}:${config.model}`,
65
+ fastConnection: fast.enabled && fast.model
66
+ ? `${fast.provider || config.provider}:${fast.model}` : null,
67
+ profiles: config.modelProfiles,
68
+ hasKey: (id) => ctx.controller.hasApiKey(id),
69
+ });
70
+ }
71
+ /**
72
+ * The no-argument answer: five lines, honest in every session state (§5.1).
73
+ *
74
+ * IT RUNS BEFORE EVERY GUARD. "How do I use this?" is a question, not a request,
75
+ * and it is answerable in a `--no-team` session - which is exactly where the user
76
+ * most needs the answer.
77
+ */
78
+ function reportStatus(ctx) {
79
+ const team = ctx.controller.getTeamConfig();
80
+ const forced = ctx.controller.getForcedTeam();
81
+ const registered = ctx.controller.isTeamRegistered();
82
+ const lines = [
83
+ forced
84
+ ? `Forced team mode: on, ${forced.count} subagents per turn.`
85
+ : 'Forced team mode: off.',
86
+ registered
87
+ ? `Team: ${ctx.controller.isTeamEnabled() ? 'on' : 'off'}; `
88
+ + `${team.maxSubagents} per dispatch, ${team.maxConcurrent} at once; `
89
+ + `supervisor ${team.overseer ? 'on' : 'off'}.`
90
+ : 'Team: off for this session (started with --no-team); /team cannot dispatch.',
91
+ `Subagent model: ${modelLine(ctx)}`,
92
+ 'Usage: /team <brief> arms it and starts now; /team off leaves it.',
93
+ 'Settings > Team has the persistent switches (mode, width, supervisor).',
94
+ ];
95
+ // DISCLOSED, NEVER REFUSED (§5.1 third degraded form): plan mode narrows what a
96
+ // child may do, and a user who is told so will not read the restraint as a bug.
97
+ if (forced && ctx.controller.getAgentMode() === 'plan') {
98
+ lines.push('Plan mode is on: dispatch read-only research, then submit_plan.');
99
+ }
100
+ ctx.notify('info', lines.join('\n'));
101
+ }
102
+ /**
103
+ * Guard 5's sentence. The provider is named because "no API key" alone leaves the
104
+ * user guessing which of their configured providers is the empty one.
105
+ */
106
+ function reportMissingKey(ctx, team) {
107
+ const config = ctx.controller.getConfig();
108
+ const resolution = ctx.controller.subagentModelResolution();
109
+ const provider = resolution.effective === 'main'
110
+ ? config.provider : resolution.ref.providerId;
111
+ const note = team.model === 'fast' && !ctx.controller.hasApiKey(undefined, 'fast')
112
+ ? ' Fast tier has no key either; children will fail and the report will say so.'
113
+ : '';
114
+ ctx.notify('warn', `No API key for ${provider}. Set one with /settings or an environment variable.${note}`);
115
+ }
116
+ /**
117
+ * The capability guards, in §3.4's exact order: registered -> enabled -> busy ->
118
+ * running -> key.
119
+ *
120
+ * RETURNS `true` WHEN THE CALLER MAY CONTINUE. Step 5 is the one place a guard
121
+ * becomes an ACTION (D-5) and it sits at its documented position rather than at
122
+ * the end for a behavioural reason: the user's own sentence is "start now", so
123
+ * the switch is turned on before any sentence about *why we cannot* is composed.
124
+ * The action is still followed by a notice in the same branch (P1-6 / AC-28),
125
+ * because `setTeamEnabled()` rebuilds the prompt and produces no `AgentEvent`, so
126
+ * without it the `Team N` chip would wait for an unrelated render.
127
+ */
128
+ function passGuards(ctx, team) {
129
+ if (!ctx.controller.isTeamRegistered()) {
130
+ ctx.notify('warn', 'Team mode is off for this session (started with --no-team). '
131
+ + 'Restart without --no-team to use /team.');
132
+ return false;
133
+ }
134
+ // STEP 5, THE ONE GUARD THAT IS NOT A REFUSAL: "start now" must not detour
135
+ // through a settings screen. Nothing is persisted here (I-4) - `Settings >
136
+ // Team` stays the only persistent writer.
137
+ if (!ctx.controller.isTeamEnabled()) {
138
+ ctx.controller.setTeamEnabled(true);
139
+ ctx.notify('info', 'Team mode was off; turned on for this session (Settings > Team saves it).');
140
+ }
141
+ // The team lock and the lead's turn are separate refusals for one reason: both
142
+ // would collide with `task`'s own BUSY_REFUSAL, and the user would see a failed
143
+ // tool card instead of a sentence explaining why nothing ran.
144
+ if (ctx.controller.isTeamBusy()) {
145
+ ctx.notify('warn', 'A team dispatch is already running. '
146
+ + 'Wait for its report, or press Esc to stop it.');
147
+ return false;
148
+ }
149
+ if (ctx.controller.isRunning()) {
150
+ ctx.notify('warn', 'A turn is in progress. Press Esc to stop it, then run /team again.');
151
+ return false;
152
+ }
153
+ if (!ctx.controller.hasApiKey()) {
154
+ reportMissingKey(ctx, team);
155
+ return false;
156
+ }
157
+ return true;
158
+ }
159
+ /** The four parse failures, each with the one sentence that fixes it. */
160
+ function reportParseError(ctx, intent) {
161
+ if (intent.error === 'count') {
162
+ ctx.notify('warn', '/team: -n needs a number of 1 or more, e.g. -n 4.');
163
+ return;
164
+ }
165
+ if (intent.error === 'conflict') {
166
+ ctx.notify('warn', '/team: use -n or --parts, not both.');
167
+ return;
168
+ }
169
+ if (intent.error === 'flag') {
170
+ ctx.notify('warn', `/team: unknown option "${intent.token ?? ''}". Try /team for usage.`);
171
+ return;
172
+ }
173
+ ctx.notify('warn', '/team <brief> - describe the work; add -n <count> to fix the fan-out.');
174
+ }
175
+ /**
176
+ * `/team off`: disarm the session, and say plainly that the CAPABILITY was not
177
+ * touched.
178
+ *
179
+ * THE SECOND HALF OF THE SENTENCE IS THE POINT (D-2 / D-11): with the switch
180
+ * moved to settings, "off" can only ever mean "this conversation stops being
181
+ * forced". A user who reads it as "team mode is now disabled everywhere" would
182
+ * next find `/team` still working tomorrow, and no test would describe that as a
183
+ * defect because no single surface would be wrong.
184
+ */
185
+ function leaveForcedMode(ctx) {
186
+ if (!ctx.controller.getForcedTeam()) {
187
+ // IDEMPOTENT, and it says so rather than reporting a change it did not make.
188
+ ctx.notify('info', 'Forced team mode was not on.');
189
+ return;
190
+ }
191
+ ctx.controller.disarmForcedTeam();
192
+ ctx.notify('info', 'Forced team mode off. Team capability unchanged (Settings > Team).');
193
+ }
194
+ /** `/team on`: arm with the default count and stop there - no turn is submitted. */
195
+ function enterForcedMode(ctx) {
196
+ if (!passGuards(ctx, ctx.controller.getTeamConfig()))
197
+ return;
198
+ const adopted = ctx.controller.armForcedTeam(DEFAULT_FORCED_COUNT);
199
+ ctx.notify('info', `Forced team mode on: ${adopted} subagents per turn. `
200
+ + 'Your next message dispatches.');
201
+ }
202
+ /** `/team [-n n|--parts a,b] [--read-only] <brief>`: arm and submit in one act. */
203
+ function armAndSubmit(ctx, intent) {
204
+ const team = ctx.controller.getTeamConfig();
205
+ if (!passGuards(ctx, team))
206
+ return;
207
+ const planned = planForced(intent.args, team);
208
+ // ARMED BEFORE THE TURN IS SUBMITTED, and with the SAME narrowed number the
209
+ // directive names: the later turns reuse this count, so the first turn and the
210
+ // rest of the session cannot disagree about how wide the fan-out is (D-7).
211
+ ctx.controller.armForcedTeam(planned.count);
212
+ ctx.notify('info', planNotice(planned, team));
213
+ const directive = buildForcedDirective({
214
+ brief: planned.brief,
215
+ count: planned.count,
216
+ readOnly: planned.readOnly,
217
+ parts: planned.parts,
218
+ maxSubagents: team.maxSubagents,
219
+ clamped: planned.requested > planned.count,
220
+ requested: planned.requested,
221
+ planMode: ctx.controller.getAgentMode() === 'plan',
222
+ });
223
+ // `userInitiated: false`: a directive the user did not type belongs in neither
224
+ // the prompt history nor the hint-fade counter (todo-plan-followthrough C-12).
225
+ ctx.submit(directive, { userInitiated: false });
226
+ }
227
+ /** The command body; `registerTeamCommand` is the only registration site. */
228
+ export function runTeamCommand(ctx) {
229
+ const intent = readTeamIntent(ctx.args);
230
+ if (intent.kind === 'status') {
231
+ reportStatus(ctx);
232
+ return;
233
+ }
234
+ if (intent.kind === 'off') {
235
+ leaveForcedMode(ctx);
236
+ return;
237
+ }
238
+ if (intent.kind === 'on') {
239
+ enterForcedMode(ctx);
240
+ return;
241
+ }
242
+ if (intent.kind === 'error') {
243
+ // PARSED BEFORE THE CAPABILITY GUARDS, unlike `on` / `<brief>`. A syntax hint
244
+ // is the answer to a syntax mistake in ANY session: sending a `--no-team` user
245
+ // to fix their flags first would send them to repair the one thing that is
246
+ // not broken.
247
+ reportParseError(ctx, intent);
248
+ return;
249
+ }
250
+ armAndSubmit(ctx, intent);
251
+ }
252
+ /** Registered from `builtins.ts` with one line, next to every other subsystem. */
253
+ export function registerTeamCommand(registry) {
254
+ registry.register({
255
+ name: 'team',
256
+ description: 'Force team mode and start now: /team <brief>; /team [on|off|status]',
257
+ run: (ctx) => { runTeamCommand(ctx); },
258
+ });
259
+ }
@@ -70,7 +70,8 @@ export declare const KIT_LIMITS: {
70
70
  * exactly as it is for `team.modelProfileId` (`schema.ts` clampTeamConfig):
71
71
  * `createModelProfileState` blanks the whole settings page when validation
72
72
  * fails, so promoting "a hand-edited file left one dangling id" into "the page
73
- * and `/team model` are both dead" is a wildly disproportionate blast radius.
73
+ * and the `Subagent model` row are both dead" is a wildly disproportionate blast
74
+ * radius.
74
75
  *
75
76
  * `duplicate_name` is deliberately NOT a code: two kits may share a name
76
77
  * (§6.6), and `/kit use` disambiguates with an `#id` prefix.
@@ -273,8 +273,9 @@ export function mergePersistedConfig(current, patch) {
273
273
  // replace the whole section and quietly revert `maxFiles`, `previewChars`
274
274
  // and — worst of all — `redactSecrets`.
275
275
  log: clampLogConfig({ ...current.log, ...(patch.log ?? {}) }),
276
- // The fourth section, and it needs the merge for the same reason: `/team max
277
- // 4` sends `{ team: { maxSubagents: 4 } }`, which a shallow spread would
276
+ // The fourth section, and it needs the merge for the same reason: the settings
277
+ // `Max subagents` row sends `{ team: { maxSubagents: 4 } }`, which a shallow
278
+ // spread would
278
279
  // turn into "changing the fan-out width silently reverted every timeout".
279
280
  team: clampTeamConfig({ ...current.team, ...(patch.team ?? {}) }),
280
281
  // The fifth section, and its omission is the worst of the five (P0-1):