@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +2632 -2589
  2. package/README.md +3720 -3706
  3. package/dist/agent/controller.d.ts +36 -5
  4. package/dist/agent/controller.js +103 -21
  5. package/dist/agent/system-prompt.d.ts +19 -0
  6. package/dist/agent/system-prompt.js +5 -0
  7. package/dist/commands/builtins.js +16 -182
  8. package/dist/commands/subagent-command.d.ts +21 -16
  9. package/dist/commands/subagent-command.js +22 -225
  10. package/dist/commands/team-command.d.ts +49 -0
  11. package/dist/commands/team-command.js +259 -0
  12. package/dist/config/model-kits.d.ts +2 -1
  13. package/dist/config/store.js +3 -2
  14. package/dist/team/dispatch-gate.d.ts +86 -0
  15. package/dist/team/dispatch-gate.js +75 -0
  16. package/dist/team/forced-mode.d.ts +116 -0
  17. package/dist/team/forced-mode.js +156 -0
  18. package/dist/team/forced-prompt.d.ts +48 -0
  19. package/dist/team/forced-prompt.js +84 -0
  20. package/dist/team/limits.d.ts +40 -0
  21. package/dist/team/limits.js +48 -0
  22. package/dist/team/model.d.ts +5 -5
  23. package/dist/team/model.js +5 -5
  24. package/dist/ui/App.js +116 -10
  25. package/dist/ui/StatusBar.d.ts +14 -0
  26. package/dist/ui/StatusBar.js +1 -0
  27. package/dist/ui/layout/status-layout.d.ts +17 -0
  28. package/dist/ui/layout/status-layout.js +9 -0
  29. package/dist/ui/overlays/HelpOverlay.js +8 -4
  30. package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
  31. package/dist/ui/overlays/SettingsScreen.js +17 -7
  32. package/dist/ui/overlays/TeamFanoutPage.d.ts +17 -1
  33. package/dist/ui/overlays/TeamFanoutPage.js +23 -11
  34. package/dist/ui/overlays/kit-settings-state.js +3 -2
  35. package/dist/ui/overlays/model-profile-state.d.ts +1 -1
  36. package/dist/ui/overlays/team-fanout-state.d.ts +46 -13
  37. package/dist/ui/overlays/team-fanout-state.js +78 -20
  38. package/package.json +81 -81
@@ -5,16 +5,16 @@
5
5
  * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
6
  *
7
7
  * EVERY CONSUMER READS THE CHILD'S MODEL THROUGH THIS FUNCTION AND NOWHERE
8
- * ELSE — `createSubagent`, the `task` report header, the settings root row and
9
- * `/team model` — following the `resolveFastTier` precedent one directory over:
10
- * four call sites each recomputing "which model do children run on?" from raw
8
+ * ELSE — `createSubagent`, the `task` report header and the settings root row —
9
+ * following the `resolveFastTier` precedent one directory over: three call
10
+ * sites each recomputing "which model do children run on?" from raw
11
11
  * config keys is exactly how the report ends up naming a model the children
12
12
  * silently fell back from.
13
13
  *
14
14
  * IT IS RE-RUN ON EVERY DISPATCH AND EVERY RENDER, NOT ONCE (rule 8 / RV-3 of
15
15
  * the fast tier, inherited here): `team.model`, the fast tier and the profile
16
- * library are all LIVE keys a settings save, `/fast off` or `/team model` can
17
- * rewrite mid-session. A resolution captured at construction would be a
16
+ * library are all LIVE keys a settings save (the `Subagent model` row), `/fast
17
+ * off` or a kit can rewrite mid-session. A resolution captured at construction would be a
18
18
  * resolution describing a session that no longer exists.
19
19
  *
20
20
  * A CHILD IS NEVER LEFT WITHOUT A MODEL (D-4). Every failure mode — fast tier
@@ -5,16 +5,16 @@
5
5
  * ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
6
6
  *
7
7
  * EVERY CONSUMER READS THE CHILD'S MODEL THROUGH THIS FUNCTION AND NOWHERE
8
- * ELSE — `createSubagent`, the `task` report header, the settings root row and
9
- * `/team model` — following the `resolveFastTier` precedent one directory over:
10
- * four call sites each recomputing "which model do children run on?" from raw
8
+ * ELSE — `createSubagent`, the `task` report header and the settings root row —
9
+ * following the `resolveFastTier` precedent one directory over: three call
10
+ * sites each recomputing "which model do children run on?" from raw
11
11
  * config keys is exactly how the report ends up naming a model the children
12
12
  * silently fell back from.
13
13
  *
14
14
  * IT IS RE-RUN ON EVERY DISPATCH AND EVERY RENDER, NOT ONCE (rule 8 / RV-3 of
15
15
  * the fast tier, inherited here): `team.model`, the fast tier and the profile
16
- * library are all LIVE keys a settings save, `/fast off` or `/team model` can
17
- * rewrite mid-session. A resolution captured at construction would be a
16
+ * library are all LIVE keys a settings save (the `Subagent model` row), `/fast
17
+ * off` or a kit can rewrite mid-session. A resolution captured at construction would be a
18
18
  * resolution describing a session that no longer exists.
19
19
  *
20
20
  * A CHILD IS NEVER LEFT WITHOUT A MODEL (D-4). Every failure mode — fast tier
package/dist/ui/App.js CHANGED
@@ -50,6 +50,11 @@ import { acceptanceChip, acceptanceRung, buildTodoRailLayout } from './layout/to
50
50
  import { buildTeamPanelLayout } from './layout/team-panel.js';
51
51
  import { isLiveTeamCard, lastTeamEntry, rosterCounts } from './team-status.js';
52
52
  import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
53
+ // THE THIRD LAYER OF FORCED TEAM MODE (team-command-forced-dispatch §3.7): the
54
+ // decision and its two strings live in a pure module, so `App` gains wiring and
55
+ // no policy. Imported beside the todo decision it shares one timer with.
56
+ import { decideForcedDispatch, FORCED_NUDGE_NOTICE } from '../team/dispatch-gate.js';
57
+ import { FORCED_NUDGE_GRACE_MS } from '../team/limits.js';
53
58
  import { useTerminalSize } from './layout/useTerminalSize.js';
54
59
  import { useHeightStore } from './use-height-store.js';
55
60
  import { useRenderGovernor } from './use-render-governor.js';
@@ -435,6 +440,32 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
435
440
  const updateToastPushed = useRef(false);
436
441
  const budgetRef = useRef(emptyBudget());
437
442
  const followTimer = useRef(null);
443
+ /**
444
+ * The forced-dispatch gate's three counters (team-command-forced-dispatch
445
+ * §3.7). REFS, for the reason the follow-through block above records: every
446
+ * field added to `ViewState` is a field `restoreEntries`, `/clear`, `/reset`
447
+ * and the session round-trip each have to have an opinion about, and none of
448
+ * them has an opinion about a counter that lives for one turn.
449
+ *
450
+ * `toolsUsed` and `dispatchAttempted` are cleared at `agent_start`; `nudges`
451
+ * is NOT - see its own comment.
452
+ */
453
+ const toolsUsedRef = useRef(0);
454
+ const dispatchAttemptedRef = useRef(false);
455
+ /**
456
+ * Fallback nudges spent. CLEARED BY A USER SUBMIT, NEVER BY `agent_start`
457
+ * (P0-2 / I-10).
458
+ *
459
+ * The nudge itself starts a NEW TURN, so a reset at `agent_start` would zero
460
+ * this counter at the top of every continuation and "at most one per turn"
461
+ * would become "one per turn, forever" - an unbounded loop of N-child
462
+ * dispatches that only ever fires while the model is being disobedient, which
463
+ * is precisely the state no manual test reproduces. The one reset lives in
464
+ * `submitMessage` under `opts.userInitiated !== false`, beside the
465
+ * `recordPrompt` call that answers the same question ("did a human ask for
466
+ * this?").
467
+ */
468
+ const nudgesRef = useRef(0);
438
469
  /** True while the run in flight was started by the grace timer, not the user. */
439
470
  const autoContinuationRef = useRef(false);
440
471
  /**
@@ -526,8 +557,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
526
557
  * Disarm an armed continuation. Idempotent; NEVER touches the budget — a
527
558
  * cancelled continuation was never issued, so it must not be charged (AC-18).
528
559
  *
529
- * Three callers and there must be no fourth: `Esc` in the idle branch, the
530
- * first line of `submitMessage`, and the subscription effect's cleanup.
560
+ * ONE SLOT: `Esc`, `submitMessage`, the reset/restore port, the effect cleanup
561
+ * and both follow-through arms (todo, then the gate) all cancel before arming.
531
562
  */
532
563
  const cancelFollowThrough = useCallback(() => {
533
564
  if (followTimer.current) {
@@ -639,6 +670,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
639
670
  if (event.type === 'agent_start') {
640
671
  interactionPhase.current = 'running';
641
672
  manualMetrics.current = createRunMetrics();
673
+ // THE TWO PER-TURN COUNTERS ONLY (I-10). `nudgesRef` is deliberately
674
+ // absent: a nudge continuation arrives here too, and clearing it would
675
+ // delete the bound that keeps a disobedient model from looping.
676
+ toolsUsedRef.current = 0;
677
+ dispatchAttemptedRef.current = false;
642
678
  }
643
679
  if (event.type === 'agent_end') {
644
680
  interactionPhase.current = 'idle';
@@ -654,6 +690,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
654
690
  if (action.type === 'notice' && action.level === 'error') {
655
691
  endReasonRef.current.errored = true;
656
692
  }
693
+ // THE GATE'S TWO FACTS, counted where the reducer's own actions already
694
+ // flow (no second subscription to keep in step). `toolExecStart` carries
695
+ // the tool NAME, so a `task` call that was REFUSED - team off, dispatch
696
+ // busy, no key - still counts as an attempt (P1-7): the upstream already
697
+ // said no, and urging a re-dispatch would reproduce that refusal one
698
+ // turn later.
699
+ if (action.type === 'toolExecStart') {
700
+ toolsUsedRef.current += 1;
701
+ if (action.toolName === 'task')
702
+ dispatchAttemptedRef.current = true;
703
+ }
657
704
  if (action.type === 'textDelta' || action.type === 'thinkingDelta') {
658
705
  pending.current.push(action);
659
706
  if (!flushTimer.current) {
@@ -688,13 +735,36 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
688
735
  const todos = controller.getTodoSnapshot();
689
736
  budgetRef.current = advanceBudget(budgetRef.current, todos, autoContinuationRef.current);
690
737
  autoContinuationRef.current = false;
691
- const decision = decideFollowThrough({
692
- mode: controller.getTodoConfig().followThrough,
693
- snapshot: todos,
738
+ // --- Forced-dispatch gate (team-command-forced-dispatch §3.7). --------
739
+ //
740
+ // THE THIRD LAYER, AND THE ONLY MECHANICAL ONE: if a whole turn went by
741
+ // with tools used and nothing dispatched, ask ONCE for a dispatch. It is
742
+ // computed BEFORE the todo decision and DOWN-GRADES that decision to
743
+ // `none` rather than returning early (P0-3 / I-9): the two statements below
744
+ // - the `endReasonRef` reset and `applyPendingMode()` - belong to EVERY
745
+ // turn, and a `return` here would silently defer a pending `plan -> build`
746
+ // by a whole turn and carry `errored` into the next one. It also keeps
747
+ // `cancelFollowThrough()`'s callers at three: the gate reuses the timer
748
+ // instead of adding a fourth canceller.
749
+ const forced = controller.getForcedTeam();
750
+ const gate = decideForcedDispatch({
751
+ armed: forced !== null,
752
+ count: forced?.count ?? 0,
753
+ dispatchAttempted: dispatchAttemptedRef.current,
754
+ toolsUsed: toolsUsedRef.current,
755
+ nudgesUsed: nudgesRef.current,
694
756
  runEnd: { ...endReasonRef.current },
695
- budget: budgetRef.current,
696
- interactive: true,
697
757
  });
758
+ const gated = gate.kind === 'nudge' && forced !== null;
759
+ const decision = gated
760
+ ? { kind: 'none' }
761
+ : decideFollowThrough({
762
+ mode: controller.getTodoConfig().followThrough,
763
+ snapshot: todos,
764
+ runEnd: { ...endReasonRef.current },
765
+ budget: budgetRef.current,
766
+ interactive: true,
767
+ });
698
768
  endReasonRef.current = { aborted: false, errored: false };
699
769
  if (decision.kind === 'notify') {
700
770
  dispatch({ type: 'notice', level: decision.level, text: decision.text });
@@ -739,6 +809,22 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
739
809
  submitRef.current(decision.message, { userInitiated: false, todoPolicy: 'continue' });
740
810
  }, decision.graceMs);
741
811
  }
812
+ // THE GATE'S OWN CONTINUATION: after the todo branch (which it has
813
+ // already silenced) and BEFORE `applyPendingMode()`, on the SAME timer and
814
+ // with the same re-checks. `level: 'warn'` and not `'error'` - the error
815
+ // level sets `endReasonRef.errored`, which would make the next turn's gate
816
+ // read "the run failed" and switch itself off for the rest of the session.
817
+ if (gated && gate.kind === 'nudge') {
818
+ nudgesRef.current += 1;
819
+ dispatch({ type: 'notice', level: 'warn', text: FORCED_NUDGE_NOTICE });
820
+ cancelFollowThrough(); // I-9 / I-3: at most one armed continuation, ever
821
+ followTimer.current = setTimeout(() => {
822
+ followTimer.current = null;
823
+ if (controller.isRunning() || controller.isCompactionBusy?.())
824
+ return;
825
+ submitRef.current(gate.message, { userInitiated: false, todoPolicy: 'continue' });
826
+ }, FORCED_NUDGE_GRACE_MS);
827
+ }
742
828
  // A deferred `plan -> build` becomes real here and nowhere else (§3.2).
743
829
  const applied = controller.applyPendingMode();
744
830
  if (applied) {
@@ -1956,8 +2042,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1956
2042
  // composer's hint fade. An auto-continuation demonstrates nothing about what
1957
2043
  // the user has learned, so suppressing the fade is correct rather than
1958
2044
  // incidental — which is why the parameter is named for the CAUSE (P2-8).
1959
- if (opts.userInitiated !== false)
2045
+ if (opts.userInitiated !== false) {
1960
2046
  recordPrompt(message);
2047
+ // THE ONE PLACE THE NUDGE BUDGET IS REFILLED (P0-2 / I-10). Same predicate
2048
+ // as `recordPrompt`, and for the same reason: the question is "did a human
2049
+ // ask for this?", and only a human may hand the gate another attempt. A
2050
+ // nudge continuation arrives here with `userInitiated: false`, so it cannot
2051
+ // refill its own budget.
2052
+ nudgesRef.current = 0;
2053
+ }
1961
2054
  // Mirrors where the reducer clears its own copies, in the same action
1962
2055
  // (C-11): a new turn's end reason must not inherit the previous one's.
1963
2056
  endReasonRef.current = { aborted: false, errored: false };
@@ -2966,7 +3059,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2966
3059
  // the save handler cannot disagree about what `(inherit)` means.
2967
3060
  ...fastSettingsFrom(cfg.fast),
2968
3061
  ...compactionSettingsFrom(cfg.compaction),
2969
- }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft, probe: probe, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
3062
+ }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft,
3063
+ // THE MISSING LINK (P1-5): without it the `Team` page cannot render
3064
+ // `on (next launch)` for a `--no-team` session, because the subpage is
3065
+ // mounted with `{...props}` and an undeclared prop arrives as
3066
+ // `undefined`. A construction-time snapshot, so reading it per frame is
3067
+ // honest (I-7).
3068
+ teamRegistered: controller.isTeamRegistered(), probe: probe, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
2970
3069
  // Read from `cfg` (render scope, re-read every frame), so `/retry off`
2971
3070
  // while the screen is open shows the new value on the next paint.
2972
3071
  retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'agents' ? (
@@ -3076,6 +3175,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
3076
3175
  railRows: native ? (nativeBudget?.band.railRows ?? 0) : (fullscreenRailVisible ? railRows : 0),
3077
3176
  width: native ? (nativeBudget?.band.railCols ?? 0) : railWidth,
3078
3177
  });
3178
+ // THE FORCED-MODE CHIP'S SOURCE (team-command-forced-dispatch §3.8 / D-9),
3179
+ // read from the controller on every frame: `armForcedTeam()` writes no
3180
+ // `ViewState`, so a mirror would have to be kept in step by hand - which is the
3181
+ // double-truth this design deletes elsewhere. It is handed to `StatusBar`, which
3182
+ // plans the primary row from its own props (a copy on `statusInput` above would
3183
+ // be a field nothing renders).
3184
+ const forcedTeam = controller.getForcedTeam();
3079
3185
  const statusInput = {
3080
3186
  columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
3081
3187
  context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
@@ -3144,7 +3250,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
3144
3250
  // partially stale. The window and its `known` flag now travel INSIDE
3145
3251
  // it, resolved by the meter - which is what makes the user's
3146
3252
  // `contextWindow` override reach the bar.
3147
- context: state.context, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
3253
+ context: state.context, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ...(forcedTeam ? { forcedTeam } : {}), ecoRung: governor.rung, ...(state.team
3148
3254
  ? {
3149
3255
  // `rosterCounts`, NOT a fourth inline phase filter (A-1): the chip and
3150
3256
  // the roster header above it must never disagree.
@@ -65,6 +65,20 @@ export interface StatusBarProps {
65
65
  } | null;
66
66
  agentMode?: AgentMode;
67
67
  pendingAgentMode?: AgentMode | null;
68
+ /**
69
+ * Forced team mode is armed, with the count it was armed with
70
+ * (team-command-forced-dispatch §3.8 / D-9). ABSENT = not armed, and every
71
+ * existing caller renders the row it always did.
72
+ *
73
+ * IT RIDES THIS COMPONENT'S PROPS AND NOT THE APP'S `statusInput`, by the rule
74
+ * the `compactionActive` chip records where `App` builds it: this component
75
+ * plans the primary row from its OWN props, so a chip left on `statusInput`
76
+ * would be a field nothing renders - which is exactly how the only persistently
77
+ * visible signal of `/team` (D-9) can ship without ever appearing on screen.
78
+ */
79
+ forcedTeam?: {
80
+ count: number;
81
+ };
68
82
  teamActive?: {
69
83
  running: number;
70
84
  total: number;
@@ -41,6 +41,7 @@ export function StatusBar(props) {
41
41
  compacting: props.compactionActive?.inFlight, retryActive: props.retryActive,
42
42
  compactionPaused: props.compactionActive?.selfDisabled === true,
43
43
  mode: props.agentMode, pendingMode: props.pendingAgentMode, servicesActive: props.servicesActive,
44
+ ...(props.forcedTeam ? { forcedTeam: props.forcedTeam } : {}),
44
45
  teamActive: props.teamActive, todoActive: props.todoActive,
45
46
  ...(props.acceptanceActive ? { acceptanceActive: props.acceptanceActive } : {}),
46
47
  ecoRung: props.ecoRung, fastActive: props.fastActive, scrolledLines: props.scrolledLines,
@@ -53,6 +53,23 @@ export interface StatusLayoutInput {
53
53
  servicesActive?: {
54
54
  live: number;
55
55
  };
56
+ /**
57
+ * Forced team mode is ARMED, with the fan-out count it was armed with
58
+ * (team-command-forced-dispatch §3.8 / D-9).
59
+ *
60
+ * THE ONLY PERSISTENTLY VISIBLE SIGNAL that `/team` happened. The directive is
61
+ * one turn, the prompt block is invisible, and the panel only appears while a
62
+ * dispatch runs - so without this chip the answer to "why is my agent
63
+ * dispatching?" is "remember what you typed". It is read at RENDER time from
64
+ * `controller.getForcedTeam()`, never mirrored into `ViewState`: a mirror is a
65
+ * second copy that can disagree, the exact defect §2.4 is about.
66
+ *
67
+ * ABSENT means "not armed" and keeps every existing caller's plan BYTE-
68
+ * IDENTICAL (the same discipline `reserveRight` records below).
69
+ */
70
+ forcedTeam?: {
71
+ count: number;
72
+ };
56
73
  teamActive?: {
57
74
  running: number;
58
75
  total: number;
@@ -220,10 +220,19 @@ export function planPrimaryStatusFields(input) {
220
220
  const label = (notification ? undefined : feedbackLabel(input, form))
221
221
  ?? (full ? labels[1] : rung >= 4 ? labels[0].slice(0, 4) : labels[0]);
222
222
  const modeText = modeFieldText(input.mode, input.pendingMode, full);
223
+ // IMMEDIATELY AFTER `mode`, and kept at EVERY rung: both describe a state
224
+ // the USER armed, so the row's emphasis budget goes on them and nothing
225
+ // else (`accent`). It shortens to `T4` instead of disappearing because a
226
+ // chip that vanishes on a narrow terminal is indistinguishable from a
227
+ // disarmed session - the one question it exists to answer.
228
+ const forcedText = input.forcedTeam
229
+ ? (rung >= 3 ? `T${input.forcedTeam.count}` : `Team ${input.forcedTeam.count}`)
230
+ : null;
223
231
  return statusLinePlan([
224
232
  statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, phaseTone),
225
233
  ...(notice ? [statusField('feedback', notice, feedbackTone)] : []),
226
234
  ...(modeText ? [statusField('mode', modeText, 'accent')] : []),
235
+ ...(forcedText ? [statusField('forcedTeam', forcedText, 'accent')] : []),
227
236
  statusField('context', (full ? 'Context ' : 'C') + pct
228
237
  + (input.compactionPaused === true ? pausedSuffix(full) : '')),
229
238
  statusField('thinking', full ? 'Think ' + (thinkingCodes[input.thinkingLevel] ? input.thinkingLevel : '?')
@@ -121,7 +121,7 @@ const COMMANDS = [
121
121
  ['/theme <name>', 'auto | warm | cool | light'],
122
122
  ['/thinking <level>', 'Set thinking level'],
123
123
  ['/max-tokens [n|auto]', 'Output token cap; no argument reports the effective one'],
124
- ['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
124
+ ['/team [on|off|<brief>]', 'Force team mode and start now: /team <brief> fans out'],
125
125
  ['/fast [on|off|model|review]', 'Fast model tier: status, switch, model, review cadence'],
126
126
  ['/kit [list|use <name>]', 'Agent kits: what is in effect, list them, use one for this session'],
127
127
  [
@@ -179,12 +179,16 @@ const PLAN_ROWS = [
179
179
  ];
180
180
  const TEAM_ROWS = [
181
181
  ['task', 'The tool the agent uses to run 2-5 subagents in parallel'],
182
- ['/team', 'Status: on/off, max subagents, and the live roster'],
183
- ['/team on | off', 'Switch team mode for this session (and save it)'],
184
- ['/team max <n>', 'Fan-out width, 1-10 (5 by default)'],
182
+ ['/team <brief>', 'Force it: arms forced mode and starts that work as a turn'],
183
+ ['/team on | off', 'Arm it without starting, or leave it; the capability is untouched'],
184
+ ['/team', 'Report the forced state, capacity, the model, and where to change it'],
185
+ ['-n <n> / --parts a,b', 'Fix the fan-out, or name the split (narrowing is disclosed)'],
186
+ ['--read-only', 'Every child refuses writes and shell, for research-only fan-outs'],
187
+ ['Team 4 (T4)', 'The status-bar chip - only while the session is armed'],
185
188
  ['--team / --no-team', 'Start a session with team mode on or off (also ARAGON_TEAM=0)'],
186
189
  ['team_send / team_wait', 'How subagents message each other; 6 each, one every 15s'],
187
190
  ['Ctrl+B / /agents', 'The board: every child, its full facts, its 24-event timeline'],
191
+ ['Settings > Team', 'Team mode, width (1-10, 8 by default), concurrency, supervisor'],
188
192
  ];
189
193
  const FAST_ROWS = [
190
194
  ['/fast', 'Status: on/off, the model, the cadence, and this session\'s totals'],
@@ -132,6 +132,20 @@ interface SettingsScreenProps {
132
132
  * screen. Absent = omit the row entirely.
133
133
  */
134
134
  retrySummary?: string;
135
+ /**
136
+ * Whether `task` is in this SESSION's tool list, for the `Team` page's
137
+ * `on (next launch)` disclosure (team-command-forced-dispatch §3.9 / P1-5).
138
+ *
139
+ * WITHOUT THIS LINK THE DISCLOSURE CANNOT RENDER. The page is mounted with
140
+ * `{...props}`, so a prop the screen does not declare reaches it as `undefined`
141
+ * and `--no-team` users are silently told their saved `on` took effect. The
142
+ * value is a CONSTRUCTION-TIME snapshot (`--no-team` cannot be undone), which is
143
+ * why reading it every frame does not conflict with I-7.
144
+ *
145
+ * OPTIONAL, like `probe` / `retrySummary` above: every fixture builds this by
146
+ * literal, and absent honestly means "unknown, so do not disclose".
147
+ */
148
+ teamRegistered?: boolean;
135
149
  }
136
150
  /**
137
151
  * The derived, non-editable line under the five fast rows — the sibling of
@@ -28,7 +28,7 @@ import { createModelProfileState, createCustomProfile, modelProfileReducer, prof
28
28
  import { KitEditor } from './KitEditor.js';
29
29
  import { KitManager } from './KitManager.js';
30
30
  import { TeamFanoutPage } from './TeamFanoutPage.js';
31
- import { fanoutRows, fanoutSummary, teamFanoutValues } from './team-fanout-state.js';
31
+ import { fanoutRows, teamFanoutValues, teamSummary } from './team-fanout-state.js';
32
32
  import { heldSlots, kitManagerRows, kitPackRow, kitReducer } from './kit-settings-state.js';
33
33
  /**
34
34
  * THE ROOT PAGE'S ROW INDEXES, AS NAMES (subagent-model V-5).
@@ -854,7 +854,10 @@ function renderProfileSubpage(props, view) {
854
854
  // because that branch is the unconditional fallback; the page name is unique,
855
855
  // so its position among the others is arbitrary.
856
856
  if (state.page === 'team')
857
- return _jsx(TeamFanoutPage, { ...props, view: { rows: fanoutRows(teamFanoutValues(state.draft)), error: state.error }, dispatch: dispatch, onBack: () => dispatch({ type: 'page', page: 'root' }) });
857
+ return _jsx(TeamFanoutPage, { ...props, view: {
858
+ rows: fanoutRows(teamFanoutValues(state.draft), props.teamRegistered),
859
+ error: state.error,
860
+ }, dispatch: dispatch, onBack: () => dispatch({ type: 'page', page: 'root' }) });
858
861
  // THE KIT PAGES (agent-model-kits §4.5). They are dispatched BEFORE the profile
859
862
  // pages because `'kit-slot'` RENDERS a profile picker while belonging to the
860
863
  // kit editor - the page name, not the component, decides where the user is.
@@ -964,14 +967,21 @@ function buildRootSettingsRows(props, view) {
964
967
  profiles: state.draft.profiles,
965
968
  hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 })}${kitTag}`
966
969
  : subagentLine;
967
- // THE FAN-OUT SUMMARY (subagent-fanout 3.4.4): a DERIVED row, like the three
968
- // above it, and deliberately not a control - the numbers are adjusted on the
969
- // page Enter opens, so the root row cannot change a value by accident.
970
- const fanout = fanoutSummary(teamFanoutValues(state.draft));
970
+ // THE TEAM SUMMARY (subagent-fanout 3.4.4, renamed by
971
+ // team-command-forced-dispatch §3.9): a DERIVED row, like the three above it,
972
+ // and deliberately not a control - the values are adjusted on the page Enter
973
+ // opens, so the root row cannot change one by accident. It now summarises the
974
+ // WHOLE section, switch included, because `Team mode` is one of those values
975
+ // and a summary that always printed numbers would describe a capability the
976
+ // user may have just switched off.
977
+ const summary = teamSummary(teamFanoutValues(state.draft));
971
978
  const lines = [`Main profile ${mainValue}`,
972
979
  `Fast profile ${fastValue}`,
973
980
  `Subagent model ${subagentValue}`,
974
- `Subagent fan-out ${fanout}`,
981
+ // `Team`, not `Subagent fan-out`: the row heads a page that now owns
982
+ // four values, only two of which are about fan-out width. `padEnd(18)` is
983
+ // unchanged, so the value column still lines up with `Subagent model`.
984
+ `Team ${summary}`,
975
985
  `Kit pack ${kitPackRow(state)}`,
976
986
  `Manage profiles ${library.entries.length} saved`,
977
987
  ...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Settings > Team fan-out: three arrows-adjustable rows (subagent-fanout 3.4.3).
2
+ * Settings > Team: four arrows-adjustable rows (subagent-fanout 3.4.3, extended
3
+ * by team-command-forced-dispatch §3.9).
3
4
  *
4
5
  * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
5
6
  *
@@ -9,6 +10,11 @@
9
10
  * root page about what is about to be saved (the discipline
10
11
  * `model-profile-state.ts` records at its own head).
11
12
  *
13
+ * FOUR ROWS, NOT THREE: `Team mode` moved here from `/team on|off`, because a
14
+ * command that both configures and executes leaves the user unable to predict
15
+ * what it did. The switch is the FIRST row - the other three describe a capability
16
+ * it turns on.
17
+ *
12
18
  * IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
13
19
  * `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
14
20
  * subpage has no save listener at all - the kit page included. A footer
@@ -30,6 +36,16 @@ interface TeamFanoutPageProps extends ProfileFrameProps {
30
36
  dispatch: (action: TeamFanoutAction) => void;
31
37
  /** Esc: back to the settings root. The draft is NOT discarded here. */
32
38
  onBack: () => void;
39
+ /**
40
+ * Whether `task` is in this SESSION's tool list (`--no-team` is the false case).
41
+ *
42
+ * OPTIONAL, like every other pass-through on this screen: fixtures build
43
+ * `ProfileFrameProps` by literal, and "absent" honestly means "not disclosed"
44
+ * rather than "registered". When it is `false` and the draft says `on`, the
45
+ * `Team mode` row reads `on (next launch)` - the tool array is immutable, so
46
+ * that is the truth about what saving will achieve (I-7 / AC-9).
47
+ */
48
+ teamRegistered?: boolean;
33
49
  }
34
50
  export declare function TeamFanoutPage(props: TeamFanoutPageProps): React.ReactElement;
35
51
  export {};
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
- * Settings > Team fan-out: three arrows-adjustable rows (subagent-fanout 3.4.3).
3
+ * Settings > Team: four arrows-adjustable rows (subagent-fanout 3.4.3, extended
4
+ * by team-command-forced-dispatch §3.9).
4
5
  *
5
6
  * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
6
7
  *
@@ -10,6 +11,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
10
11
  * root page about what is about to be saved (the discipline
11
12
  * `model-profile-state.ts` records at its own head).
12
13
  *
14
+ * FOUR ROWS, NOT THREE: `Team mode` moved here from `/team on|off`, because a
15
+ * command that both configures and executes leaves the user unable to predict
16
+ * what it did. The switch is the FIRST row - the other three describe a capability
17
+ * it turns on.
18
+ *
13
19
  * IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
14
20
  * `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
15
21
  * subpage has no save listener at all - the kit page included. A footer
@@ -24,10 +30,18 @@ import { OverlayFrame } from '../layout/OverlayFrame.js';
24
30
  import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
25
31
  /**
26
32
  * Label column, matching the root page's `padEnd(18)` rows above and below this
27
- * one. `Subagent fan-out` is 16 characters, so two spaces of padding are all it
28
- * needs to line up with `Subagent model` - measured, not guessed.
33
+ * one. `Team mode` is 9 characters and `Max subagents` is 13, so 18 keeps the
34
+ * value column aligned with `Subagent model` on the root page - measured, not
35
+ * guessed.
29
36
  */
30
37
  const LABEL_PAD = 18;
38
+ /**
39
+ * The two rows `Enter` toggles, and nothing else (R-P1-12).
40
+ *
41
+ * A stray Return that silently changed a NUMBER is indistinguishable from a bug,
42
+ * and the arrow keys are already the obvious control there.
43
+ */
44
+ const TOGGLE_FIELDS = new Set(['enabled', 'overseer']);
31
45
  export function TeamFanoutPage(props) {
32
46
  const [index, setIndex] = useState(0);
33
47
  const rows = props.view.rows;
@@ -59,20 +73,18 @@ export function TeamFanoutPage(props) {
59
73
  nudge(key.leftArrow ? -1 : 1);
60
74
  return;
61
75
  }
62
- // `Enter` toggles the Supervisor row and does NOTHING on a numeric row:
63
- // a stray Return that silently changed a number is indistinguishable from a
64
- // bug, and the arrow keys are already the obvious control.
65
- if (key.return && focused?.field === 'overseer')
76
+ // `Enter` toggles the two boolean rows and does NOTHING on a numeric row.
77
+ if (key.return && focused && TOGGLE_FIELDS.has(focused.field))
66
78
  nudge(1);
67
79
  });
68
80
  const lines = rows.map((row) => `${row.label.padEnd(LABEL_PAD)}${row.value}`);
69
81
  const glyphs = pickGlyphs(props.caps);
70
82
  const body = lines.map((line, at) => (_jsx(Text, { wrap: "truncate", color: at === cursor ? props.theme.accent : props.theme.muted, children: `${at === cursor ? glyphs.caret : ' '} ${clipProfileText(line, props.cols - 8)}` }, at)));
71
- // The hint explains the FOCUSED row rather than decorating the frame: three
72
- // numbers whose meaning is not obvious ("per dispatch" versus "at once") are
73
- // the reason this page exists at all.
83
+ // The hint explains the FOCUSED row rather than decorating the frame: numbers
84
+ // whose meaning is not obvious ("per dispatch" versus "at once") are the reason
85
+ // this page exists at all.
74
86
  if (focused) {
75
87
  body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText(focused.hint, props.cols - 8) }, "hint"));
76
88
  }
77
- return _jsx(OverlayFrame, { ...props, rows: body, scrollOffset: offset, title: props.view.error ?? 'Team fan-out', hint: "Arrows adjust. Esc goes back; Ctrl+S on the main page saves." });
89
+ return _jsx(OverlayFrame, { ...props, rows: body, scrollOffset: offset, title: props.view.error ?? 'Team', hint: "Arrows adjust. Esc goes back; Ctrl+S on the main page saves." });
78
90
  }
@@ -232,8 +232,9 @@ export function kitPackRow(state) {
232
232
  const partial = session.held.length < 3;
233
233
  const held = partial ? `: ${session.held.join('+')}` : '';
234
234
  // THE SAME `~` THE CHIP SHOWS (D-12): dropping a slot is only ONE way to
235
- // drift. The other is a connection that `/model`, `/fast` or `/team model`
236
- // rewrote after the kit was applied, and a row that stayed silent about that
235
+ // drift. The other is a connection that `/model`, `/fast` or the settings
236
+ // `Subagent model` row rewrote after the kit was applied, and a row that
237
+ // stayed silent about that
237
238
  // would disagree with the status bar about the same session.
238
239
  const drift = liveDrift(state, session) ? '~' : '';
239
240
  return `${sessionKit.name} [session${held}]${drift}${defaultClause}`;
@@ -146,7 +146,7 @@ export declare function draftTeam(draft: ModelSettingsDraft): {
146
146
  export declare function draftSubagentProfileId(draft: ModelSettingsDraft): string | null;
147
147
  /** The projected picker selection for the subagent role (6.2). */
148
148
  export declare function subagentSelectedId(draft: ModelSettingsDraft): string | null;
149
- /** What the settings root row and `/team model` show for the current mode. */
149
+ /** What the settings root row and `/team`'s status lines show for the current mode. */
150
150
  export interface SubagentModelSummaryInput {
151
151
  mode: SubagentModelMode;
152
152
  modelProfileId: string | null;