@pellux/goodvibes-tui 1.16.1 → 1.17.0

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 (219) hide show
  1. package/CHANGELOG.md +89 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1091 -271
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/parser.ts +5 -0
  12. package/src/cli/status.ts +13 -0
  13. package/src/cli/surface-command.ts +5 -5
  14. package/src/cli/tui-startup.ts +60 -97
  15. package/src/cli/types.ts +2 -0
  16. package/src/config/index.ts +1 -1
  17. package/src/config/tui-extension-settings.ts +33 -0
  18. package/src/core/composer-state.ts +1 -1
  19. package/src/core/conversation-line-cache.ts +3 -3
  20. package/src/core/conversation-rendering.ts +4 -4
  21. package/src/core/conversation.ts +2 -2
  22. package/src/core/long-task-notifier.ts +7 -7
  23. package/src/core/stream-event-wiring.ts +8 -6
  24. package/src/core/system-message-noise.ts +1 -1
  25. package/src/core/system-message-router.ts +6 -6
  26. package/src/core/turn-cancellation.ts +1 -1
  27. package/src/core/turn-event-wiring.ts +8 -8
  28. package/src/daemon/cli.ts +20 -5
  29. package/src/daemon/lifecycle.ts +41 -0
  30. package/src/daemon/service-commands.ts +5 -5
  31. package/src/export/cost-utils.ts +128 -6
  32. package/src/input/autocomplete.ts +2 -2
  33. package/src/input/command-registry.ts +34 -9
  34. package/src/input/commands/ci-runtime.ts +5 -1
  35. package/src/input/commands/codebase-runtime.ts +6 -6
  36. package/src/input/commands/cost-runtime.ts +1 -1
  37. package/src/input/commands/health-runtime.ts +2 -2
  38. package/src/input/commands/intelligence-runtime.ts +1 -1
  39. package/src/input/commands/local-auth-runtime.ts +1 -1
  40. package/src/input/commands/local-provider-runtime.ts +49 -10
  41. package/src/input/commands/local-runtime.ts +53 -5
  42. package/src/input/commands/memory.ts +2 -2
  43. package/src/input/commands/operator-panel-runtime.ts +5 -5
  44. package/src/input/commands/permissions-provenance.ts +1 -1
  45. package/src/input/commands/permissions-runtime.ts +99 -8
  46. package/src/input/commands/planning-runtime.ts +3 -3
  47. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  48. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  49. package/src/input/commands/provider.ts +7 -5
  50. package/src/input/commands/recall-files-git.ts +13 -51
  51. package/src/input/commands/recall-files.ts +8 -11
  52. package/src/input/commands/recall-review.ts +2 -2
  53. package/src/input/commands/relay-runtime.ts +1 -1
  54. package/src/input/commands/remote-runtime.ts +1 -1
  55. package/src/input/commands/runtime-services.ts +4 -4
  56. package/src/input/commands/schedule-runtime.ts +14 -7
  57. package/src/input/commands/services-runtime.ts +1 -1
  58. package/src/input/commands/session-workflow.ts +2 -2
  59. package/src/input/commands/settings-sync-runtime.ts +2 -2
  60. package/src/input/commands/share-runtime.ts +1 -1
  61. package/src/input/commands/shell-core.ts +46 -68
  62. package/src/input/commands/subscription-runtime.ts +1 -1
  63. package/src/input/commands/update-runtime.ts +153 -96
  64. package/src/input/commands/work-plan-runtime.ts +1 -1
  65. package/src/input/commands/workstream-runtime.ts +2 -2
  66. package/src/input/commands.ts +0 -2
  67. package/src/input/config-modal-types.ts +3 -3
  68. package/src/input/config-modal.ts +10 -11
  69. package/src/input/feature-unit-layout.ts +93 -82
  70. package/src/input/feed-context-factory.ts +2 -2
  71. package/src/input/handler-command-route.ts +2 -2
  72. package/src/input/handler-content-actions.ts +1 -1
  73. package/src/input/handler-feed-routes.ts +7 -7
  74. package/src/input/handler-feed.ts +4 -4
  75. package/src/input/handler-interactions.ts +3 -2
  76. package/src/input/handler-modal-routes.ts +12 -4
  77. package/src/input/handler-modal-token-routes.ts +1 -1
  78. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  79. package/src/input/handler-onboarding.ts +3 -2
  80. package/src/input/handler-picker-routes.ts +26 -13
  81. package/src/input/handler-shortcuts.ts +6 -6
  82. package/src/input/handler.ts +2 -2
  83. package/src/input/keybindings.ts +2 -2
  84. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  85. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  86. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  87. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  88. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  89. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  90. package/src/input/panel-integration-actions.ts +2 -2
  91. package/src/input/panel-mouse-geometry.ts +1 -1
  92. package/src/input/panel-paste-flood-guard.ts +1 -1
  93. package/src/input/provider-key-intake.ts +167 -0
  94. package/src/input/sandbox-exec-config.ts +2 -2
  95. package/src/input/selection-modal.ts +1 -1
  96. package/src/input/settings-modal-activation.ts +5 -0
  97. package/src/input/settings-modal-adjustment.ts +4 -31
  98. package/src/input/settings-modal-data.ts +22 -21
  99. package/src/input/settings-modal-mutations.ts +12 -68
  100. package/src/input/settings-modal-types.ts +23 -16
  101. package/src/input/settings-modal.ts +74 -55
  102. package/src/main.ts +48 -48
  103. package/src/panels/agent-inspector-shared.ts +4 -4
  104. package/src/panels/base-panel.ts +2 -2
  105. package/src/panels/builtin/agent.ts +4 -4
  106. package/src/panels/builtin/development.ts +4 -4
  107. package/src/panels/builtin/knowledge.ts +1 -1
  108. package/src/panels/builtin/operations.ts +29 -10
  109. package/src/panels/builtin/session.ts +5 -5
  110. package/src/panels/builtin/shared.ts +4 -4
  111. package/src/panels/builtin-modals.ts +10 -10
  112. package/src/panels/builtin-panels.ts +1 -1
  113. package/src/panels/cost-tracker-panel.ts +124 -7
  114. package/src/panels/diff-panel.ts +5 -5
  115. package/src/panels/eval-registry.ts +2 -2
  116. package/src/panels/fleet-deep-link.ts +1 -1
  117. package/src/panels/fleet-panel-format.ts +44 -8
  118. package/src/panels/fleet-panel.ts +16 -16
  119. package/src/panels/fleet-read-model.ts +52 -13
  120. package/src/panels/fleet-steer.ts +2 -2
  121. package/src/panels/fleet-stop.ts +3 -3
  122. package/src/panels/fleet-tabs.ts +13 -14
  123. package/src/panels/fleet-transcript.ts +5 -5
  124. package/src/panels/git-panel.ts +2 -2
  125. package/src/panels/index.ts +4 -1
  126. package/src/panels/local-auth-panel.ts +1 -1
  127. package/src/panels/modals/hooks-modal.ts +1 -1
  128. package/src/panels/modals/keybindings-modal.ts +2 -2
  129. package/src/panels/modals/knowledge-modal.ts +1 -1
  130. package/src/panels/modals/marketplace-modal.ts +4 -4
  131. package/src/panels/modals/memory-modal.ts +4 -4
  132. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  133. package/src/panels/modals/modal-theme.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +2 -2
  135. package/src/panels/modals/planning-modal.ts +2 -2
  136. package/src/panels/modals/plugins-modal.ts +1 -1
  137. package/src/panels/modals/policy-modal.ts +1 -1
  138. package/src/panels/modals/security-modal.ts +1 -1
  139. package/src/panels/modals/skills-modal.ts +1 -1
  140. package/src/panels/modals/work-plan-modal.ts +3 -3
  141. package/src/panels/notifications-feed.ts +127 -0
  142. package/src/panels/notifications-panel.ts +160 -0
  143. package/src/panels/panel-manager.ts +9 -9
  144. package/src/panels/plugins-panel.ts +4 -4
  145. package/src/panels/project-planning-answer-actions.ts +1 -1
  146. package/src/panels/provider-health-routes.ts +2 -2
  147. package/src/panels/skills-panel.ts +4 -5
  148. package/src/panels/token-budget-panel.ts +11 -3
  149. package/src/panels/types.ts +5 -5
  150. package/src/permissions/broker-approval-card.ts +201 -0
  151. package/src/permissions/hunk-selection.ts +28 -4
  152. package/src/permissions/prompt.ts +244 -29
  153. package/src/renderer/autocomplete-overlay.ts +107 -54
  154. package/src/renderer/code-block.ts +2 -2
  155. package/src/renderer/compaction-history-modal.ts +1 -1
  156. package/src/renderer/compaction-preview.ts +1 -1
  157. package/src/renderer/compaction-quality.ts +1 -1
  158. package/src/renderer/compositor.ts +2 -2
  159. package/src/renderer/config-modal.ts +1 -1
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/footer-tips.ts +3 -3
  162. package/src/renderer/hint-grammar.ts +1 -1
  163. package/src/renderer/modal-factory.ts +1 -1
  164. package/src/renderer/model-picker-overlay.ts +1 -1
  165. package/src/renderer/model-workspace.ts +7 -2
  166. package/src/renderer/panel-composite.ts +2 -2
  167. package/src/renderer/panel-workspace-bar.ts +1 -1
  168. package/src/renderer/process-indicator.ts +1 -1
  169. package/src/renderer/selection-modal-overlay.ts +1 -1
  170. package/src/renderer/session-picker-modal.ts +2 -2
  171. package/src/renderer/settings-modal-helpers.ts +14 -5
  172. package/src/renderer/settings-modal.ts +127 -39
  173. package/src/renderer/shell-surface.ts +3 -0
  174. package/src/renderer/terminal-bg-probe.ts +1 -1
  175. package/src/renderer/theme.ts +3 -3
  176. package/src/renderer/tool-call.ts +1 -1
  177. package/src/renderer/tool-result-summary.ts +1 -1
  178. package/src/renderer/turn-injection.ts +7 -6
  179. package/src/renderer/ui-factory.ts +6 -1
  180. package/src/runtime/bootstrap-command-context.ts +4 -1
  181. package/src/runtime/bootstrap-command-parts.ts +6 -4
  182. package/src/runtime/bootstrap-core.ts +35 -35
  183. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  184. package/src/runtime/bootstrap-shell.ts +6 -5
  185. package/src/runtime/bootstrap.ts +34 -32
  186. package/src/runtime/channel-composition.ts +46 -0
  187. package/src/runtime/code-index-services.ts +3 -4
  188. package/src/runtime/daemon-attach-notices.ts +120 -0
  189. package/src/runtime/durability-services.ts +55 -0
  190. package/src/runtime/embedded-service-factories.ts +40 -0
  191. package/src/runtime/feature-settings.ts +147 -0
  192. package/src/runtime/index.ts +4 -21
  193. package/src/runtime/legacy-daemon-migration.ts +29 -19
  194. package/src/runtime/memory-fold.ts +1 -1
  195. package/src/runtime/notification-dispatch.ts +110 -0
  196. package/src/runtime/onboarding/apply.ts +3 -29
  197. package/src/runtime/onboarding/fast-path.ts +39 -6
  198. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  199. package/src/runtime/onboarding/snapshot.ts +0 -1
  200. package/src/runtime/onboarding/types.ts +9 -3
  201. package/src/runtime/provider-fallback.ts +55 -0
  202. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  203. package/src/runtime/release-artifacts.ts +19 -103
  204. package/src/runtime/resume-notice.ts +1 -1
  205. package/src/runtime/services.ts +81 -82
  206. package/src/runtime/session-continuity-hints.ts +38 -0
  207. package/src/runtime/surface-feature-flags.ts +40 -9
  208. package/src/runtime/terminal-output-guard.ts +2 -2
  209. package/src/runtime/trust/workspace-registration.ts +10 -2
  210. package/src/runtime/trust/workspace-trust.ts +58 -31
  211. package/src/runtime/ui-services.ts +3 -1
  212. package/src/runtime/update-check.ts +15 -69
  213. package/src/runtime/workstream-services.ts +2 -2
  214. package/src/shell/blocking-input.ts +102 -6
  215. package/src/shell/ui-openers.ts +38 -6
  216. package/src/utils/format-duration.ts +1 -1
  217. package/src/verification/verification-ledger.ts +21 -4
  218. package/src/version.ts +1 -1
  219. package/src/input/commands/flags-runtime.ts +0 -328
@@ -1,328 +0,0 @@
1
- /**
2
- * `/flags` — surface every feature flag grouped by state, with runtime toggling
3
- * for the flags that support it and a doctor section that names the "dark"
4
- * subsystems (built but disabled) alongside the command to switch each on.
5
- *
6
- * The flag registry and runtime state come from the FeatureFlagManager
7
- * (ctx.platform.featureFlagManager); persistence of an override goes through
8
- * the config `featureFlags` category, reusing persistFlagState so the on-disk
9
- * behavior matches the settings modal exactly.
10
- */
11
-
12
- import type { CommandRegistry, CommandContext } from '../command-registry.ts';
13
- import type { FeatureFlag, FlagState } from '@/runtime/index.ts';
14
- import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
15
- import { FEATURE_FLAG_CONFIG } from '@pellux/goodvibes-sdk/platform/runtime/state';
16
- import { persistFlagState } from '../settings-modal-mutations.ts';
17
- import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
18
- import { SETTINGS_CATEGORY_GROUPS, type SettingsCategory } from '../settings-modal-types.ts';
19
- import { getFeatureUnitHostCategory } from '../feature-unit-layout.ts';
20
-
21
- /** The flags.graduation.report response, as typed by the installed operator contract. */
22
- type GraduationReport = OperatorMethodOutput<'flags.graduation.report'>;
23
- type GraduationEntry = GraduationReport['entries'][number];
24
-
25
- /** A flag paired with its live runtime state and any pending-restart marker. */
26
- export interface FlagSnapshotEntry {
27
- readonly flag: FeatureFlag;
28
- /** Live effective state the runtime currently sees. */
29
- readonly state: FlagState;
30
- /** Persisted config-layer value; differs from `state` only for a startup-gated flag awaiting restart. */
31
- readonly persistedState: FlagState;
32
- /** True when a startup-gated flag's saved value needs a restart to take effect. */
33
- readonly pendingRestart: boolean;
34
- }
35
-
36
- const ENABLED_MARK = '●';
37
- const DISABLED_MARK = '○';
38
- const KILLED_MARK = '✕';
39
-
40
- function toggleability(flag: FeatureFlag): string {
41
- return flag.runtimeToggleable ? 'runtime-toggleable' : 'startup-only';
42
- }
43
-
44
- function stateMark(state: FlagState): string {
45
- return state === 'enabled' ? ENABLED_MARK : state === 'killed' ? KILLED_MARK : DISABLED_MARK;
46
- }
47
-
48
- /** The config keys a flag tunes, from FEATURE_FLAG_CONFIG (empty for no-config flags). */
49
- function flagConfigKeys(flagId: string): readonly string[] {
50
- return FEATURE_FLAG_CONFIG[flagId]?.configKeys ?? [];
51
- }
52
-
53
- /** Format one flag as a feature unit: state + name + toggleability, then a config-key summary. */
54
- function formatUnit(entry: FlagSnapshotEntry): string {
55
- const { flag } = entry;
56
- const lines = [` ${stateMark(entry.state)} ${flag.id} — ${flag.name} [${toggleability(flag)}]`];
57
- if (entry.pendingRestart) {
58
- lines.push(` restart pending: saved ${entry.persistedState}; still ${entry.state} until next launch`);
59
- }
60
- const keys = flagConfigKeys(flag.id);
61
- if (keys.length > 0) {
62
- const shown = keys.slice(0, 4).join(', ');
63
- const more = keys.length > 4 ? `, +${keys.length - 4} more` : '';
64
- lines.push(` tunes: ${shown}${more} (edit in /settings)`);
65
- } else {
66
- lines.push(' no config keys — toggle only');
67
- }
68
- if (entry.state === 'killed' && flag.killReason) {
69
- lines.push(` killed: ${flag.killReason}`);
70
- }
71
- return lines.join('\n');
72
- }
73
-
74
- /** Top-level settings group label that hosts a flag's feature unit (mirrors the settings modal grouping). */
75
- function groupLabelForFlag(flagId: string): string {
76
- const host = getFeatureUnitHostCategory(flagId);
77
- for (const group of SETTINGS_CATEGORY_GROUPS) {
78
- if (group.categories.includes(host as SettingsCategory)) return group.label;
79
- }
80
- return 'Advanced';
81
- }
82
-
83
- /**
84
- * Build the `/flags` overview: feature units grouped by their topical settings
85
- * group — each flag with the config keys it tunes — matching how the settings
86
- * modal now presents them. This replaces the old flat Enabled/Disabled/Killed
87
- * dump; the doctor and graduation views keep their diagnostic form. Pure —
88
- * exported for testing.
89
- */
90
- export function formatFlagsOverview(entries: readonly FlagSnapshotEntry[]): string {
91
- if (entries.length === 0) return 'No feature flags are registered in this runtime.';
92
-
93
- const enabled = entries.filter((e) => e.state === 'enabled').length;
94
- const dark = entries.filter((e) => e.state === 'disabled').length;
95
- const killed = entries.filter((e) => e.state === 'killed').length;
96
-
97
- const out: string[] = [
98
- `Feature flags (${entries.length} total) — ${enabled} enabled · ${dark} dark · ${killed} killed`,
99
- 'Each flag is one feature unit: its toggle plus the config keys it tunes. Full editing lives in /settings.',
100
- '',
101
- ];
102
-
103
- // Bucket the (already tier-then-id sorted) entries by their topical group,
104
- // preserving SETTINGS_CATEGORY_GROUPS order.
105
- const byGroup = new Map<string, FlagSnapshotEntry[]>();
106
- for (const entry of entries) {
107
- const label = groupLabelForFlag(entry.flag.id);
108
- if (!byGroup.has(label)) byGroup.set(label, []);
109
- byGroup.get(label)!.push(entry);
110
- }
111
- const orderedLabels = [
112
- ...SETTINGS_CATEGORY_GROUPS.map((g) => g.label).filter((label) => byGroup.has(label)),
113
- ...[...byGroup.keys()].filter((label) => !SETTINGS_CATEGORY_GROUPS.some((g) => g.label === label)),
114
- ];
115
-
116
- for (const label of orderedLabels) {
117
- out.push(`${label.toUpperCase()}:`);
118
- for (const entry of byGroup.get(label)!) out.push(formatUnit(entry));
119
- out.push('');
120
- }
121
-
122
- out.push('Toggle runtime-toggleable flags: /flags on <id> /flags off <id>');
123
- out.push('Startup-only flags apply on next launch. Interactive toggling + full config: /settings.');
124
- out.push('See /flags doctor for dark subsystems you can switch on; /flags graduation for release readiness.');
125
- return out.join('\n');
126
- }
127
-
128
- /** Build the `/flags doctor` text: disabled (built, dark) flags + how to enable each. Pure. */
129
- export function formatFlagsDoctor(entries: readonly FlagSnapshotEntry[]): string {
130
- const dark = entries.filter((e) => e.state === 'disabled');
131
- if (dark.length === 0) {
132
- return 'Feature flag doctor: no dark subsystems — every built flag is enabled or killed.';
133
- }
134
- const out: string[] = [
135
- `Feature flag doctor — ${dark.length} dark subsystem${dark.length === 1 ? '' : 's'} (built but disabled):`,
136
- '',
137
- ];
138
- for (const entry of dark) {
139
- const { flag } = entry;
140
- out.push(` ${DISABLED_MARK} ${flag.id} — ${flag.name}`);
141
- out.push(` ${flag.description}`);
142
- if (flag.runtimeToggleable) {
143
- out.push(` enable now: /flags on ${flag.id}`);
144
- } else {
145
- out.push(` enable (next launch, startup-only): /flags on ${flag.id}`);
146
- }
147
- out.push('');
148
- }
149
- return out.join('\n').trimEnd();
150
- }
151
-
152
- /** Snapshot the manager's flags into a stable, tier-then-id-sorted list. */
153
- function snapshotEntries(ctx: CommandContext): FlagSnapshotEntry[] {
154
- const manager = ctx.platform.featureFlagManager;
155
- if (!manager) return [];
156
- return Array.from(manager.getAll().values())
157
- .map(({ flag, state, persistedState, pendingRestart }) => ({ flag, state, persistedState, pendingRestart }))
158
- .sort((a, b) => (a.flag.tier - b.flag.tier) || a.flag.id.localeCompare(b.flag.id));
159
- }
160
-
161
- function applyToggle(ctx: CommandContext, id: string, target: 'enabled' | 'disabled'): void {
162
- const manager = ctx.platform.featureFlagManager;
163
- if (!manager) {
164
- ctx.print('Feature flags are not available in this runtime.');
165
- return;
166
- }
167
- const entry = snapshotEntries(ctx).find((e) => e.flag.id === id);
168
- if (!entry) {
169
- ctx.print(`Unknown feature flag: ${id}. Run /flags to list them.`);
170
- return;
171
- }
172
- if (entry.state === target) {
173
- ctx.print(`Flag ${id} is already ${target}.`);
174
- return;
175
- }
176
- const { flag } = entry;
177
-
178
- // Persist the override, then apply the same value to the live manager through
179
- // applyConfigState — the same consistent path the settings modal uses. It
180
- // flips runtime-toggleable flags immediately and records a pending-restart
181
- // marker (never a fake live flip) for startup-gated ones.
182
- try {
183
- persistFlagState(ctx.platform.configManager, flag.id, target, flag.defaultState);
184
- manager.applyConfigState(flag.id, target);
185
- } catch (err) {
186
- ctx.print(`Could not ${target === 'enabled' ? 'enable' : 'disable'} ${id}: ${err instanceof Error ? err.message : String(err)}`);
187
- return;
188
- }
189
-
190
- if (manager.hasPendingRestart(flag.id)) {
191
- ctx.print(`Flag ${id} is startup-only. Saved ${target} — it applies on next launch (currently ${manager.getState(flag.id)}).`);
192
- return;
193
- }
194
- ctx.print(`Flag ${id} is now ${manager.getState(flag.id)} (runtime + persisted).`);
195
- ctx.renderRequest();
196
- }
197
-
198
- // ---------------------------------------------------------------------------
199
- // /flags graduation — render flags.graduation.report (the release-readiness lane
200
- // for each flag). Candidates awaiting a decision are the only release-blocking
201
- // state, so they sort first; every flag shows its evidence, with "no evidence
202
- // collected" stated plainly rather than implied.
203
- // ---------------------------------------------------------------------------
204
-
205
- const GRADUATION_MARK: Record<GraduationEntry['state'], string> = {
206
- 'graduate-candidate': '◆',
207
- blocked: '✕',
208
- soaking: '◐',
209
- dark: '○',
210
- graduated: '●',
211
- };
212
-
213
- /** Sort rank: candidates (need a decision) first, then blocked, soaking, dark, graduated. */
214
- const GRADUATION_RANK: Record<GraduationEntry['state'], number> = {
215
- 'graduate-candidate': 0,
216
- blocked: 1,
217
- soaking: 2,
218
- dark: 3,
219
- graduated: 4,
220
- };
221
-
222
- /** One evidence line — real divergence readings, or an explicit absence of them. */
223
- function formatEvidence(entry: GraduationEntry): string {
224
- const { evidence } = entry;
225
- const parts = [evidence.note];
226
- if (evidence.divergence) {
227
- const d = evidence.divergence;
228
- parts.push(`divergence ${(d.divergenceRate * 100).toFixed(2)}% over ${d.totalEvaluations} evals, gate ${d.gateStatus}`);
229
- }
230
- return `${parts.join(' · ')} (instrumentation: ${evidence.instrumentation})`;
231
- }
232
-
233
- function formatGraduationEntry(entry: GraduationEntry): string {
234
- const toggle = entry.runtimeToggleable ? 'runtime-toggleable' : 'startup-only';
235
- const lines = [
236
- ` ${GRADUATION_MARK[entry.state]} ${entry.flagId} — ${entry.name} [tier ${entry.tier}, default ${entry.currentDefault}, ${toggle}]`,
237
- ` state: ${entry.state}`,
238
- ` evidence: ${formatEvidence(entry)}`,
239
- ];
240
- if (entry.blocker) lines.push(` blocked ${entry.blocker.date}: ${entry.blocker.reason}`);
241
- if (entry.note) lines.push(` note: ${entry.note}`);
242
- return lines.join('\n');
243
- }
244
-
245
- /** Build the `/flags graduation` overview. Pure — exported for testing. */
246
- export function formatGraduationReport(report: GraduationReport): string {
247
- const { summary } = report;
248
- const out: string[] = [
249
- `Feature-flag graduation (${summary.total} flags) — generated ${new Date(report.generatedAt).toISOString()}`,
250
- ` candidates awaiting decision: ${summary.graduateCandidate} · graduated: ${summary.graduated} · soaking: ${summary.soaking} · dark: ${summary.dark} · blocked: ${summary.blocked}`,
251
- '',
252
- ];
253
-
254
- const sorted = [...report.entries].sort(
255
- (a, b) => (GRADUATION_RANK[a.state] - GRADUATION_RANK[b.state]) || (a.tier - b.tier) || a.flagId.localeCompare(b.flagId),
256
- );
257
-
258
- if (report.releaseBlockers.length > 0) {
259
- out.push(`RELEASE BLOCKERS (${report.releaseBlockers.length}) — each must graduate (flip default ON) or record a dated blocker:`);
260
- for (const entry of sorted.filter((e) => e.state === 'graduate-candidate')) out.push(formatGraduationEntry(entry));
261
- out.push('');
262
- } else {
263
- out.push('No release blockers — nothing sits in graduate-candidate.');
264
- out.push('');
265
- }
266
-
267
- const rest = sorted.filter((e) => e.state !== 'graduate-candidate');
268
- if (rest.length > 0) {
269
- out.push('All other flags:');
270
- for (const entry of rest) out.push(formatGraduationEntry(entry));
271
- }
272
- return out.join('\n').trimEnd();
273
- }
274
-
275
- async function runGraduationReport(ctx: CommandContext): Promise<void> {
276
- const rpc = getOperatorRpc(ctx);
277
- if (!rpc.available) {
278
- ctx.print(`[flags graduation] ${rpc.reason}`);
279
- return;
280
- }
281
- try {
282
- const report = await rpc.sdk.operator.invoke('flags.graduation.report', {});
283
- ctx.print(formatGraduationReport(report));
284
- } catch (error) {
285
- ctx.print(`[flags graduation] ${describeOperatorRpcError(error)}`);
286
- }
287
- }
288
-
289
- export function registerFlagsRuntimeCommands(registry: CommandRegistry): void {
290
- registry.register({
291
- name: 'flags',
292
- description: 'List feature flags by state, toggle runtime-toggleable ones, surface dark subsystems, and report graduation readiness',
293
- usage: '[list|on <id>|off <id>|doctor|graduation]',
294
- argsHint: '[on|off <id> | doctor | graduation]',
295
- async handler(args, ctx) {
296
- const sub = (args[0] ?? 'list').toLowerCase();
297
- switch (sub) {
298
- case 'list':
299
- case 'ls':
300
- ctx.print(formatFlagsOverview(snapshotEntries(ctx)));
301
- return;
302
- case 'doctor':
303
- ctx.print(formatFlagsDoctor(snapshotEntries(ctx)));
304
- return;
305
- case 'graduation':
306
- case 'grad':
307
- await runGraduationReport(ctx);
308
- return;
309
- case 'on':
310
- case 'enable': {
311
- const id = args[1];
312
- if (!id) { ctx.print('Usage: /flags on <id>'); return; }
313
- applyToggle(ctx, id, 'enabled');
314
- return;
315
- }
316
- case 'off':
317
- case 'disable': {
318
- const id = args[1];
319
- if (!id) { ctx.print('Usage: /flags off <id>'); return; }
320
- applyToggle(ctx, id, 'disabled');
321
- return;
322
- }
323
- default:
324
- ctx.print(`Unknown /flags subcommand: ${sub}. Use: list | on <id> | off <id> | doctor | graduation`);
325
- }
326
- },
327
- });
328
- }