@pellux/goodvibes-tui 1.16.0 → 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 (220) hide show
  1. package/CHANGELOG.md +95 -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/onboarding/onboarding-wizard.ts +22 -8
  167. package/src/renderer/panel-composite.ts +2 -2
  168. package/src/renderer/panel-workspace-bar.ts +1 -1
  169. package/src/renderer/process-indicator.ts +1 -1
  170. package/src/renderer/selection-modal-overlay.ts +102 -24
  171. package/src/renderer/session-picker-modal.ts +2 -2
  172. package/src/renderer/settings-modal-helpers.ts +14 -5
  173. package/src/renderer/settings-modal.ts +127 -39
  174. package/src/renderer/shell-surface.ts +3 -0
  175. package/src/renderer/terminal-bg-probe.ts +1 -1
  176. package/src/renderer/theme.ts +3 -3
  177. package/src/renderer/tool-call.ts +1 -1
  178. package/src/renderer/tool-result-summary.ts +1 -1
  179. package/src/renderer/turn-injection.ts +7 -6
  180. package/src/renderer/ui-factory.ts +6 -1
  181. package/src/runtime/bootstrap-command-context.ts +4 -1
  182. package/src/runtime/bootstrap-command-parts.ts +6 -4
  183. package/src/runtime/bootstrap-core.ts +35 -35
  184. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  185. package/src/runtime/bootstrap-shell.ts +6 -5
  186. package/src/runtime/bootstrap.ts +34 -32
  187. package/src/runtime/channel-composition.ts +46 -0
  188. package/src/runtime/code-index-services.ts +3 -4
  189. package/src/runtime/daemon-attach-notices.ts +120 -0
  190. package/src/runtime/durability-services.ts +55 -0
  191. package/src/runtime/embedded-service-factories.ts +40 -0
  192. package/src/runtime/feature-settings.ts +147 -0
  193. package/src/runtime/index.ts +4 -21
  194. package/src/runtime/legacy-daemon-migration.ts +29 -19
  195. package/src/runtime/memory-fold.ts +1 -1
  196. package/src/runtime/notification-dispatch.ts +110 -0
  197. package/src/runtime/onboarding/apply.ts +3 -29
  198. package/src/runtime/onboarding/fast-path.ts +39 -6
  199. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  200. package/src/runtime/onboarding/snapshot.ts +0 -1
  201. package/src/runtime/onboarding/types.ts +9 -3
  202. package/src/runtime/provider-fallback.ts +55 -0
  203. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  204. package/src/runtime/release-artifacts.ts +19 -103
  205. package/src/runtime/resume-notice.ts +1 -1
  206. package/src/runtime/services.ts +81 -82
  207. package/src/runtime/session-continuity-hints.ts +38 -0
  208. package/src/runtime/surface-feature-flags.ts +40 -9
  209. package/src/runtime/terminal-output-guard.ts +2 -2
  210. package/src/runtime/trust/workspace-registration.ts +10 -2
  211. package/src/runtime/trust/workspace-trust.ts +58 -31
  212. package/src/runtime/ui-services.ts +3 -1
  213. package/src/runtime/update-check.ts +15 -69
  214. package/src/runtime/workstream-services.ts +2 -2
  215. package/src/shell/blocking-input.ts +102 -6
  216. package/src/shell/ui-openers.ts +38 -6
  217. package/src/utils/format-duration.ts +1 -1
  218. package/src/verification/verification-ledger.ts +21 -4
  219. package/src/version.ts +1 -1
  220. package/src/input/commands/flags-runtime.ts +0 -328
@@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises';
4
4
  import type { CommandRegistry, CommandContext } from '../command-registry.ts';
5
5
  import type { SelectionItem } from '../selection-modal.ts';
6
6
  import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers';
7
+ import { formatModelDiscoveryReport } from '@pellux/goodvibes-sdk/platform/providers';
7
8
  import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils';
8
9
  import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, resolveSecretRef } from '@pellux/goodvibes-sdk/platform/config';
9
10
  import { openCommandPanel, requireBookmarkManager, requireProviderApi, requireSecretsManager } from './runtime-services.ts';
@@ -70,7 +71,7 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
70
71
  handler(args, ctx) {
71
72
  const sub = (args[0] ?? '').toLowerCase();
72
73
  if (sub === 'panel' || sub === 'review') {
73
- // W6.1 (the purge): 'tools'/ToolInspectorPanel was DELETE-disposition
74
+ // (the purge): 'tools'/ToolInspectorPanel was DELETE-disposition
74
75
  // (no surviving human surface — tool results render inline in the
75
76
  // transcript, plus a per-node tool list in Fleet). There is no alias
76
77
  // to resolve through, so this prints an honest notice and opens
@@ -200,10 +201,42 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
200
201
  const flags = new Set(rest.filter((value) => value.startsWith('--')));
201
202
  const valueParts = rest.filter((value) => !value.startsWith('--'));
202
203
  const [key, ...rawValueParts] = valueParts;
203
- if (!key || valueParts.length === 0) {
204
+ if (!key) {
204
205
  ctx.print(`[secrets] Usage: /secrets ${sub} <KEY> <${sub === 'link' ? 'secret-ref' : 'value'}> [--user|--project] [--secure|--plaintext]`);
205
206
  return;
206
207
  }
208
+ const scope = flags.has('--user') ? 'user' : 'project';
209
+ const medium = flags.has('--plaintext') ? 'plaintext' : 'secure';
210
+ // /secrets set KEY (no value) drops into the concealed prompt so the
211
+ // value is typed with the keystrokes masked and never enters history.
212
+ if (sub === 'set' && rawValueParts.length === 0) {
213
+ if (!ctx.beginConcealedInput) {
214
+ ctx.print('[secrets] Usage: /secrets set <KEY> <value> [--user|--project] [--secure|--plaintext]');
215
+ return;
216
+ }
217
+ ctx.print(`[secrets] Enter value for ${key} (input is masked; press Enter to store, Esc to cancel).`);
218
+ ctx.beginConcealedInput({
219
+ label: key,
220
+ onSubmit: (value) => {
221
+ if (value.length === 0) {
222
+ ctx.print(`[secrets] ${key} not set (empty value).`);
223
+ return;
224
+ }
225
+ void (async () => {
226
+ try {
227
+ await mgr.set(key, value, { scope, medium });
228
+ ctx.print(`[secrets] Stored: ${key} (${scope}, ${medium})`);
229
+ } catch (error) {
230
+ ctx.print(`[secrets] Could not store ${key}: ${summarizeError(error)}`);
231
+ }
232
+ })();
233
+ },
234
+ onCancel: () => {
235
+ ctx.print(`[secrets] ${key} cancelled.`);
236
+ },
237
+ });
238
+ return;
239
+ }
207
240
  const value = rawValueParts.join(' ');
208
241
  if (sub === 'link' && !isGoodVibesSecretRefInput(value)) {
209
242
  ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
@@ -213,8 +246,6 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
213
246
  ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
214
247
  return;
215
248
  }
216
- const scope = flags.has('--user') ? 'user' : 'project';
217
- const medium = flags.has('--plaintext') ? 'plaintext' : 'secure';
218
249
  await mgr.set(key, value, { scope, medium });
219
250
  ctx.print(sub === 'link'
220
251
  ? `[secrets] Linked: ${key} -> ${describeSecretRef(value)} (${scope}, ${medium})`
@@ -337,7 +368,24 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
337
368
  } catch (e) {
338
369
  ctx.print(`Token limits refresh failed: ${summarizeError(e)}`);
339
370
  }
340
- if (!catalogOk || !benchmarksOk || !limitsOk) ctx.print('Some refreshes failed — see messages above.');
371
+ ctx.print('Refreshing live provider model lists...');
372
+ let discoveryOk = false;
373
+ try {
374
+ // Explicit user refresh → force past each provider's TTL cache and
375
+ // report the delta ("3 new, 1 retired") or the honest fallback reason.
376
+ const reports = await providerApi.refreshLiveModelDiscovery(undefined, { force: true });
377
+ discoveryOk = true;
378
+ if (reports.length === 0) {
379
+ ctx.print('No providers perform live model discovery.');
380
+ } else {
381
+ for (const report of reports) {
382
+ ctx.print(` ${formatModelDiscoveryReport(report.providerId, report)}`);
383
+ }
384
+ }
385
+ } catch (e) {
386
+ ctx.print(`Live model discovery failed: ${summarizeError(e)}`);
387
+ }
388
+ if (!catalogOk || !benchmarksOk || !limitsOk || !discoveryOk) ctx.print('Some refreshes failed — see messages above.');
341
389
  },
342
390
  });
343
391
 
@@ -24,7 +24,7 @@ import { handleRecallFilesApply, handleRecallFilesReview, handleRecallFilesSync
24
24
  // ── Top-level command ─────────────────────────────────────────────────────────
25
25
 
26
26
  /**
27
- * DEBT-5 item 3 divergence note (historical): the work order that shipped
27
+ * item 3 divergence note (historical): the work order that shipped
28
28
  * this command named its front door "/memory", but at the time `/memory` was
29
29
  * already a distinct, unrelated command (session-pinned sticky notes,
30
30
  * src/input/commands/session-content.ts) with no modal surface — so that
@@ -84,7 +84,7 @@ export const recallCommand: SlashCommand = {
84
84
  handler: async (args: string[], context: CommandContext): Promise<void> => {
85
85
  const [sub, ...rest] = args;
86
86
 
87
- // DEBT-5 item 3: bare `/recall` opens the memory-modal surface — the old
87
+ // item 3: bare `/recall` opens the memory-modal surface — the old
88
88
  // bare/unknown-subcommand usage block moved to an explicit `report`
89
89
  // subcommand (scriptability preserved: /recall report).
90
90
  if (sub === undefined) {
@@ -5,7 +5,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
5
 
6
6
  /**
7
7
  * Parse `--target <id>[:<kind>]` out of the args following `<panel-id>
8
- * [top|bottom]` (DEBT-5 item 4). Splits the args array in place (removing the
8
+ * [top|bottom]` (item 4). Splits the args array in place (removing the
9
9
  * flag + its value) so positional `pane` parsing downstream is unaffected by
10
10
  * where the flag appears. Only `open fleet` currently has a deep-link
11
11
  * consumer (FleetPanel.receiveDeepLink) — other panel ids just ignore an
@@ -32,7 +32,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
32
32
  const pm = requirePanelManager(ctx);
33
33
  const sub = args[0]?.toLowerCase() ?? '';
34
34
  if (!sub || sub === 'toggle') {
35
- // W6.1 (the purge): 'panel-list' (the browse-all-panels picker) was
35
+ // (the purge): 'panel-list' (the browse-all-panels picker) was
36
36
  // DELETE-disposition — a picker over a handful of panels is dead
37
37
  // weight now (see the Ctrl+P selectionModal repoint in
38
38
  // shell/ui-openers.ts for its interactive replacement). Bare
@@ -71,9 +71,9 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
71
71
  // no panel lands in the workspace. Report that honestly rather than
72
72
  // claiming "Panel opened: <id>".
73
73
  const redirectTarget = pm.getModalRedirect(id);
74
- // UX-C focus rule 1a: the command path leaves focus in the composer
74
+ // focus rule 1a: the command path leaves focus in the composer
75
75
  // ("mid-command-flow") — showPanel does not grab panel focus here.
76
- // DEBT-5: forward the deep-link target so the panel lands on the row.
76
+ // forward the deep-link target so the panel lands on the row.
77
77
  if (ctx.showPanel) ctx.showPanel(id, pane as 'top' | 'bottom' | undefined, target);
78
78
  else {
79
79
  pm.open(id, pane as 'top' | 'bottom' | undefined, target);
@@ -169,7 +169,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
169
169
  } else {
170
170
  const id = args[0]!;
171
171
  try {
172
- // UX-C: bare `/panel <id>` is the same command path as `/panel open
172
+ // bare `/panel <id>` is the same command path as `/panel open
173
173
  // <id>` — composer stays focused.
174
174
  if (ctx.showPanel) ctx.showPanel(id);
175
175
  else {
@@ -63,7 +63,7 @@ const PERMISSION_KEYS: readonly { readonly key: string; readonly label: string;
63
63
  { key: 'permissions.mode', label: 'Permission mode', note: 'also the current session mode' },
64
64
  { key: 'permissions.backgroundAgents', label: 'Background-agent mode' },
65
65
  { key: 'behavior.autoApprove', label: 'Auto-approve everything' },
66
- { key: 'featureFlags.permissions-policy-engine', label: 'Runtime policy engine (flag)' },
66
+ { key: 'permissions.engine', label: 'Permission evaluator', note: 'baseline or policy-engine' },
67
67
  { key: 'permissions.tools.read', label: 'Tool rule: read' },
68
68
  { key: 'permissions.tools.write', label: 'Tool rule: write' },
69
69
  { key: 'permissions.tools.edit', label: 'Tool rule: edit' },
@@ -1,21 +1,112 @@
1
- import type { CommandRegistry } from '../command-registry.ts';
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
+ import type { SelectionItem, SelectionAction } from '../selection-modal.ts';
2
3
  import { buildPermissionProvenance, renderPermissionProvenance } from './permissions-provenance.ts';
4
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
+
6
+ /** One remembered-rule row: what it does, and how/when it was remembered. */
7
+ function describeRuleRow(record: {
8
+ rule: { id: string; description?: string | undefined; effect: 'allow' | 'deny' };
9
+ tier: string;
10
+ tool: string;
11
+ createdAt: number;
12
+ }): { label: string; detail: string } {
13
+ const what = record.rule.description ?? record.rule.id;
14
+ return {
15
+ label: `${record.tool}: ${what}`,
16
+ detail: `${record.rule.effect} · remembered ${record.tier} · ${new Date(record.createdAt).toLocaleString()}`,
17
+ };
18
+ }
19
+
20
+ function showRememberedRules(ctx: CommandContext): void {
21
+ const store = ctx.clients?.userPermissionRuleStore;
22
+ if (!store) {
23
+ ctx.print('Remembered permission rules are not available on this surface.');
24
+ return;
25
+ }
26
+ const records = store.list();
27
+ if (records.length === 0) {
28
+ ctx.print('No remembered permission rules. Approving an ask with a remember tier stores one here.');
29
+ return;
30
+ }
31
+ if (ctx.openSelection) {
32
+ const revoke = new Map<string, SelectionAction>([['d', 'delete']]);
33
+ const items: SelectionItem[] = records.map((record) => {
34
+ const { label, detail } = describeRuleRow(record);
35
+ return { id: record.rule.id, label, detail, actions: '[d] revoke' };
36
+ });
37
+ ctx.openSelection('Remembered permission rules', items, { allowSearch: true, customActions: revoke }, (result) => {
38
+ if (!result || result.action !== 'delete') return;
39
+ void (async () => {
40
+ try {
41
+ const removed = await store.delete(result.item.id);
42
+ ctx.print(removed ? `Revoked remembered rule: ${result.item.label}` : `Rule already gone: ${result.item.label}`);
43
+ } catch (error) {
44
+ ctx.print(`Could not revoke the rule: ${summarizeError(error)}`);
45
+ }
46
+ })();
47
+ });
48
+ return;
49
+ }
50
+ // No selection surface: list them honestly with the revoke command to run.
51
+ ctx.print([
52
+ 'Remembered permission rules:',
53
+ ...records.map((record) => {
54
+ const { label, detail } = describeRuleRow(record);
55
+ return ` ${record.rule.id} ${label} (${detail})`;
56
+ }),
57
+ '',
58
+ 'Revoke one with: /permissions revoke <rule-id>',
59
+ ].join('\n'));
60
+ }
61
+
62
+ async function revokeRememberedRule(ctx: CommandContext, ruleId: string): Promise<void> {
63
+ const store = ctx.clients?.userPermissionRuleStore;
64
+ if (!store) {
65
+ ctx.print('Remembered permission rules are not available on this surface.');
66
+ return;
67
+ }
68
+ if (!ruleId) {
69
+ ctx.print('Usage: /permissions revoke <rule-id> (see /permissions rules for ids)');
70
+ return;
71
+ }
72
+ try {
73
+ const removed = await store.delete(ruleId);
74
+ ctx.print(removed ? `Revoked remembered rule: ${ruleId}` : `No remembered rule with id: ${ruleId}`);
75
+ } catch (error) {
76
+ ctx.print(`Could not revoke the rule: ${summarizeError(error)}`);
77
+ }
78
+ }
3
79
 
4
80
  /**
5
- * `/permissions` — the permission provenance panel. Prints every
6
- * permission-relevant setting in effect and where each value came from
7
- * (default / config file / runtime override / session mode), plus the current
8
- * session mode. Read-only: it changes no config. See permissions-provenance.ts
9
- * for how origin is resolved from the platform's own ConfigManager.
81
+ * `/permissions` — the permission settings surface. With no arguments it prints
82
+ * every permission-relevant setting in effect and where each value came from
83
+ * (provenance; read-only). `rules` lists the durable remembered approvals with a
84
+ * press-Enter revoke; `revoke <id>` removes one by id. Revokes hit the same
85
+ * in-process rule store the evaluator reads, so they take effect live.
10
86
  */
11
87
  export function registerPermissionsRuntimeCommands(registry: CommandRegistry): void {
12
88
  registry.register({
13
89
  name: 'permissions',
14
90
  aliases: ['perms'],
15
- description: 'Show permission settings in effect and where each value came from (provenance)',
16
- handler(_args, ctx) {
91
+ description: 'Show permission settings and provenance, and list or revoke remembered approval rules',
92
+ usage: '[rules | revoke <rule-id>]',
93
+ argsHint: '[rules|revoke]',
94
+ async handler(args, ctx) {
95
+ const sub = (args[0] ?? '').toLowerCase();
96
+ if (sub === 'rules' || sub === 'rule') {
97
+ showRememberedRules(ctx);
98
+ return;
99
+ }
100
+ if (sub === 'revoke' || sub === 'forget') {
101
+ await revokeRememberedRule(ctx, args[1] ?? '');
102
+ return;
103
+ }
17
104
  const provenance = buildPermissionProvenance(ctx.platform.configManager);
18
105
  ctx.print(renderPermissionProvenance(provenance));
106
+ const ruleCount = ctx.clients?.userPermissionRuleStore?.list().length ?? 0;
107
+ ctx.print(ruleCount > 0
108
+ ? `\n${ruleCount} remembered approval rule${ruleCount === 1 ? '' : 's'} in effect. Use /permissions rules to review or revoke them.`
109
+ : '\nNo remembered approval rules. Use /permissions rules once you have some.');
19
110
  },
20
111
  });
21
112
  }
@@ -13,7 +13,7 @@ import { togglePlanMode, permissionModeLabel, type PermissionModeValue } from '.
13
13
  * A lone one of these is refused rather than seeded as a goal, so a stray verb
14
14
  * can never overwrite the project goal with itself.
15
15
  *
16
- * DEBT-3: `dismiss` and `answer` are now REAL subcommands (handled above this
16
+ * `dismiss` and `answer` are now REAL subcommands (handled above this
17
17
  * guard), so they were removed from the refuse-list. `pause`/`stop`/`cancel`
18
18
  * remain here — they still have no backing verb and must not seed a goal.
19
19
  */
@@ -178,7 +178,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
178
178
  return;
179
179
  }
180
180
 
181
- // DEBT-3: /plan dismiss — archive the current plan. Dismisses the active
181
+ // /plan dismiss — archive the current plan. Dismisses the active
182
182
  // execution plan (ExecutionPlanManager.dismiss, honest per-state) AND
183
183
  // deactivates the project-planning interview state shown in the modal so a
184
184
  // later /plan <goal> starts fresh. Mid-execution is refused outright.
@@ -223,7 +223,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
223
223
  return;
224
224
  }
225
225
 
226
- // DEBT-3: /plan answer <n|question-id> <text> — record a real answer to an
226
+ // /plan answer <n|question-id> <text> — record a real answer to an
227
227
  // open planning question (moves open → answered, consumed on next refine).
228
228
  if (args[0] === 'answer') {
229
229
  if (!projectPlanningService || !projectId) {
@@ -98,7 +98,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
98
98
  const shellPaths = requireShellPaths(ctx);
99
99
  const sub = args[0] ?? 'open';
100
100
  if (sub === 'open' || sub === 'panel') {
101
- ctx.openModal?.('sandbox-modal'); // W6.1: sandbox panel -> config modal
101
+ ctx.openModal?.('sandbox-modal'); // sandbox panel -> config modal
102
102
  return;
103
103
  }
104
104
  if (sub === 'review') {
@@ -3,11 +3,13 @@ import type {
3
3
  ProviderAccountRecord,
4
4
  ProviderAccountSnapshot,
5
5
  } from '@/runtime/index.ts';
6
+ import type { SelectionItem } from '../selection-modal.ts';
6
7
  import {
7
- openCommandPanel,
8
8
  requireOperatorClient,
9
9
  } from './runtime-services.ts';
10
10
 
11
+ type RecommendedAction = ProviderAccountRecord['recommendedActions'][number];
12
+
11
13
  async function loadProviderAccountSnapshot(context: CommandContext): Promise<ProviderAccountSnapshot> {
12
14
  return await requireOperatorClient(context).providers.accountSnapshot();
13
15
  }
@@ -20,6 +22,71 @@ function findProviderAccountRecord(
20
22
  return snapshot.providers.find((entry) => entry.providerId === providerId);
21
23
  }
22
24
 
25
+ /**
26
+ * Present a record's recommended actions as executable rows. A recommended
27
+ * action that carries a structured command becomes a press-Enter row that RUNS
28
+ * that command through the command registry — never a line the user has to
29
+ * retype. Actions with no command are genuinely out-of-product steps, so they
30
+ * stay as plain (non-executing) rows. When there is no interactive selection
31
+ * surface, each action is printed honestly instead.
32
+ */
33
+ export function presentRecommendedActions(
34
+ ctx: CommandContext,
35
+ providerId: string,
36
+ actions: readonly RecommendedAction[],
37
+ ): void {
38
+ if (actions.length === 0) {
39
+ ctx.print('No active repair actions suggested.');
40
+ return;
41
+ }
42
+ if (ctx.openSelection) {
43
+ const items: SelectionItem[] = actions.map((action, index) => {
44
+ if (action.command) {
45
+ const argsPart = action.command.args.length > 0 ? ` ${action.command.args.join(' ')}` : '';
46
+ return {
47
+ id: String(index),
48
+ label: action.description,
49
+ detail: `runs /${action.command.name}${argsPart}`,
50
+ primaryAction: 'select',
51
+ };
52
+ }
53
+ return {
54
+ id: String(index),
55
+ label: action.description,
56
+ detail: 'manual step (nothing to run)',
57
+ primaryAction: 'select',
58
+ };
59
+ });
60
+ ctx.openSelection(
61
+ `Repair ${providerId}`,
62
+ items,
63
+ { allowSearch: false, primaryVerbLabel: 'Run' },
64
+ (result) => {
65
+ if (!result) return;
66
+ const action = actions[Number(result.item.id)];
67
+ if (!action) return;
68
+ if (action.command) {
69
+ const args = [...action.command.args];
70
+ void (ctx.executeCommand?.(action.command.name, args)
71
+ ?? Promise.resolve(ctx.print(`Cannot run /${action.command.name} — command execution is unavailable here.`)));
72
+ return;
73
+ }
74
+ ctx.print(action.description);
75
+ },
76
+ );
77
+ return;
78
+ }
79
+ // No selection surface (headless/command-only): list actions honestly.
80
+ ctx.print([
81
+ 'Recommended actions:',
82
+ ...actions.map((action) => (
83
+ action.command
84
+ ? ` • ${action.description} — /${action.command.name}${action.command.args.length > 0 ? ` ${action.command.args.join(' ')}` : ''}`
85
+ : ` • ${action.description} (manual step)`
86
+ )),
87
+ ].join('\n'));
88
+ }
89
+
23
90
  export function registerProviderAccountsRuntimeCommands(registry: CommandRegistry): void {
24
91
  registry.register({
25
92
  name: 'accounts',
@@ -29,7 +96,7 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
29
96
  async handler(args, ctx) {
30
97
  const sub = (args[0] ?? 'review').toLowerCase();
31
98
  if (sub === 'panel' || sub === 'open') {
32
- ctx.openModal?.('providers-modal'); // W6.1: accounts alias -> provider-health modal
99
+ ctx.openModal?.('providers-modal'); // accounts alias -> provider-health modal
33
100
  return;
34
101
  }
35
102
  const snapshot = await loadProviderAccountSnapshot(ctx);
@@ -63,10 +130,8 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
63
130
  ` preferred route: ${record.preferredRoute}`,
64
131
  ...(record.fallbackRisk ? [` fallback: ${record.fallbackRisk}`] : []),
65
132
  ...(record.issues.map((issue) => ` issue: ${issue}`)),
66
- ...(record.recommendedActions.length > 0
67
- ? [' next:', ...record.recommendedActions.map((action) => ` ${action}`)]
68
- : [' No active repair actions suggested.']),
69
133
  ].join('\n'));
134
+ presentRecommendedActions(ctx, record.providerId, record.recommendedActions);
70
135
  return;
71
136
  }
72
137
  if (sub === 'show') {
@@ -95,8 +160,8 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
95
160
  ...record.usageWindows.map((entry) => ` window: ${entry.label} — ${entry.detail}`),
96
161
  ...record.issues.map((issue) => ` issue: ${issue}`),
97
162
  ...record.notes.map((note) => ` note: ${note}`),
98
- ...record.recommendedActions.map((action) => ` next: ${action}`),
99
163
  ].join('\n'));
164
+ presentRecommendedActions(ctx, record.providerId, record.recommendedActions);
100
165
  return;
101
166
  }
102
167
  ctx.print([
@@ -15,14 +15,13 @@
15
15
  */
16
16
 
17
17
  import type { SlashCommand, CommandContext } from '../command-registry.ts';
18
- import type { ConfigKey } from '../../config/index.ts';
19
18
  import type { RouteExplanation } from '@pellux/goodvibes-sdk/platform/providers';
20
19
  import type { FallbackTestResult, FallbackTransition } from '@pellux/goodvibes-sdk/platform/providers';
21
20
  import type { ProviderApiModelRecord } from '@pellux/goodvibes-sdk/platform/providers';
22
21
  import { requireProviderApi } from './runtime-services.ts';
22
+ import { featureEnablementWrite } from '../../runtime/feature-settings.ts';
23
23
 
24
24
  const PROVIDER_OPTIMIZER_FLAG = 'provider-optimizer';
25
- const PROVIDER_OPTIMIZER_CONFIG_KEY = `featureFlags.${PROVIDER_OPTIMIZER_FLAG}` as ConfigKey;
26
25
 
27
26
  // ---------------------------------------------------------------------------
28
27
  // Formatting helpers
@@ -95,9 +94,12 @@ function handleOptimizerToggle(
95
94
  const wasEnabled = optimizer.enabled;
96
95
  optimizer.setEnabled(enable);
97
96
 
98
- // Persist to config so the setting survives restart.
99
- const flagValue = enable ? 'enabled' : 'disabled';
100
- context.platform.configManager.setDynamic(PROVIDER_OPTIMIZER_CONFIG_KEY, flagValue);
97
+ // Persist to config so the setting survives restart: the optimizer is
98
+ // governed by the provider.optimizerMode settings key (off = disabled;
99
+ // manual/auto/pinned = active), and the settings bridge forwards the same
100
+ // write to the runtime gate.
101
+ const write = featureEnablementWrite(PROVIDER_OPTIMIZER_FLAG, enable);
102
+ if (write) context.platform.configManager.setDynamic(write.key, write.value);
101
103
 
102
104
  if (enable && !wasEnabled) {
103
105
  context.print('[provider] Optimizer enabled.');
@@ -1,57 +1,19 @@
1
1
  /**
2
- * recall-files-git.ts — the synchronous git seam for memory file projection.
2
+ * recall-files-git.ts — the git seam for memory file projection.
3
3
  *
4
- * `projectMemoryToFiles(records, dir, { git })` (SDK, platform/state) is a
5
- * pure synchronous function and its `MemoryProjectionGit` seam is declared
6
- * synchronous (`add(dir): void`, `commit(dir, message): void`) — it throws on
7
- * failure rather than returning a result. The TUI's existing git wrapper,
8
- * `GitService` (platform/git), is async (wraps simple-git), so it cannot
9
- * satisfy this seam directly. `GitService`'s own static helpers
10
- * (initRepo/isGitRepo/getRepoRoot) ARE synchronous, using `Bun.spawnSync`
11
- * directly (see service.ts) — this file follows the exact same convention
12
- * for `add`/`commit` so the seam matches the SDK's synchronous contract.
4
+ * The SDK owns the production seam now (`createMemoryProjectionGit`,
5
+ * platform/state): it shells out to the system git, commits with a neutral
6
+ * projection identity (never the operator's), tolerates an unchanged tree,
7
+ * and — through `projectMemoryToFiles`'s ownership gate — only ever commits
8
+ * to a repository whose toplevel IS the projection directory, initializing
9
+ * one there when needed. A projection directory nested inside some other
10
+ * checkout can never pollute that checkout. This module is a thin local
11
+ * seam factory so command code keeps a single import site.
13
12
  */
13
+ import { createMemoryProjectionGit } from '@pellux/goodvibes-sdk/platform/state';
14
14
  import type { MemoryProjectionGit } from '@pellux/goodvibes-sdk/platform/state';
15
- import { GitService } from '@pellux/goodvibes-sdk/platform/git';
16
15
 
17
- function decode(bytes: Uint8Array | undefined): string {
18
- return bytes ? new TextDecoder().decode(bytes).trim() : '';
19
- }
20
-
21
- /**
22
- * Build a synchronous git seam rooted at `repoRoot`. `commit()` treats a
23
- * "nothing to commit" exit as a benign no-op (not an error) so
24
- * `/recall files sync` stays safely re-runnable when the projection files
25
- * did not actually change since the last sync — any OTHER commit failure
26
- * (no git identity configured, hook rejection, etc.) still throws, since the
27
- * seam's contract has no way to report partial failure back to the caller
28
- * other than throwing.
29
- */
30
- export function createSyncGitSeam(repoRoot: string): MemoryProjectionGit {
31
- return {
32
- add(dir: string): void {
33
- const result = Bun.spawnSync(['git', 'add', dir], { cwd: repoRoot });
34
- if (result.exitCode !== 0) {
35
- throw new Error(`git add failed: ${decode(result.stderr) || 'unknown error'}`);
36
- }
37
- },
38
- commit(dir: string, message: string): void {
39
- const result = Bun.spawnSync(['git', 'commit', '-m', message, '--', dir], { cwd: repoRoot });
40
- if (result.exitCode === 0) return;
41
- const output = `${decode(result.stdout)} ${decode(result.stderr)}`;
42
- if (/nothing to commit/i.test(output)) return; // benign — no new/changed files this run
43
- throw new Error(`git commit failed: ${output.trim() || 'unknown error'}`);
44
- },
45
- };
46
- }
47
-
48
- /**
49
- * Resolve the git seam for `dir`, or null when `dir` is not inside a git
50
- * repository. Never auto-initializes a repository — that is a much bigger
51
- * side effect than a memory sync command should take unasked.
52
- */
53
- export function resolveSyncGitSeam(dir: string): { seam: MemoryProjectionGit; repoRoot: string } | null {
54
- const repoRoot = GitService.getRepoRoot(dir);
55
- if (!repoRoot) return null;
56
- return { seam: createSyncGitSeam(repoRoot), repoRoot };
16
+ /** The projection git seam (see module note — SDK-owned behavior). */
17
+ export function createSyncGitSeam(): MemoryProjectionGit {
18
+ return createMemoryProjectionGit();
57
19
  }
@@ -39,7 +39,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
39
39
  import { requireShellPaths } from './runtime-services.ts';
40
40
  import { getMemorySpine } from './recall-query.ts';
41
41
  import { readMemoryProjectionDir } from './recall-files-config.ts';
42
- import { resolveSyncGitSeam } from './recall-files-git.ts';
42
+ import { createSyncGitSeam } from './recall-files-git.ts';
43
43
 
44
44
  const PROJECTION_LIST_LIMIT = 5000;
45
45
 
@@ -58,19 +58,16 @@ export async function handleRecallFilesSync(args: string[], context: CommandCont
58
58
 
59
59
  const dir = resolveProjectionDir(args, context);
60
60
  const records = await memory.list({ limit: PROJECTION_LIST_LIMIT });
61
- // Resolve the git seam from the project working directory, not `dir` itself
62
- // — `dir` may not exist yet on a first sync, and `git -C <dir>` requires the
63
- // directory to already exist. The working directory is always real.
64
- const gitSeam = resolveSyncGitSeam(requireShellPaths(context).workingDirectory);
65
61
 
66
62
  try {
67
- const report = projectMemoryToFiles(records, dir, gitSeam ? { git: gitSeam.seam } : {});
63
+ // The SDK's ownership gate commits only to a repository whose toplevel IS
64
+ // the projection directory (initializing one there when needed) — never
65
+ // to a checkout the directory merely sits inside.
66
+ const report = projectMemoryToFiles(records, dir, { git: createSyncGitSeam() });
68
67
  context.print(`[recall] Projected ${report.written.length} record(s) to ${report.dir}`);
69
- if (gitSeam) {
70
- context.print(report.committed ? ` committed in ${gitSeam.repoRoot}` : ' not committed (git seam did not run)');
71
- } else {
72
- context.print(` not committed: ${dir} is not inside a git repository`);
73
- }
68
+ context.print(report.committed
69
+ ? ` committed in ${report.dir} (the projection's own repository)`
70
+ : ' not committed (git seam did not run)');
74
71
  } catch (error) {
75
72
  context.print(`[recall] Projection sync failed: ${summarizeError(error)}`);
76
73
  }
@@ -112,9 +112,9 @@ export function handleRecallExplain(args: string[], context: CommandContext): vo
112
112
  * `exportState()` returns the same full `AgentRecord[]` (it's what
113
113
  * compactConversation() already uses for the same reason).
114
114
  *
115
- * With NO agent id (wo805 wiring), renders the MAIN interactive session's own
115
+ * With NO agent id (wiring), renders the MAIN interactive session's own
116
116
  * per-turn injection ring — `Orchestrator.getTurnInjections()`, threaded onto
117
- * the command context as `session.getMainSessionTurnInjections`. As of wo805 the
117
+ * the command context as `session.getMainSessionTurnInjections`. As of the
118
118
  * main session DOES route through the passive-injection engine, so this is the
119
119
  * honest main-session default (previously there was no record source, so the
120
120
  * no-id path could only print a usage hint). When the accessor isn't wired
@@ -47,7 +47,7 @@ async function renderStatus(ctx: CommandContext): Promise<void> {
47
47
  async function renderPair(ctx: CommandContext): Promise<void> {
48
48
  const cfg = relayConfigured(ctx);
49
49
  if (!cfg.enabled || !cfg.flagOn) {
50
- ctx.print('Relay is disabled — enable relay.enabled and the relay-connect feature flag first (see /config relay, /flags).');
50
+ ctx.print('Relay is disabled — turn on relay.enabled in /config relay first.');
51
51
  return;
52
52
  }
53
53
  const externalServices = ctx.platform.externalServices;
@@ -48,7 +48,7 @@ export function registerRemoteRuntimeCommands(registry: CommandRegistry): void {
48
48
  async handler(args, ctx) {
49
49
  if (args.length === 0) {
50
50
  if (ctx.openModal) {
51
- ctx.openModal('remote-modal'); // W6.1: remote panel -> config modal
51
+ ctx.openModal('remote-modal'); // remote panel -> config modal
52
52
  return;
53
53
  }
54
54
  ctx.print('Remote surface is not available in this runtime.');
@@ -104,8 +104,8 @@ export function openCommandPanel(
104
104
  }
105
105
 
106
106
  /**
107
- * W6.1 (group-B migration): open a config-modal surface by name via the
108
- * ctx.openModal seam (ui-openers wires it; the WO-A host replaces the interim
107
+ * (group-B migration): open a config-modal surface by name via the
108
+ * ctx.openModal seam (ui-openers wires it; the host replaces the interim
109
109
  * "not available yet" implementation with real dispatch). Front-doors for
110
110
  * panels that migrated to modals call this instead of openCommandPanel — the
111
111
  * modal is the surface's new home. Stubbed in tests by setting ctx.openModal.
@@ -256,7 +256,7 @@ export function requireProviderApi(context: CommandContext): ProviderApi {
256
256
  }
257
257
 
258
258
  /** Result of a manual /compact run: the SDK's CompactionEvent, plus an
259
- * out-of-band quality score (W5.4/B28) when one could be computed. */
259
+ * out-of-band quality score () when one could be computed. */
260
260
  export interface CompactionRunResult {
261
261
  readonly event: CompactionEvent;
262
262
  readonly qualityScore: CompactionQualityScore | null;
@@ -328,7 +328,7 @@ export async function compactConversation(context: CommandContext): Promise<Comp
328
328
  const eventAfter = getLastCompactionEvent();
329
329
  // Return the new event only if it differs from the one recorded before the call.
330
330
  if (eventAfter !== null && eventAfter !== eventBefore) {
331
- // W5.4/B28: score this run out-of-band (see compaction-quality.ts for why
331
+ //: score this run out-of-band (see compaction-quality.ts for why
332
332
  // this can't just subscribe to the SDK's own CompactionManager pipeline)
333
333
  // and keep the score keyed by this event's timestamp for /compact-history.
334
334
  const qualityScore = scoreCompactionRun({