@aragon-agent/cli 0.6.39 → 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.
- package/CHANGELOG.md +109 -0
- package/README.md +126 -20
- package/dist/agent/controller.d.ts +139 -14
- package/dist/agent/controller.js +299 -40
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +29 -1
- package/dist/agent/reducer.js +34 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +33 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +21 -5
- package/dist/commands/builtins.js +28 -185
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +30 -0
- package/dist/commands/subagent-command.js +39 -0
- package/dist/commands/team-command.d.ts +49 -0
- package/dist/commands/team-command.js +259 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/load.js +50 -3
- package/dist/config/model-kits.d.ts +2 -1
- package/dist/config/schema.d.ts +82 -6
- package/dist/config/schema.js +107 -9
- package/dist/config/store.js +10 -4
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/dispatch-gate.d.ts +86 -0
- package/dist/team/dispatch-gate.js +75 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -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 +61 -1
- package/dist/team/limits.js +69 -1
- package/dist/team/model.d.ts +5 -5
- package/dist/team/model.js +5 -5
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/ui/App.js +130 -11
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +2 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/layout/status-layout.d.ts +27 -0
- package/dist/ui/layout/status-layout.js +27 -1
- package/dist/ui/overlays/HelpOverlay.js +8 -4
- package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
- package/dist/ui/overlays/SettingsScreen.js +39 -5
- package/dist/ui/overlays/TeamFanoutPage.d.ts +51 -0
- package/dist/ui/overlays/TeamFanoutPage.js +90 -0
- package/dist/ui/overlays/kit-settings-state.js +3 -2
- package/dist/ui/overlays/model-profile-state.d.ts +20 -2
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +106 -0
- package/dist/ui/overlays/team-fanout-state.js +155 -0
- package/package.json +81 -81
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/subagent` — parse the user's arguments and compile them into ONE user turn
|
|
3
|
+
* (subagent-fanout 3.5.2 / 3.5.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope, and this module
|
|
6
|
+
* also produces text a model reads in a `cmd.exe` terminal.
|
|
7
|
+
*
|
|
8
|
+
* WHY A PROMPT AND NOT A DIRECT `dispatch()` CALL (D-8 / 3.5.1, option C). The
|
|
9
|
+
* alternative — the command splits the brief itself and calls
|
|
10
|
+
* `TeamRuntime.dispatch()` — needs a SECOND dispatch entry point (breaking I-1),
|
|
11
|
+
* a run lifecycle the command does not own (Esc, the lead's idle watchdog, the
|
|
12
|
+
* report rendered into the transcript, spend accounting, the concurrency lock),
|
|
13
|
+
* and a planner that cannot see the conversation. Compiling the user's intent
|
|
14
|
+
* into a normal user turn keeps all of that on the one path that already has it.
|
|
15
|
+
* The cost is that forcing is INSTRUCTION-LEVEL rather than mechanical, which is
|
|
16
|
+
* why the command body prints a plan ("the next turn dispatches them") instead
|
|
17
|
+
* of claiming results (D-10).
|
|
18
|
+
*
|
|
19
|
+
* TWO PURE FUNCTIONS, NO IO: parse and compile. Both are deterministic, so the
|
|
20
|
+
* whole surface is unit-testable without a terminal, a config file or a model.
|
|
21
|
+
*/
|
|
22
|
+
import { TEAM_LIMITS } from './limits.js';
|
|
23
|
+
import { toSlug } from './normalize.js';
|
|
24
|
+
/**
|
|
25
|
+
* Bumped whenever the directive's wording changes; the string appears VERBATIM
|
|
26
|
+
* on the directive's first line so a behaviour report ties to one revision with
|
|
27
|
+
* a single grep (D-15). The `-YYYY-MM` suffix is part of the version, not a
|
|
28
|
+
* comment: `v1` alone is easy to confuse with the other counters in this package.
|
|
29
|
+
*/
|
|
30
|
+
export const FORCED_DISPATCH_VERSION = 'v1-2026-10';
|
|
31
|
+
/**
|
|
32
|
+
* A DECIMAL POSITIVE INTEGER, and nothing else.
|
|
33
|
+
*
|
|
34
|
+
* `0` is deliberately NOT accepted (R-P1-2). Writing `/subagent -n 0` means the
|
|
35
|
+
* user mistyped; silently clamping it to 1 would rewrite their intent, which is
|
|
36
|
+
* the one thing a clamp must never do. `-n 12` is different: 12 is unambiguous
|
|
37
|
+
* and merely exceeds the ceiling, so it is accepted and DISCLOSED as narrowed.
|
|
38
|
+
*/
|
|
39
|
+
const COUNT_PATTERN = /^[1-9][0-9]*$/;
|
|
40
|
+
/** The flag name of a token, or `null` for a plain word (part of the brief). */
|
|
41
|
+
function flagName(word) {
|
|
42
|
+
if (word.length < 2 || !word.startsWith('-'))
|
|
43
|
+
return null;
|
|
44
|
+
return word.replace(/^--?/, '').toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
/** The brief, clipped with a VISIBLE marker (`forcedBriefChars`). */
|
|
47
|
+
function clippedBrief(brief) {
|
|
48
|
+
const limit = TEAM_LIMITS.forcedBriefChars;
|
|
49
|
+
return brief.length > limit ? `${brief.slice(0, limit)}\n[brief truncated]` : brief;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse `/subagent` arguments. ONE parameter (R-P1-1): the structural bounds are
|
|
53
|
+
* constants this module imports, not a caller's opinion.
|
|
54
|
+
*
|
|
55
|
+
* NEVER THROWS and never guesses. Every malformed input maps to one of the four
|
|
56
|
+
* errors the command body has a sentence for, because "we did something
|
|
57
|
+
* reasonable with your typo" is indistinguishable from a bug at the terminal.
|
|
58
|
+
*/
|
|
59
|
+
export function parseSubagentArgs(raw) {
|
|
60
|
+
const words = raw.split(/\s+/).filter((word) => word.length > 0);
|
|
61
|
+
const brief = [];
|
|
62
|
+
const parts = [];
|
|
63
|
+
let count = null;
|
|
64
|
+
let readOnly = false;
|
|
65
|
+
// `--parts` was SEEN, even if every item it carried cleaned away. The conflict
|
|
66
|
+
// rule is about the two flags appearing together, not about the usable parts
|
|
67
|
+
// that survived: `-n 3 --parts ,,,` still names two sources for one number, and
|
|
68
|
+
// silently obeying the count would drop a flag the user typed.
|
|
69
|
+
let sawParts = false;
|
|
70
|
+
for (let index = 0; index < words.length;) {
|
|
71
|
+
const word = words[index];
|
|
72
|
+
const flag = flagName(word);
|
|
73
|
+
if (flag === null) {
|
|
74
|
+
brief.push(word);
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (flag === 'n' || flag === 'count') {
|
|
79
|
+
const value = words[index + 1];
|
|
80
|
+
if (value === undefined || !COUNT_PATTERN.test(value))
|
|
81
|
+
return { ok: false, error: 'count' };
|
|
82
|
+
count = Number(value);
|
|
83
|
+
index += 2;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (flag === 'parts') {
|
|
87
|
+
const value = words[index + 1];
|
|
88
|
+
// A flag with no value is unusable rather than guessable, so it is
|
|
89
|
+
// reported as a bad option instead of being read as "no parts, then".
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return { ok: false, error: 'flag', token: word };
|
|
92
|
+
sawParts = true;
|
|
93
|
+
// A part that cleans to nothing is DROPPED rather than kept as an empty
|
|
94
|
+
// label; if every part cleans away the flag is treated as absent, because
|
|
95
|
+
// an empty part list cannot narrow a dispatch the brief still describes.
|
|
96
|
+
for (const item of value.split(',')) {
|
|
97
|
+
const slug = toSlug(item);
|
|
98
|
+
if (slug.length > 0 && !parts.includes(slug))
|
|
99
|
+
parts.push(slug);
|
|
100
|
+
}
|
|
101
|
+
index += 2;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (flag === 'read-only') {
|
|
105
|
+
readOnly = true;
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// `-n=6` lands here: ONE spelling per option, so the `=` form is an unknown
|
|
110
|
+
// token rather than a second grammar with its own edge cases.
|
|
111
|
+
return { ok: false, error: 'flag', token: word };
|
|
112
|
+
}
|
|
113
|
+
// Count and parts answer the same question, so a request carrying both is
|
|
114
|
+
// refused instead of resolved in one direction the user did not choose.
|
|
115
|
+
if (count !== null && sawParts)
|
|
116
|
+
return { ok: false, error: 'conflict' };
|
|
117
|
+
const text = brief.join(' ').trim();
|
|
118
|
+
if (text.length === 0)
|
|
119
|
+
return { ok: false, error: 'brief' };
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
args: {
|
|
123
|
+
brief: text,
|
|
124
|
+
count,
|
|
125
|
+
parts: parts.slice(0, TEAM_LIMITS.forcedPartsMax),
|
|
126
|
+
partsRequested: parts.length,
|
|
127
|
+
readOnly,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The sentence naming how many children to start.
|
|
133
|
+
*
|
|
134
|
+
* THE PERMISSION CLAUSE IS NOT OPTIONAL IN EITHER BRANCH. `--parts` REPLACES the
|
|
135
|
+
* count sentence, so a `readOnly` qualifier written only into the count branch
|
|
136
|
+
* disappears exactly when both flags are used - and `/subagent --read-only
|
|
137
|
+
* --parts api,ui <brief>` is one of the three examples the README advertises. The
|
|
138
|
+
* children would then be writeable while the notice said a read-only fan-out was
|
|
139
|
+
* forced (AC-13).
|
|
140
|
+
*/
|
|
141
|
+
function callSentence(request) {
|
|
142
|
+
const { count, readOnly, parts } = request;
|
|
143
|
+
if (parts.length > 0) {
|
|
144
|
+
const labels = 'Use those exact labels';
|
|
145
|
+
return `Call the \`task\` tool ONCE with one subagent per part: ${parts.join(', ')}. `
|
|
146
|
+
+ (readOnly ? `${labels}; every spec readOnly: true.` : `${labels}.`);
|
|
147
|
+
}
|
|
148
|
+
const noun = count === 1 ? 'subagent' : 'subagents';
|
|
149
|
+
const permission = readOnly ? ', all readOnly: true' : '';
|
|
150
|
+
return `Call the \`task\` tool ONCE with exactly ${count} ${noun}${permission}.`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The ONE user turn a `/subagent` invocation submits. Same input, same bytes -
|
|
154
|
+
* which is what makes the snapshot tests meaningful.
|
|
155
|
+
*
|
|
156
|
+
* THE ORDER OF THE SENTENCES IS THE ORDER OF THE MODEL'S DECISIONS: what to do
|
|
157
|
+
* (call `task` ONCE), what it must not do instead (work alone), then the lucky
|
|
158
|
+
* case (the brief does not split), then the brief itself, then the disclosure
|
|
159
|
+
* lines, then the per-spec rules.
|
|
160
|
+
*/
|
|
161
|
+
export function buildForcedDirective(request) {
|
|
162
|
+
const { brief, count, parts, maxSubagents, clamped, requested, planMode } = request;
|
|
163
|
+
// ONE SENTENCE PER LINE, and deliberately not hard-wrapped: the contract in
|
|
164
|
+
// 6.4 is checked by grepping phrases (`Call the `task` tool ONCE`, `Use those
|
|
165
|
+
// exact labels`), and a wrapper that split one of them across a newline would
|
|
166
|
+
// make the document and the artifact disagree about the same bytes.
|
|
167
|
+
const lines = [
|
|
168
|
+
`[forced team dispatch ${FORCED_DISPATCH_VERSION}] The user is forcing a parallel team `
|
|
169
|
+
+ 'on this brief.',
|
|
170
|
+
callSentence(request),
|
|
171
|
+
'Then act on the combined report. Do not do the work yourself; the subagents exist to do it.',
|
|
172
|
+
];
|
|
173
|
+
// NO "at least 2" FALLBACK FOR A SINGLE CHILD (R-P1-4): a sentence asking for
|
|
174
|
+
// at least two would be the louder instruction in a directive that asked for
|
|
175
|
+
// exactly one, and the model would follow it.
|
|
176
|
+
if (count > 1 && parts.length === 0) {
|
|
177
|
+
lines.push(`If the brief genuinely cannot be split into ${count} independent parts, use as many `
|
|
178
|
+
+ 'as it splits into (at least 2) and say why in one line.');
|
|
179
|
+
}
|
|
180
|
+
lines.push('', 'Brief:', clippedBrief(brief), '');
|
|
181
|
+
// A PARTS LIST CAN BE NARROWED BY TWO DIFFERENT CEILINGS (R-P1-3), and the
|
|
182
|
+
// spec's sentence - "the configured maximum is N" - is only true when the
|
|
183
|
+
// POLICY ceiling is the one that bit. `parts.length >= maxSubagents` says it
|
|
184
|
+
// is; below that the structural `forcedPartsMax` did the cutting, and naming
|
|
185
|
+
// the configured maximum would point the user at a number they can change to
|
|
186
|
+
// fix a limit they cannot.
|
|
187
|
+
if (clamped) {
|
|
188
|
+
if (parts.length > 0) {
|
|
189
|
+
lines.push(parts.length >= maxSubagents
|
|
190
|
+
? `You asked for ${requested} parts; the configured maximum is ${maxSubagents}, `
|
|
191
|
+
+ 'so the remaining parts are folded into these.'
|
|
192
|
+
: `You asked for ${requested} parts; only ${parts.length} fit in one directive, `
|
|
193
|
+
+ 'so the remaining parts are folded into these.');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push(`You asked for ${requested}; the configured maximum is ${maxSubagents}, `
|
|
197
|
+
+ `so use ${count}.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (planMode) {
|
|
201
|
+
lines.push('Plan mode is on; if the task tool is unavailable this turn, say so instead of '
|
|
202
|
+
+ 'doing the work alone.');
|
|
203
|
+
}
|
|
204
|
+
lines.push('Rules for each spec: a self-contained prompt (the child has no memory of this '
|
|
205
|
+
+ `conversation), a description of at most ${TEAM_LIMITS.descriptionChars} chars, a short `
|
|
206
|
+
+ 'slug label, and a disjoint set of files per child.');
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forced team mode: the SESSION STATE type and the PURE half of `/team`
|
|
3
|
+
* (team-command-forced-dispatch §3.3 / §3.5 / §5.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope, and this module
|
|
6
|
+
* also produces text a model reads in a `cmd.exe` terminal.
|
|
7
|
+
*
|
|
8
|
+
* WHAT IS AND IS NOT HERE. Every export below is a deterministic function of its
|
|
9
|
+
* arguments: no `CommandContext`, no controller, no IO, no React. That is what
|
|
10
|
+
* lets the whole grammar - control words, the quote escape hatch, the two
|
|
11
|
+
* ceilings, the disclosure line - be tested without a terminal, a config file or
|
|
12
|
+
* a model. The command body (`commands/team-command.ts`) keeps the ctx-dependent
|
|
13
|
+
* half: speaking to the user, arming the session, submitting the turn.
|
|
14
|
+
*
|
|
15
|
+
* THE INTENT IS PARSED IN ONE STEP (§3.3), because the three rules interact:
|
|
16
|
+
*
|
|
17
|
+
* 1. CONTROL WORDS ARE WHOLE-ARGUMENT ONLY. `off the record` is a brief whose
|
|
18
|
+
* first word happens to be `off`; treating it as "turn off, then take notes"
|
|
19
|
+
* would delete a legitimate brief from the grammar.
|
|
20
|
+
* 2. THE QUOTE ESCAPE HATCH IS CHECKED BEFORE THE CONTROL WORDS. `/team "off"`
|
|
21
|
+
* asks for a fan-out on the brief `off`. Without this rule the three control
|
|
22
|
+
* words would be permanently unavailable as briefs, and a user working on a
|
|
23
|
+
* feature called "off" would have no way to say so.
|
|
24
|
+
* 3. FLAG PARSING IS NOT REIMPLEMENTED. `parseSubagentArgs` owns the four error
|
|
25
|
+
* classes and both disclosure ceilings, and its bytes are pinned by
|
|
26
|
+
* `subagent-command.test.ts`; a second grammar here would drift from it on
|
|
27
|
+
* the first `-n=6` someone tried.
|
|
28
|
+
*/
|
|
29
|
+
import type { SubagentCommandArgs } from './forced-dispatch.js';
|
|
30
|
+
import type { TeamConfig } from '../config/schema.js';
|
|
31
|
+
/**
|
|
32
|
+
* The default count when the user names none (D-7).
|
|
33
|
+
*
|
|
34
|
+
* FOUR, not `maxSubagents`: `<team_mode>` tells the model two to four is usually
|
|
35
|
+
* enough, so "force one dispatch" must not silently become the most expensive
|
|
36
|
+
* call the session can make. The status line advertises the real ceiling so the
|
|
37
|
+
* user who wants the widest fan-out knows which flag to add.
|
|
38
|
+
*/
|
|
39
|
+
export declare const DEFAULT_FORCED_COUNT = 4;
|
|
40
|
+
/**
|
|
41
|
+
* What one `/team ...` argument string means. A TOTAL union: every input the
|
|
42
|
+
* lexer can see maps to exactly one member, which is what lets the command body
|
|
43
|
+
* be a flat dispatch rather than a chain of "did we already answer?" flags.
|
|
44
|
+
*/
|
|
45
|
+
export type TeamIntent = {
|
|
46
|
+
kind: 'status';
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'off';
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'on';
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'brief';
|
|
53
|
+
args: SubagentCommandArgs;
|
|
54
|
+
quoted: boolean;
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'error';
|
|
57
|
+
error: 'count' | 'conflict' | 'flag' | 'brief';
|
|
58
|
+
token?: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Session-level forced team mode: the state `/team` writes and `<forced_team_mode>`
|
|
62
|
+
* plus the status chip read (team-command-forced-dispatch §3.5 / §6.1).
|
|
63
|
+
*
|
|
64
|
+
* `null` MEANS "NOT ARMED" and is the constructor's value. It is never persisted
|
|
65
|
+
* (D-4): no `config.json` key, no session-file field, no checkpoint. A persisted
|
|
66
|
+
* "always fan out" would be inherited by `aragon exec` and by every launch after
|
|
67
|
+
* one experiment, at N children a turn - and the whole point of the bound in
|
|
68
|
+
* `dispatch-gate.ts` is that nobody may widen it by accident.
|
|
69
|
+
*/
|
|
70
|
+
export interface ForcedTeamState {
|
|
71
|
+
/** Children per turn. Already clamped to `[1, team.maxSubagents]`. */
|
|
72
|
+
count: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse the whole argument of `/team`. NEVER THROWS and never guesses.
|
|
76
|
+
*
|
|
77
|
+
* The empty argument is the STATUS question, not an error: "what is on?" is the
|
|
78
|
+
* one thing a user asks a mode command most often, and it must answer even in a
|
|
79
|
+
* session that cannot dispatch at all (that is exactly where they ask).
|
|
80
|
+
*/
|
|
81
|
+
export declare function readTeamIntent(raw: string): TeamIntent;
|
|
82
|
+
/**
|
|
83
|
+
* How many children the ONE directive text must ask for, and why the count
|
|
84
|
+
* shrank. Every field is read by either the directive or the disclosure line.
|
|
85
|
+
*/
|
|
86
|
+
export interface ForcedPlan {
|
|
87
|
+
brief: string;
|
|
88
|
+
/** Already narrowed to `[1, team.maxSubagents]`. */
|
|
89
|
+
count: number;
|
|
90
|
+
/** What the request implied BEFORE `maxSubagents` narrowed it. */
|
|
91
|
+
requested: number;
|
|
92
|
+
/** Already narrowed to `maxSubagents`; may be empty. */
|
|
93
|
+
parts: string[];
|
|
94
|
+
/** Parts that survived the structural cap, before the policy cap applied. */
|
|
95
|
+
partsAdmitted: number;
|
|
96
|
+
/** Parts the user asked for, before the structural `forcedPartsMax` cap. */
|
|
97
|
+
partsRequested: number;
|
|
98
|
+
readOnly: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Narrow the parsed intent against the LIVE width.
|
|
102
|
+
*
|
|
103
|
+
* TWO CEILINGS, IN THIS ORDER (R-P1-3): the structural `forcedPartsMax` bounds
|
|
104
|
+
* what one argument may carry (the parser already applied it), and the policy
|
|
105
|
+
* `maxSubagents` bounds what this session may dispatch - which only this layer
|
|
106
|
+
* can apply, because only it can read the live config.
|
|
107
|
+
*/
|
|
108
|
+
export declare function planForced(args: SubagentCommandArgs, team: TeamConfig): ForcedPlan;
|
|
109
|
+
/**
|
|
110
|
+
* The one line that says what the NEXT turn will do - a plan, not a result.
|
|
111
|
+
*
|
|
112
|
+
* IT IS PRINTED BEFORE THE TURN IS SUBMITTED and it never claims children
|
|
113
|
+
* started: forcing is instruction-level for the first turn, and a notice that
|
|
114
|
+
* said "4 subagents started" would be a promise the command cannot keep.
|
|
115
|
+
*/
|
|
116
|
+
export declare function planNotice(planned: ForcedPlan, team: TeamConfig): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forced team mode: the SESSION STATE type and the PURE half of `/team`
|
|
3
|
+
* (team-command-forced-dispatch §3.3 / §3.5 / §5.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope, and this module
|
|
6
|
+
* also produces text a model reads in a `cmd.exe` terminal.
|
|
7
|
+
*
|
|
8
|
+
* WHAT IS AND IS NOT HERE. Every export below is a deterministic function of its
|
|
9
|
+
* arguments: no `CommandContext`, no controller, no IO, no React. That is what
|
|
10
|
+
* lets the whole grammar - control words, the quote escape hatch, the two
|
|
11
|
+
* ceilings, the disclosure line - be tested without a terminal, a config file or
|
|
12
|
+
* a model. The command body (`commands/team-command.ts`) keeps the ctx-dependent
|
|
13
|
+
* half: speaking to the user, arming the session, submitting the turn.
|
|
14
|
+
*
|
|
15
|
+
* THE INTENT IS PARSED IN ONE STEP (§3.3), because the three rules interact:
|
|
16
|
+
*
|
|
17
|
+
* 1. CONTROL WORDS ARE WHOLE-ARGUMENT ONLY. `off the record` is a brief whose
|
|
18
|
+
* first word happens to be `off`; treating it as "turn off, then take notes"
|
|
19
|
+
* would delete a legitimate brief from the grammar.
|
|
20
|
+
* 2. THE QUOTE ESCAPE HATCH IS CHECKED BEFORE THE CONTROL WORDS. `/team "off"`
|
|
21
|
+
* asks for a fan-out on the brief `off`. Without this rule the three control
|
|
22
|
+
* words would be permanently unavailable as briefs, and a user working on a
|
|
23
|
+
* feature called "off" would have no way to say so.
|
|
24
|
+
* 3. FLAG PARSING IS NOT REIMPLEMENTED. `parseSubagentArgs` owns the four error
|
|
25
|
+
* classes and both disclosure ceilings, and its bytes are pinned by
|
|
26
|
+
* `subagent-command.test.ts`; a second grammar here would drift from it on
|
|
27
|
+
* the first `-n=6` someone tried.
|
|
28
|
+
*/
|
|
29
|
+
import { parseSubagentArgs } from './forced-dispatch.js';
|
|
30
|
+
/**
|
|
31
|
+
* The default count when the user names none (D-7).
|
|
32
|
+
*
|
|
33
|
+
* FOUR, not `maxSubagents`: `<team_mode>` tells the model two to four is usually
|
|
34
|
+
* enough, so "force one dispatch" must not silently become the most expensive
|
|
35
|
+
* call the session can make. The status line advertises the real ceiling so the
|
|
36
|
+
* user who wants the widest fan-out knows which flag to add.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_FORCED_COUNT = 4;
|
|
39
|
+
/** The three words that command the session rather than describe work. */
|
|
40
|
+
const CONTROL_WORDS = new Set(['on', 'off', 'status']);
|
|
41
|
+
/**
|
|
42
|
+
* The argument with one pair of wrapping double quotes removed, or `null`.
|
|
43
|
+
*
|
|
44
|
+
* DELIBERATELY ONE PAIR AND NOT A LEXER. `/team "off"` and `/team "the off
|
|
45
|
+
* switch"` both work; nested or unbalanced quotes fall through to the ordinary
|
|
46
|
+
* parser, where the text simply keeps its quotes - the honest outcome for input
|
|
47
|
+
* the grammar does not claim.
|
|
48
|
+
*/
|
|
49
|
+
function unquoted(raw) {
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
if (trimmed.length < 2)
|
|
52
|
+
return null;
|
|
53
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"'))
|
|
54
|
+
return null;
|
|
55
|
+
return trimmed.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse the whole argument of `/team`. NEVER THROWS and never guesses.
|
|
59
|
+
*
|
|
60
|
+
* The empty argument is the STATUS question, not an error: "what is on?" is the
|
|
61
|
+
* one thing a user asks a mode command most often, and it must answer even in a
|
|
62
|
+
* session that cannot dispatch at all (that is exactly where they ask).
|
|
63
|
+
*/
|
|
64
|
+
export function readTeamIntent(raw) {
|
|
65
|
+
const trimmed = raw.trim();
|
|
66
|
+
if (trimmed.length === 0)
|
|
67
|
+
return { kind: 'status' };
|
|
68
|
+
// RULE 2, FIRST: the escape hatch has to win, or the control words could never
|
|
69
|
+
// be briefs at all.
|
|
70
|
+
const inner = unquoted(trimmed);
|
|
71
|
+
if (inner !== null) {
|
|
72
|
+
const parsed = parseSubagentArgs(inner);
|
|
73
|
+
if (!parsed.ok)
|
|
74
|
+
return { kind: 'error', error: parsed.error, token: parsed.token };
|
|
75
|
+
return { kind: 'brief', args: parsed.args, quoted: true };
|
|
76
|
+
}
|
|
77
|
+
// RULE 1: whole-argument equality, compared case-insensitively.
|
|
78
|
+
const word = trimmed.toLowerCase();
|
|
79
|
+
if (CONTROL_WORDS.has(word))
|
|
80
|
+
return { kind: word };
|
|
81
|
+
// RULE 3: everything else is a brief, parsed by the one grammar there is.
|
|
82
|
+
const parsed = parseSubagentArgs(trimmed);
|
|
83
|
+
if (!parsed.ok)
|
|
84
|
+
return { kind: 'error', error: parsed.error, token: parsed.token };
|
|
85
|
+
return { kind: 'brief', args: parsed.args, quoted: false };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Narrow the parsed intent against the LIVE width.
|
|
89
|
+
*
|
|
90
|
+
* TWO CEILINGS, IN THIS ORDER (R-P1-3): the structural `forcedPartsMax` bounds
|
|
91
|
+
* what one argument may carry (the parser already applied it), and the policy
|
|
92
|
+
* `maxSubagents` bounds what this session may dispatch - which only this layer
|
|
93
|
+
* can apply, because only it can read the live config.
|
|
94
|
+
*/
|
|
95
|
+
export function planForced(args, team) {
|
|
96
|
+
const max = team.maxSubagents;
|
|
97
|
+
if (args.parts.length > 0) {
|
|
98
|
+
// A PARTS LIST STARTS WITH A CHILD PER SURVIVING PART, and `requested` is the
|
|
99
|
+
// number the user TYPED - not the structural 8 the parser already applied.
|
|
100
|
+
// Quoting the post-truncation count made the old notice tell a user who asked
|
|
101
|
+
// for eleven parts that they had asked for eight (R-4: the notice and the
|
|
102
|
+
// directive must both name the user's number).
|
|
103
|
+
const parts = args.parts.slice(0, max);
|
|
104
|
+
return {
|
|
105
|
+
brief: args.brief,
|
|
106
|
+
parts,
|
|
107
|
+
partsAdmitted: args.parts.length,
|
|
108
|
+
partsRequested: args.partsRequested,
|
|
109
|
+
requested: args.partsRequested,
|
|
110
|
+
count: parts.length,
|
|
111
|
+
readOnly: args.readOnly,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const requested = args.count ?? Math.min(DEFAULT_FORCED_COUNT, max);
|
|
115
|
+
return {
|
|
116
|
+
brief: args.brief,
|
|
117
|
+
parts: [],
|
|
118
|
+
partsAdmitted: 0,
|
|
119
|
+
partsRequested: 0,
|
|
120
|
+
requested,
|
|
121
|
+
count: Math.max(1, Math.min(requested, max)),
|
|
122
|
+
readOnly: args.readOnly,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The one line that says what the NEXT turn will do - a plan, not a result.
|
|
127
|
+
*
|
|
128
|
+
* IT IS PRINTED BEFORE THE TURN IS SUBMITTED and it never claims children
|
|
129
|
+
* started: forcing is instruction-level for the first turn, and a notice that
|
|
130
|
+
* said "4 subagents started" would be a promise the command cannot keep.
|
|
131
|
+
*/
|
|
132
|
+
export function planNotice(planned, team) {
|
|
133
|
+
const noun = planned.count === 1 ? 'subagent' : 'subagents';
|
|
134
|
+
const head = `Forcing a fan-out of ${planned.count} ${noun}`;
|
|
135
|
+
const followUp = `${head} (max ${team.maxSubagents}) - the next turn dispatches them. `
|
|
136
|
+
+ 'Esc stops the turn.';
|
|
137
|
+
// TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may
|
|
138
|
+
// carry and `team.maxSubagents` bounds what may be dispatched; naming the wrong
|
|
139
|
+
// one sends the user to change a number that is not the reason.
|
|
140
|
+
if (planned.parts.length > 0) {
|
|
141
|
+
if (planned.parts.length < planned.partsAdmitted) {
|
|
142
|
+
return `${head} (asked for ${planned.partsRequested} parts; `
|
|
143
|
+
+ `team.maxSubagents is ${team.maxSubagents}).`;
|
|
144
|
+
}
|
|
145
|
+
if (planned.partsAdmitted < planned.partsRequested) {
|
|
146
|
+
return `${head} (asked for ${planned.partsRequested} parts, `
|
|
147
|
+
+ `the first ${planned.parts.length} are used).`;
|
|
148
|
+
}
|
|
149
|
+
return followUp;
|
|
150
|
+
}
|
|
151
|
+
if (planned.requested > planned.count) {
|
|
152
|
+
return `${head} (asked for ${planned.requested}; `
|
|
153
|
+
+ `team.maxSubagents is ${team.maxSubagents}).`;
|
|
154
|
+
}
|
|
155
|
+
return followUp;
|
|
156
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<forced_team_mode>`: the SECOND of the three layers that make `/team <brief>`
|
|
3
|
+
* mean something (team-command-forced-dispatch §3.6 / §6.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* WHY A BLOCK AT ALL. The first layer - the directive `buildForcedDirective()`
|
|
8
|
+
* submits - is a single user turn, and it dies with that turn. The session stays
|
|
9
|
+
* in forced mode until `/team off`, so every LATER turn needs the same
|
|
10
|
+
* constraint, and the only surface that survives a turn is the system prompt.
|
|
11
|
+
*
|
|
12
|
+
* WHY NOT SIMPLY WIDEN `<team_mode>`. That block says "you CAN dispatch"; this
|
|
13
|
+
* one says "you MUST". Merging them would make the permission and the obligation
|
|
14
|
+
* one string, and a session that only needs the first (an ordinary team session)
|
|
15
|
+
* would start paying for the second's words and, worse, inheriting its tone.
|
|
16
|
+
*
|
|
17
|
+
* THE LAST PARAGRAPH IS A SAFETY VALVE AND MUST STAY LAST: "If the task tool is
|
|
18
|
+
* unavailable this turn, say so in one line" is what remains when all three
|
|
19
|
+
* layers fail, and it has to be the last sentence the model read. The plan-mode
|
|
20
|
+
* variant is therefore spliced BEFORE `Answer without dispatching`, never at the
|
|
21
|
+
* end (§6.3, P1-8).
|
|
22
|
+
*/
|
|
23
|
+
import { FORCED_TEAM_BLOCK_VERSION } from './limits.js';
|
|
24
|
+
/**
|
|
25
|
+
* What one `<forced_team_mode>` rendering needs, and NOTHING ELSE.
|
|
26
|
+
*
|
|
27
|
+
* NO CEILING FIELD (§3.6 lists `maxSubagents`; §6.3 never spends it). The block
|
|
28
|
+
* names one number twice - the armed count - and `forced-prompt.test.ts` asserts
|
|
29
|
+
* that no ceiling appears in it: a sentence offering "up to 8" beside "Count: 4"
|
|
30
|
+
* is read as permission for eight, which is the cost D-7 caps at four. Carrying
|
|
31
|
+
* an unused ceiling here would invite exactly that sentence back.
|
|
32
|
+
*/
|
|
33
|
+
export interface ForcedTeamBlockParams {
|
|
34
|
+
/** Already narrowed to `[1, team.maxSubagents]` by `armForcedTeam()`. */
|
|
35
|
+
count: number;
|
|
36
|
+
/** Plan mode variant sentence: dispatch read-only research, then `submit_plan`. */
|
|
37
|
+
planMode: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Render the block for one prompt build.
|
|
41
|
+
*
|
|
42
|
+
* NO TRUNCATION PATH (unlike the team report): the text is a constant with two
|
|
43
|
+
* numbers in it, so its length is knowable at build time, and
|
|
44
|
+
* `forced-prompt.test.ts` asserts both the ASCII-only rule and the ceiling. A
|
|
45
|
+
* silent elision would delete the safety valve this block ends with.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildForcedTeamBlock(params: ForcedTeamBlockParams): string;
|
|
48
|
+
export { FORCED_TEAM_BLOCK_VERSION };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<forced_team_mode>`: the SECOND of the three layers that make `/team <brief>`
|
|
3
|
+
* mean something (team-command-forced-dispatch §3.6 / §6.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* WHY A BLOCK AT ALL. The first layer - the directive `buildForcedDirective()`
|
|
8
|
+
* submits - is a single user turn, and it dies with that turn. The session stays
|
|
9
|
+
* in forced mode until `/team off`, so every LATER turn needs the same
|
|
10
|
+
* constraint, and the only surface that survives a turn is the system prompt.
|
|
11
|
+
*
|
|
12
|
+
* WHY NOT SIMPLY WIDEN `<team_mode>`. That block says "you CAN dispatch"; this
|
|
13
|
+
* one says "you MUST". Merging them would make the permission and the obligation
|
|
14
|
+
* one string, and a session that only needs the first (an ordinary team session)
|
|
15
|
+
* would start paying for the second's words and, worse, inheriting its tone.
|
|
16
|
+
*
|
|
17
|
+
* THE LAST PARAGRAPH IS A SAFETY VALVE AND MUST STAY LAST: "If the task tool is
|
|
18
|
+
* unavailable this turn, say so in one line" is what remains when all three
|
|
19
|
+
* layers fail, and it has to be the last sentence the model read. The plan-mode
|
|
20
|
+
* variant is therefore spliced BEFORE `Answer without dispatching`, never at the
|
|
21
|
+
* end (§6.3, P1-8).
|
|
22
|
+
*/
|
|
23
|
+
import { FORCED_TEAM_BLOCK_MAX_CHARS, FORCED_TEAM_BLOCK_VERSION, } from './limits.js';
|
|
24
|
+
/** The plan-mode sentence, spliced before the "answer without dispatching" line. */
|
|
25
|
+
const PLAN_SENTENCE = 'Plan mode is on: dispatch the read-only research '
|
|
26
|
+
+ '(every spec readOnly: true), then call submit_plan.';
|
|
27
|
+
/**
|
|
28
|
+
* The block's body, in the exact order and wording §6.3 fixes.
|
|
29
|
+
*
|
|
30
|
+
* `{count}` appears TWICE (the `Count:` line and the "more than N" clause) and
|
|
31
|
+
* both are the same number on purpose: a block that allowed "up to maxSubagents"
|
|
32
|
+
* in one sentence and "N" in the other would be read as authorising the wider
|
|
33
|
+
* number, which is exactly the cost D-7 caps at four.
|
|
34
|
+
*/
|
|
35
|
+
function body(count, planMode) {
|
|
36
|
+
const wide = [
|
|
37
|
+
`<forced_team_mode ${FORCED_TEAM_BLOCK_VERSION}>`,
|
|
38
|
+
'The user put this session in forced team mode. Work is done by subagents, not by you.',
|
|
39
|
+
'For every turn that produces or changes something, call the `task` tool ONCE and let',
|
|
40
|
+
'the children do it. Never do that work yourself, and never end such a turn having done',
|
|
41
|
+
'it alone: a turn that did the work itself is a failed turn here.',
|
|
42
|
+
`Count: ${count} subagents unless the work genuinely splits into fewer. `
|
|
43
|
+
+ 'Two to four is usual;',
|
|
44
|
+
`more than ${count} only when the parts are truly independent. Each spec needs a `
|
|
45
|
+
+ 'self-contained',
|
|
46
|
+
'prompt (the child has no memory of this conversation), a short slug label, a description',
|
|
47
|
+
'of at most 60 chars, and a disjoint set of files.',
|
|
48
|
+
'You may plan first: research with read_file / glob / grep, or write the plan with',
|
|
49
|
+
'todo_write. Planning is yours; executing the plan is theirs. Relay the combined report',
|
|
50
|
+
'in your own words and say which child did what.',
|
|
51
|
+
];
|
|
52
|
+
// SPLICED HERE, BEFORE the closing sentence, for the reason the header records.
|
|
53
|
+
const plan = planMode ? [PLAN_SENTENCE] : [];
|
|
54
|
+
return [
|
|
55
|
+
...wide,
|
|
56
|
+
...plan,
|
|
57
|
+
'Answer without dispatching only when the turn is a question this conversation already',
|
|
58
|
+
'answers - no new files, no new commands, no new findings.',
|
|
59
|
+
'If the task tool is unavailable this turn, say so in one line instead of doing the work',
|
|
60
|
+
'alone.',
|
|
61
|
+
'</forced_team_mode>',
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Render the block for one prompt build.
|
|
66
|
+
*
|
|
67
|
+
* NO TRUNCATION PATH (unlike the team report): the text is a constant with two
|
|
68
|
+
* numbers in it, so its length is knowable at build time, and
|
|
69
|
+
* `forced-prompt.test.ts` asserts both the ASCII-only rule and the ceiling. A
|
|
70
|
+
* silent elision would delete the safety valve this block ends with.
|
|
71
|
+
*/
|
|
72
|
+
export function buildForcedTeamBlock(params) {
|
|
73
|
+
const count = Math.max(1, Math.floor(params.count));
|
|
74
|
+
const text = body(count, params.planMode).join('\n');
|
|
75
|
+
// A CEILING, NOT A CLIP. Every sentence in this block is load-bearing, so a
|
|
76
|
+
// length overrun is a wording bug to fix in the source rather than a paragraph
|
|
77
|
+
// to lose at run time - and this branch is unreachable today (1143 chars).
|
|
78
|
+
if (text.length > FORCED_TEAM_BLOCK_MAX_CHARS) {
|
|
79
|
+
throw new Error(`forced team block is ${text.length} chars, ceiling is `
|
|
80
|
+
+ `${FORCED_TEAM_BLOCK_MAX_CHARS} (team/forced-prompt.ts)`);
|
|
81
|
+
}
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
export { FORCED_TEAM_BLOCK_VERSION };
|