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