@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
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
|
-
*
|
|
530
|
-
*
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
:
|
|
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,
|
|
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.
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -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;
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -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
|
|
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', '
|
|
183
|
-
['/team on | off', '
|
|
184
|
-
['/team
|
|
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,
|
|
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: {
|
|
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
|
|
968
|
-
//
|
|
969
|
-
//
|
|
970
|
-
|
|
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
|
|
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
|
|
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
|
|
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. `
|
|
28
|
-
*
|
|
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
|
|
63
|
-
|
|
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:
|
|
72
|
-
//
|
|
73
|
-
//
|
|
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
|
|
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
|
|
236
|
-
// rewrote after the kit was applied, and a row 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
|
|
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;
|