@aragon-agent/cli 0.6.40 → 0.6.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2632 -2589
- package/README.md +3720 -3706
- package/dist/agent/controller.d.ts +36 -5
- package/dist/agent/controller.js +103 -21
- 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/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 +40 -0
- package/dist/team/limits.js +48 -0
- package/dist/team/model.d.ts +5 -5
- package/dist/team/model.js +5 -5
- package/dist/ui/App.js +116 -10
- 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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dispatch gate: the THIRD and only MECHANICAL layer of forced team mode
|
|
3
|
+
* (team-command-forced-dispatch §3.7 / §6.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* WHY A MECHANICAL LAYER EXISTS AT ALL. The directive and the prompt block are
|
|
8
|
+
* both requests, and a request can be declined. The requirement behind `/team`
|
|
9
|
+
* is a COMMITMENT - "after the lead plans, subagents must do the work" - so the
|
|
10
|
+
* one part of it that does not depend on the model's cooperation is a decision
|
|
11
|
+
* taken by the host after the turn has already ended. Everything here is a pure
|
|
12
|
+
* function of four facts the caller counted, which is what keeps `ui/App.tsx`
|
|
13
|
+
* to wiring rather than policy.
|
|
14
|
+
*
|
|
15
|
+
* THREE BOUNDS, AND NONE OF THEM IS DECORATION (D-8):
|
|
16
|
+
* - `toolsUsed > 0` - a pure question ("what did those two children find?")
|
|
17
|
+
* must be answerable from context; nudging it would buy a
|
|
18
|
+
* second dispatch of a fact already in the transcript.
|
|
19
|
+
* - `nudgesUsed < 1` - a model that will not dispatch must not turn into an
|
|
20
|
+
* unbounded paid loop. `FORCED_NUDGE_LIMIT` is the whole
|
|
21
|
+
* defence, and I-10 pins WHERE it is reset (the user's own
|
|
22
|
+
* submit, never `agent_start`) because a per-turn reset
|
|
23
|
+
* would make this bound disappear while every test stayed
|
|
24
|
+
* green.
|
|
25
|
+
* - `!aborted && !errored` - Esc and a provider failure are not disobedience.
|
|
26
|
+
* Urging a re-dispatch after an error would spend money to
|
|
27
|
+
* reproduce the error.
|
|
28
|
+
*
|
|
29
|
+
* `dispatchAttempted` IS NOT `dispatched` (P1-7). The `task` tool refuses for
|
|
30
|
+
* three reasons that never emit `dispatch_start` - team off, dispatch busy, no
|
|
31
|
+
* API key - and each of those refusals is the upstream telling the model "no".
|
|
32
|
+
* Counting only real dispatches would have the gate say "you dispatched nothing,
|
|
33
|
+
* dispatch now", the model comply, and the same refusal arrive twice: two turns
|
|
34
|
+
* of noise around a fact both layers already knew.
|
|
35
|
+
*/
|
|
36
|
+
/** The four facts the caller counts, and the run's own verdict on itself. */
|
|
37
|
+
export interface ForcedDispatchProbe {
|
|
38
|
+
/** `getForcedTeam() !== null`. */
|
|
39
|
+
armed: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The armed fan-out count, echoed into the nudge's first sentence.
|
|
42
|
+
*
|
|
43
|
+
* NOT IN §3.7's FIVE-FIELD PROBE, and the omission does not survive contact
|
|
44
|
+
* with §5.2: the decision carries the MESSAGE (`{ kind: 'nudge'; message }`)
|
|
45
|
+
* and `App` submits `gate.message` verbatim, so the number the nudge names has
|
|
46
|
+
* to reach this function somehow. Reading it here - from the same probe the
|
|
47
|
+
* gate already receives - keeps the decision a pure function of its argument
|
|
48
|
+
* instead of forcing the caller to rebuild the message and losing the guarantee
|
|
49
|
+
* that `buildDispatchNudge()` is the only author of those bytes. Ignored when
|
|
50
|
+
* `armed` is false (see `team-command-forced-dispatch` spec,
|
|
51
|
+
* `## 实施过程发现的方案缺陷`).
|
|
52
|
+
*/
|
|
53
|
+
count: number;
|
|
54
|
+
/** This turn emitted `dispatch_start` OR called `task` at least once (even refused). */
|
|
55
|
+
dispatchAttempted: boolean;
|
|
56
|
+
/** Tool calls the MODEL made this turn. Zero means a pure answer. */
|
|
57
|
+
toolsUsed: number;
|
|
58
|
+
/** Fallback nudges already spent. Reset only by a user-initiated submit (I-10). */
|
|
59
|
+
nudgesUsed: number;
|
|
60
|
+
runEnd: {
|
|
61
|
+
aborted: boolean;
|
|
62
|
+
errored: boolean;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export type ForcedDispatchDecision = {
|
|
66
|
+
kind: 'none';
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'nudge';
|
|
69
|
+
message: string;
|
|
70
|
+
};
|
|
71
|
+
/** The line written to the USER before the nudge is submitted (§6.4). */
|
|
72
|
+
export declare const FORCED_NUDGE_NOTICE = "Forced team mode: no dispatch last turn - asking the agent to dispatch now.";
|
|
73
|
+
/**
|
|
74
|
+
* The nudge itself, verbatim from §6.4.
|
|
75
|
+
*
|
|
76
|
+
* It names a NUMBER and then immediately permits fewer, because the one thing
|
|
77
|
+
* worse than a missing dispatch is four children inventing work to justify
|
|
78
|
+
* themselves. The closing sentence repeats the directive's prohibition: this
|
|
79
|
+
* message is the model's whole context for the turn it is about to run.
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildDispatchNudge(count: number): string;
|
|
82
|
+
/**
|
|
83
|
+
* Should the host ask for one more turn? Every clause must hold, short-circuiting
|
|
84
|
+
* in the order the header records so a cheap fact answers before a costly one.
|
|
85
|
+
*/
|
|
86
|
+
export declare function decideForcedDispatch(probe: ForcedDispatchProbe): ForcedDispatchDecision;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dispatch gate: the THIRD and only MECHANICAL layer of forced team mode
|
|
3
|
+
* (team-command-forced-dispatch §3.7 / §6.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* WHY A MECHANICAL LAYER EXISTS AT ALL. The directive and the prompt block are
|
|
8
|
+
* both requests, and a request can be declined. The requirement behind `/team`
|
|
9
|
+
* is a COMMITMENT - "after the lead plans, subagents must do the work" - so the
|
|
10
|
+
* one part of it that does not depend on the model's cooperation is a decision
|
|
11
|
+
* taken by the host after the turn has already ended. Everything here is a pure
|
|
12
|
+
* function of four facts the caller counted, which is what keeps `ui/App.tsx`
|
|
13
|
+
* to wiring rather than policy.
|
|
14
|
+
*
|
|
15
|
+
* THREE BOUNDS, AND NONE OF THEM IS DECORATION (D-8):
|
|
16
|
+
* - `toolsUsed > 0` - a pure question ("what did those two children find?")
|
|
17
|
+
* must be answerable from context; nudging it would buy a
|
|
18
|
+
* second dispatch of a fact already in the transcript.
|
|
19
|
+
* - `nudgesUsed < 1` - a model that will not dispatch must not turn into an
|
|
20
|
+
* unbounded paid loop. `FORCED_NUDGE_LIMIT` is the whole
|
|
21
|
+
* defence, and I-10 pins WHERE it is reset (the user's own
|
|
22
|
+
* submit, never `agent_start`) because a per-turn reset
|
|
23
|
+
* would make this bound disappear while every test stayed
|
|
24
|
+
* green.
|
|
25
|
+
* - `!aborted && !errored` - Esc and a provider failure are not disobedience.
|
|
26
|
+
* Urging a re-dispatch after an error would spend money to
|
|
27
|
+
* reproduce the error.
|
|
28
|
+
*
|
|
29
|
+
* `dispatchAttempted` IS NOT `dispatched` (P1-7). The `task` tool refuses for
|
|
30
|
+
* three reasons that never emit `dispatch_start` - team off, dispatch busy, no
|
|
31
|
+
* API key - and each of those refusals is the upstream telling the model "no".
|
|
32
|
+
* Counting only real dispatches would have the gate say "you dispatched nothing,
|
|
33
|
+
* dispatch now", the model comply, and the same refusal arrive twice: two turns
|
|
34
|
+
* of noise around a fact both layers already knew.
|
|
35
|
+
*/
|
|
36
|
+
import { FORCED_NUDGE_LIMIT } from './limits.js';
|
|
37
|
+
/** The line written to the USER before the nudge is submitted (§6.4). */
|
|
38
|
+
export const FORCED_NUDGE_NOTICE = 'Forced team mode: no dispatch last turn - asking the agent to dispatch now.';
|
|
39
|
+
/**
|
|
40
|
+
* The nudge itself, verbatim from §6.4.
|
|
41
|
+
*
|
|
42
|
+
* It names a NUMBER and then immediately permits fewer, because the one thing
|
|
43
|
+
* worse than a missing dispatch is four children inventing work to justify
|
|
44
|
+
* themselves. The closing sentence repeats the directive's prohibition: this
|
|
45
|
+
* message is the model's whole context for the turn it is about to run.
|
|
46
|
+
*/
|
|
47
|
+
export function buildDispatchNudge(count) {
|
|
48
|
+
return [
|
|
49
|
+
'Forced team mode is on and the last turn dispatched nothing. Call the `task` tool now',
|
|
50
|
+
`with ${count} subagents - or fewer if the work genuinely does not split - then report `
|
|
51
|
+
+ 'their',
|
|
52
|
+
'combined result. Do not do the work yourself.',
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Should the host ask for one more turn? Every clause must hold, short-circuiting
|
|
57
|
+
* in the order the header records so a cheap fact answers before a costly one.
|
|
58
|
+
*/
|
|
59
|
+
export function decideForcedDispatch(probe) {
|
|
60
|
+
if (!probe.armed)
|
|
61
|
+
return { kind: 'none' };
|
|
62
|
+
// A REFUSED `task` STILL COUNTS (P1-7): "attempted" is the honest predicate.
|
|
63
|
+
if (probe.dispatchAttempted)
|
|
64
|
+
return { kind: 'none' };
|
|
65
|
+
if (!(probe.toolsUsed > 0))
|
|
66
|
+
return { kind: 'none' };
|
|
67
|
+
if (probe.runEnd.aborted || probe.runEnd.errored)
|
|
68
|
+
return { kind: 'none' };
|
|
69
|
+
if (probe.nudgesUsed >= FORCED_NUDGE_LIMIT)
|
|
70
|
+
return { kind: 'none' };
|
|
71
|
+
// THE MESSAGE IS BUILT HERE, from the same probe, so `buildDispatchNudge()`
|
|
72
|
+
// stays the only author of these bytes and the caller can submit
|
|
73
|
+
// `decision.message` without reconstructing it.
|
|
74
|
+
return { kind: 'nudge', message: buildDispatchNudge(probe.count) };
|
|
75
|
+
}
|
|
@@ -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 };
|
package/dist/team/limits.d.ts
CHANGED
|
@@ -266,3 +266,43 @@ export declare const TEAM_SUBAGENT_TOOL_NAMES: ReadonlySet<string>;
|
|
|
266
266
|
* report can be tied to a block revision with one grep.
|
|
267
267
|
*/
|
|
268
268
|
export declare const TEAM_BLOCK_VERSION = "v2-2026-10";
|
|
269
|
+
/**
|
|
270
|
+
* The version string on `<forced_team_mode>`'s first line.
|
|
271
|
+
*
|
|
272
|
+
* The block's words are a CONTRACT with the model, so a behaviour report ("it
|
|
273
|
+
* still did the work itself") has to be tieable to one revision. Kept beside
|
|
274
|
+
* `TEAM_BLOCK_VERSION` rather than reusing it: the two blocks answer different
|
|
275
|
+
* questions ("can dispatch" versus "must dispatch") and a shared number would
|
|
276
|
+
* name neither revision.
|
|
277
|
+
*/
|
|
278
|
+
export declare const FORCED_TEAM_BLOCK_VERSION = "v1-2026-10";
|
|
279
|
+
/**
|
|
280
|
+
* Length ceiling of the `<forced_team_mode>` text.
|
|
281
|
+
*
|
|
282
|
+
* THE FIRST BLOCK LENGTH IN THIS SUBSYSTEM THAT A TEST ASSERTS: the `<team_mode>`
|
|
283
|
+
* block's 1200 is a comment in `team/prompt.ts`, and unifying the two is a
|
|
284
|
+
* separate small refactor (deliberately out of scope here). Looser than 1200
|
|
285
|
+
* because this block is only ever paid for while the session is ARMED, and it
|
|
286
|
+
* carries three layers of instruction rather than one.
|
|
287
|
+
*/
|
|
288
|
+
export declare const FORCED_TEAM_BLOCK_MAX_CHARS = 1400;
|
|
289
|
+
/**
|
|
290
|
+
* Fallback nudges per user turn. ONE, and not configurable.
|
|
291
|
+
*
|
|
292
|
+
* The gate exists because the first two layers (the directive and the prompt
|
|
293
|
+
* block) both depend on the model cooperating. A second nudge would double the
|
|
294
|
+
* worst case of a model that is simply not going to dispatch - at N children per
|
|
295
|
+
* attempt - and D-8's whole argument is that the mechanical layer must be
|
|
296
|
+
* bounded by something other than the model's willingness.
|
|
297
|
+
*/
|
|
298
|
+
export declare const FORCED_NUDGE_LIMIT = 1;
|
|
299
|
+
/**
|
|
300
|
+
* Grace window before the fallback nudge fires, in milliseconds.
|
|
301
|
+
*
|
|
302
|
+
* SAME ORDER AS THE TODO FOLLOW-THROUGH GRACE, for a structural reason rather
|
|
303
|
+
* than a stylistic one: both continuations share ONE `followTimer` in `App`
|
|
304
|
+
* (I-9), so "at most one armed continuation" is only explicable while the two
|
|
305
|
+
* numbers stay in the same order of magnitude. **Changing todo's grace means
|
|
306
|
+
* re-reading this constant.**
|
|
307
|
+
*/
|
|
308
|
+
export declare const FORCED_NUDGE_GRACE_MS = 3000;
|
package/dist/team/limits.js
CHANGED
|
@@ -280,3 +280,51 @@ export const TEAM_SUBAGENT_TOOL_NAMES = new Set(['team_send', 'team_wait']);
|
|
|
280
280
|
* report can be tied to a block revision with one grep.
|
|
281
281
|
*/
|
|
282
282
|
export const TEAM_BLOCK_VERSION = 'v2-2026-10';
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Forced team mode (`/team <brief>`, team-command-forced-dispatch §6.5).
|
|
285
|
+
//
|
|
286
|
+
// FOUR STRUCTURAL NUMBERS, here rather than in the modules that read them for
|
|
287
|
+
// the reason this file's header records: none of them is a user preference, and
|
|
288
|
+
// a `config.json` key for any of them would make "the model ignored the
|
|
289
|
+
// instruction" tunable by the person the instruction exists to protect.
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
/**
|
|
292
|
+
* The version string on `<forced_team_mode>`'s first line.
|
|
293
|
+
*
|
|
294
|
+
* The block's words are a CONTRACT with the model, so a behaviour report ("it
|
|
295
|
+
* still did the work itself") has to be tieable to one revision. Kept beside
|
|
296
|
+
* `TEAM_BLOCK_VERSION` rather than reusing it: the two blocks answer different
|
|
297
|
+
* questions ("can dispatch" versus "must dispatch") and a shared number would
|
|
298
|
+
* name neither revision.
|
|
299
|
+
*/
|
|
300
|
+
export const FORCED_TEAM_BLOCK_VERSION = 'v1-2026-10';
|
|
301
|
+
/**
|
|
302
|
+
* Length ceiling of the `<forced_team_mode>` text.
|
|
303
|
+
*
|
|
304
|
+
* THE FIRST BLOCK LENGTH IN THIS SUBSYSTEM THAT A TEST ASSERTS: the `<team_mode>`
|
|
305
|
+
* block's 1200 is a comment in `team/prompt.ts`, and unifying the two is a
|
|
306
|
+
* separate small refactor (deliberately out of scope here). Looser than 1200
|
|
307
|
+
* because this block is only ever paid for while the session is ARMED, and it
|
|
308
|
+
* carries three layers of instruction rather than one.
|
|
309
|
+
*/
|
|
310
|
+
export const FORCED_TEAM_BLOCK_MAX_CHARS = 1400;
|
|
311
|
+
/**
|
|
312
|
+
* Fallback nudges per user turn. ONE, and not configurable.
|
|
313
|
+
*
|
|
314
|
+
* The gate exists because the first two layers (the directive and the prompt
|
|
315
|
+
* block) both depend on the model cooperating. A second nudge would double the
|
|
316
|
+
* worst case of a model that is simply not going to dispatch - at N children per
|
|
317
|
+
* attempt - and D-8's whole argument is that the mechanical layer must be
|
|
318
|
+
* bounded by something other than the model's willingness.
|
|
319
|
+
*/
|
|
320
|
+
export const FORCED_NUDGE_LIMIT = 1;
|
|
321
|
+
/**
|
|
322
|
+
* Grace window before the fallback nudge fires, in milliseconds.
|
|
323
|
+
*
|
|
324
|
+
* SAME ORDER AS THE TODO FOLLOW-THROUGH GRACE, for a structural reason rather
|
|
325
|
+
* than a stylistic one: both continuations share ONE `followTimer` in `App`
|
|
326
|
+
* (I-9), so "at most one armed continuation" is only explicable while the two
|
|
327
|
+
* numbers stay in the same order of magnitude. **Changing todo's grace means
|
|
328
|
+
* re-reading this constant.**
|
|
329
|
+
*/
|
|
330
|
+
export const FORCED_NUDGE_GRACE_MS = 3000;
|