@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.
- package/CHANGELOG.md +2632 -2589
- package/README.md +3720 -3706
- package/dist/agent/controller.d.ts +36 -5
- package/dist/agent/controller.js +112 -33
- package/dist/agent/headless.js +6 -2
- package/dist/agent/system-prompt.d.ts +19 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/commands/builtins.js +16 -182
- package/dist/commands/subagent-command.d.ts +21 -16
- package/dist/commands/subagent-command.js +22 -225
- package/dist/commands/team-command.d.ts +49 -0
- package/dist/commands/team-command.js +259 -0
- package/dist/config/model-kits.d.ts +2 -1
- package/dist/config/store.js +3 -2
- package/dist/exec/runner.js +2 -1
- package/dist/team/bus.d.ts +4 -0
- package/dist/team/bus.js +12 -2
- package/dist/team/comm-tools.js +3 -0
- package/dist/team/dispatch-gate.d.ts +86 -0
- package/dist/team/dispatch-gate.js +75 -0
- package/dist/team/forced-mode.d.ts +116 -0
- package/dist/team/forced-mode.js +156 -0
- package/dist/team/forced-prompt.d.ts +48 -0
- package/dist/team/forced-prompt.js +84 -0
- package/dist/team/limits.d.ts +41 -1
- package/dist/team/limits.js +49 -1
- package/dist/team/model.d.ts +22 -6
- package/dist/team/model.js +19 -5
- package/dist/team/overseer.d.ts +4 -0
- package/dist/team/overseer.js +38 -6
- package/dist/team/prompt.js +3 -3
- package/dist/team/report.d.ts +21 -0
- package/dist/team/report.js +40 -9
- package/dist/team/runtime.d.ts +3 -0
- package/dist/team/runtime.js +61 -5
- package/dist/team/subagent.js +25 -7
- package/dist/ui/App.js +140 -20
- package/dist/ui/StatusBar.d.ts +14 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/layout/status-layout.d.ts +17 -0
- package/dist/ui/layout/status-layout.js +9 -0
- package/dist/ui/overlays/HelpOverlay.js +8 -4
- package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
- package/dist/ui/overlays/SettingsScreen.js +17 -7
- package/dist/ui/overlays/TeamFanoutPage.d.ts +17 -1
- package/dist/ui/overlays/TeamFanoutPage.js +23 -11
- package/dist/ui/overlays/kit-settings-state.js +3 -2
- package/dist/ui/overlays/model-profile-state.d.ts +1 -1
- package/dist/ui/overlays/team-fanout-state.d.ts +46 -13
- package/dist/ui/overlays/team-fanout-state.js +78 -20
- package/package.json +81 -81
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `/subagent
|
|
3
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
3
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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 {
|
|
22
|
-
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
20
|
+
import { runTeamCommand } from './team-command.js';
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
22
|
+
* Run `/team`, having first said out loud that this is the same thing.
|
|
25
23
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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.
|
package/dist/config/store.js
CHANGED
|
@@ -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:
|
|
277
|
-
//
|
|
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):
|