@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.
Files changed (51) 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 +112 -33
  5. package/dist/agent/headless.js +6 -2
  6. package/dist/agent/system-prompt.d.ts +19 -0
  7. package/dist/agent/system-prompt.js +5 -0
  8. package/dist/commands/builtins.js +16 -182
  9. package/dist/commands/subagent-command.d.ts +21 -16
  10. package/dist/commands/subagent-command.js +22 -225
  11. package/dist/commands/team-command.d.ts +49 -0
  12. package/dist/commands/team-command.js +259 -0
  13. package/dist/config/model-kits.d.ts +2 -1
  14. package/dist/config/store.js +3 -2
  15. package/dist/exec/runner.js +2 -1
  16. package/dist/team/bus.d.ts +4 -0
  17. package/dist/team/bus.js +12 -2
  18. package/dist/team/comm-tools.js +3 -0
  19. package/dist/team/dispatch-gate.d.ts +86 -0
  20. package/dist/team/dispatch-gate.js +75 -0
  21. package/dist/team/forced-mode.d.ts +116 -0
  22. package/dist/team/forced-mode.js +156 -0
  23. package/dist/team/forced-prompt.d.ts +48 -0
  24. package/dist/team/forced-prompt.js +84 -0
  25. package/dist/team/limits.d.ts +41 -1
  26. package/dist/team/limits.js +49 -1
  27. package/dist/team/model.d.ts +22 -6
  28. package/dist/team/model.js +19 -5
  29. package/dist/team/overseer.d.ts +4 -0
  30. package/dist/team/overseer.js +38 -6
  31. package/dist/team/prompt.js +3 -3
  32. package/dist/team/report.d.ts +21 -0
  33. package/dist/team/report.js +40 -9
  34. package/dist/team/runtime.d.ts +3 -0
  35. package/dist/team/runtime.js +61 -5
  36. package/dist/team/subagent.js +25 -7
  37. package/dist/ui/App.js +140 -20
  38. package/dist/ui/StatusBar.d.ts +14 -0
  39. package/dist/ui/StatusBar.js +1 -0
  40. package/dist/ui/layout/status-layout.d.ts +17 -0
  41. package/dist/ui/layout/status-layout.js +9 -0
  42. package/dist/ui/overlays/HelpOverlay.js +8 -4
  43. package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
  44. package/dist/ui/overlays/SettingsScreen.js +17 -7
  45. package/dist/ui/overlays/TeamFanoutPage.d.ts +17 -1
  46. package/dist/ui/overlays/TeamFanoutPage.js +23 -11
  47. package/dist/ui/overlays/kit-settings-state.js +3 -2
  48. package/dist/ui/overlays/model-profile-state.d.ts +1 -1
  49. package/dist/ui/overlays/team-fanout-state.d.ts +46 -13
  50. package/dist/ui/overlays/team-fanout-state.js +78 -20
  51. package/package.json +81 -81
package/dist/ui/App.js CHANGED
@@ -50,6 +50,12 @@ 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';
58
+ import { priceTeamUsage } from '../team/model.js';
53
59
  import { useTerminalSize } from './layout/useTerminalSize.js';
54
60
  import { useHeightStore } from './use-height-store.js';
55
61
  import { useRenderGovernor } from './use-render-governor.js';
@@ -435,6 +441,32 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
435
441
  const updateToastPushed = useRef(false);
436
442
  const budgetRef = useRef(emptyBudget());
437
443
  const followTimer = useRef(null);
444
+ /**
445
+ * The forced-dispatch gate's three counters (team-command-forced-dispatch
446
+ * §3.7). REFS, for the reason the follow-through block above records: every
447
+ * field added to `ViewState` is a field `restoreEntries`, `/clear`, `/reset`
448
+ * and the session round-trip each have to have an opinion about, and none of
449
+ * them has an opinion about a counter that lives for one turn.
450
+ *
451
+ * `toolsUsed` and `dispatchAttempted` are cleared at `agent_start`; `nudges`
452
+ * is NOT - see its own comment.
453
+ */
454
+ const toolsUsedRef = useRef(0);
455
+ const dispatchAttemptedRef = useRef(false);
456
+ /**
457
+ * Fallback nudges spent. CLEARED BY A USER SUBMIT, NEVER BY `agent_start`
458
+ * (P0-2 / I-10).
459
+ *
460
+ * The nudge itself starts a NEW TURN, so a reset at `agent_start` would zero
461
+ * this counter at the top of every continuation and "at most one per turn"
462
+ * would become "one per turn, forever" - an unbounded loop of N-child
463
+ * dispatches that only ever fires while the model is being disobedient, which
464
+ * is precisely the state no manual test reproduces. The one reset lives in
465
+ * `submitMessage` under `opts.userInitiated !== false`, beside the
466
+ * `recordPrompt` call that answers the same question ("did a human ask for
467
+ * this?").
468
+ */
469
+ const nudgesRef = useRef(0);
438
470
  /** True while the run in flight was started by the grace timer, not the user. */
439
471
  const autoContinuationRef = useRef(false);
440
472
  /**
@@ -526,8 +558,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
526
558
  * Disarm an armed continuation. Idempotent; NEVER touches the budget — a
527
559
  * cancelled continuation was never issued, so it must not be charged (AC-18).
528
560
  *
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.
561
+ * ONE SLOT: `Esc`, `submitMessage`, the reset/restore port, the effect cleanup
562
+ * and both follow-through arms (todo, then the gate) all cancel before arming.
531
563
  */
532
564
  const cancelFollowThrough = useCallback(() => {
533
565
  if (followTimer.current) {
@@ -639,6 +671,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
639
671
  if (event.type === 'agent_start') {
640
672
  interactionPhase.current = 'running';
641
673
  manualMetrics.current = createRunMetrics();
674
+ // THE TWO PER-TURN COUNTERS ONLY (I-10). `nudgesRef` is deliberately
675
+ // absent: a nudge continuation arrives here too, and clearing it would
676
+ // delete the bound that keeps a disobedient model from looping.
677
+ toolsUsedRef.current = 0;
678
+ dispatchAttemptedRef.current = false;
642
679
  }
643
680
  if (event.type === 'agent_end') {
644
681
  interactionPhase.current = 'idle';
@@ -654,6 +691,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
654
691
  if (action.type === 'notice' && action.level === 'error') {
655
692
  endReasonRef.current.errored = true;
656
693
  }
694
+ // THE GATE'S TWO FACTS, counted where the reducer's own actions already
695
+ // flow (no second subscription to keep in step). `toolExecStart` carries
696
+ // the tool NAME, so a `task` call that was REFUSED - team off, dispatch
697
+ // busy, no key - still counts as an attempt (P1-7): the upstream already
698
+ // said no, and urging a re-dispatch would reproduce that refusal one
699
+ // turn later.
700
+ if (action.type === 'toolExecStart') {
701
+ toolsUsedRef.current += 1;
702
+ if (action.toolName === 'task')
703
+ dispatchAttemptedRef.current = true;
704
+ }
657
705
  if (action.type === 'textDelta' || action.type === 'thinkingDelta') {
658
706
  pending.current.push(action);
659
707
  if (!flushTimer.current) {
@@ -688,13 +736,36 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
688
736
  const todos = controller.getTodoSnapshot();
689
737
  budgetRef.current = advanceBudget(budgetRef.current, todos, autoContinuationRef.current);
690
738
  autoContinuationRef.current = false;
691
- const decision = decideFollowThrough({
692
- mode: controller.getTodoConfig().followThrough,
693
- snapshot: todos,
739
+ // --- Forced-dispatch gate (team-command-forced-dispatch §3.7). --------
740
+ //
741
+ // THE THIRD LAYER, AND THE ONLY MECHANICAL ONE: if a whole turn went by
742
+ // with tools used and nothing dispatched, ask ONCE for a dispatch. It is
743
+ // computed BEFORE the todo decision and DOWN-GRADES that decision to
744
+ // `none` rather than returning early (P0-3 / I-9): the two statements below
745
+ // - the `endReasonRef` reset and `applyPendingMode()` - belong to EVERY
746
+ // turn, and a `return` here would silently defer a pending `plan -> build`
747
+ // by a whole turn and carry `errored` into the next one. It also keeps
748
+ // `cancelFollowThrough()`'s callers at three: the gate reuses the timer
749
+ // instead of adding a fourth canceller.
750
+ const forced = controller.getForcedTeam();
751
+ const gate = decideForcedDispatch({
752
+ armed: forced !== null,
753
+ count: forced?.count ?? 0,
754
+ dispatchAttempted: dispatchAttemptedRef.current,
755
+ toolsUsed: toolsUsedRef.current,
756
+ nudgesUsed: nudgesRef.current,
694
757
  runEnd: { ...endReasonRef.current },
695
- budget: budgetRef.current,
696
- interactive: true,
697
758
  });
759
+ const gated = gate.kind === 'nudge' && forced !== null;
760
+ const decision = gated
761
+ ? { kind: 'none' }
762
+ : decideFollowThrough({
763
+ mode: controller.getTodoConfig().followThrough,
764
+ snapshot: todos,
765
+ runEnd: { ...endReasonRef.current },
766
+ budget: budgetRef.current,
767
+ interactive: true,
768
+ });
698
769
  endReasonRef.current = { aborted: false, errored: false };
699
770
  if (decision.kind === 'notify') {
700
771
  dispatch({ type: 'notice', level: decision.level, text: decision.text });
@@ -739,6 +810,22 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
739
810
  submitRef.current(decision.message, { userInitiated: false, todoPolicy: 'continue' });
740
811
  }, decision.graceMs);
741
812
  }
813
+ // THE GATE'S OWN CONTINUATION: after the todo branch (which it has
814
+ // already silenced) and BEFORE `applyPendingMode()`, on the SAME timer and
815
+ // with the same re-checks. `level: 'warn'` and not `'error'` - the error
816
+ // level sets `endReasonRef.errored`, which would make the next turn's gate
817
+ // read "the run failed" and switch itself off for the rest of the session.
818
+ if (gated && gate.kind === 'nudge') {
819
+ nudgesRef.current += 1;
820
+ dispatch({ type: 'notice', level: 'warn', text: FORCED_NUDGE_NOTICE });
821
+ cancelFollowThrough(); // I-9 / I-3: at most one armed continuation, ever
822
+ followTimer.current = setTimeout(() => {
823
+ followTimer.current = null;
824
+ if (controller.isRunning() || controller.isCompactionBusy?.())
825
+ return;
826
+ submitRef.current(gate.message, { userInitiated: false, todoPolicy: 'continue' });
827
+ }, FORCED_NUDGE_GRACE_MS);
828
+ }
742
829
  // A deferred `plan -> build` becomes real here and nowhere else (§3.2).
743
830
  const applied = controller.applyPendingMode();
744
831
  if (applied) {
@@ -843,21 +930,34 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
843
930
  // handler above records): this effect's dependency list never changes,
844
931
  // so it closes over the `cfg` object of the render it mounted in, and
845
932
  // `setModel` builds a NEW one.
846
- const live = controller.getConfig();
847
- // Every child runs the lead's own model (main-agent parity), so the
848
- // lead's live connection IS the tier that billed.
849
- const ref = { providerId: live.provider, modelId: live.model };
850
- // Unknown pricing contributes ZERO rather than a fabricated figure
851
- // (C-11 / RV-4); `/fast status` and the dispatch report say `unknown`.
852
- const costDelta = controller.isPricedModel(ref)
853
- ? computeCost(event.usage, controller.getModelInfoFor(ref).cost)
854
- : 0;
933
+ //
934
+ // PRICED AT THE MODEL THE CHILDREN RAN (`team.model`), through the same
935
+ // resolution the dispatch report prices with: under `fast` / `profile` the
936
+ // lead's table would over-report by up to an order of magnitude, and an
937
+ // unpriced child model must read as unknown (ZERO here), not as the lead's.
938
+ const costDelta = priceTeamUsage(controller.subagentModelResolution(), event.usage, {
939
+ mainCost: controller.getModelInfo().cost,
940
+ isPriced: (ref) => controller.isPricedModel(ref),
941
+ costFor: (ref) => controller.getModelInfoFor(ref).cost,
942
+ });
855
943
  dispatch({ type: 'teamUsage', usage: event.usage, costDelta });
856
944
  break;
857
945
  }
858
- case 'dispatch_end':
946
+ case 'dispatch_end': {
947
+ // The supervisor's own calls never emit a usage event (they go through the fast
948
+ // registry directly), so their spend reaches the readout HERE, once per
949
+ // dispatch, at the fast tier's price - the same channel review spend uses.
950
+ const spend = event.outcome.overseerUsage;
951
+ if (spend !== undefined && (spend.inputTokens > 0 || spend.outputTokens > 0)) {
952
+ const tier = controller.getFastStatus().tier;
953
+ const costDelta = tier.ok && controller.isPricedModel(tier.ref)
954
+ ? computeCost(spend, controller.getModelInfoFor(tier.ref).cost)
955
+ : 0;
956
+ dispatch({ type: 'fastUsage', usage: spend, costDelta });
957
+ }
859
958
  dispatch({ type: 'teamEnd', outcome: event.outcome });
860
959
  break;
960
+ }
861
961
  }
862
962
  });
863
963
  return () => {
@@ -1956,8 +2056,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1956
2056
  // composer's hint fade. An auto-continuation demonstrates nothing about what
1957
2057
  // the user has learned, so suppressing the fade is correct rather than
1958
2058
  // incidental — which is why the parameter is named for the CAUSE (P2-8).
1959
- if (opts.userInitiated !== false)
2059
+ if (opts.userInitiated !== false) {
1960
2060
  recordPrompt(message);
2061
+ // THE ONE PLACE THE NUDGE BUDGET IS REFILLED (P0-2 / I-10). Same predicate
2062
+ // as `recordPrompt`, and for the same reason: the question is "did a human
2063
+ // ask for this?", and only a human may hand the gate another attempt. A
2064
+ // nudge continuation arrives here with `userInitiated: false`, so it cannot
2065
+ // refill its own budget.
2066
+ nudgesRef.current = 0;
2067
+ }
1961
2068
  // Mirrors where the reducer clears its own copies, in the same action
1962
2069
  // (C-11): a new turn's end reason must not inherit the previous one's.
1963
2070
  endReasonRef.current = { aborted: false, errored: false };
@@ -2966,7 +3073,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2966
3073
  // the save handler cannot disagree about what `(inherit)` means.
2967
3074
  ...fastSettingsFrom(cfg.fast),
2968
3075
  ...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 }),
3076
+ }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft,
3077
+ // THE MISSING LINK (P1-5): without it the `Team` page cannot render
3078
+ // `on (next launch)` for a `--no-team` session, because the subpage is
3079
+ // mounted with `{...props}` and an undeclared prop arrives as
3080
+ // `undefined`. A construction-time snapshot, so reading it per frame is
3081
+ // honest (I-7).
3082
+ teamRegistered: controller.isTeamRegistered(), probe: probe, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
2970
3083
  // Read from `cfg` (render scope, re-read every frame), so `/retry off`
2971
3084
  // while the screen is open shows the new value on the next paint.
2972
3085
  retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'agents' ? (
@@ -3076,6 +3189,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
3076
3189
  railRows: native ? (nativeBudget?.band.railRows ?? 0) : (fullscreenRailVisible ? railRows : 0),
3077
3190
  width: native ? (nativeBudget?.band.railCols ?? 0) : railWidth,
3078
3191
  });
3192
+ // THE FORCED-MODE CHIP'S SOURCE (team-command-forced-dispatch §3.8 / D-9),
3193
+ // read from the controller on every frame: `armForcedTeam()` writes no
3194
+ // `ViewState`, so a mirror would have to be kept in step by hand - which is the
3195
+ // double-truth this design deletes elsewhere. It is handed to `StatusBar`, which
3196
+ // plans the primary row from its own props (a copy on `statusInput` above would
3197
+ // be a field nothing renders).
3198
+ const forcedTeam = controller.getForcedTeam();
3079
3199
  const statusInput = {
3080
3200
  columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
3081
3201
  context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
@@ -3144,7 +3264,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
3144
3264
  // partially stale. The window and its `known` flag now travel INSIDE
3145
3265
  // it, resolved by the meter - which is what makes the user's
3146
3266
  // `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
3267
+ 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
3268
  ? {
3149
3269
  // `rosterCounts`, NOT a fourth inline phase filter (A-1): the chip and
3150
3270
  // 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;