@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
@@ -0,0 +1,55 @@
1
+ /**
2
+ * durability-services.ts — the data-safety and remembered-decision stores the
3
+ * daemon composition wires (mirrors the SDK's own createRuntimeServices):
4
+ *
5
+ * - StoreSnapshotScheduler: a daily snapshot of every SQLite store this
6
+ * runtime writes (memory store, memory vector index, code index store),
7
+ * bounded by the retention engine. Timers are unref'd so an undisposed
8
+ * scheduler cannot pin the event loop; hosts that tear down a runtime
9
+ * stop() it themselves.
10
+ * - UserPermissionRuleStore: durable user-origin permission rules
11
+ * (remembered approvals) — one store per project, shared by every
12
+ * PermissionManager built on this runtime; permissions.rules.*
13
+ * lists/deletes. Background init is fail-safe (asks just prompt).
14
+ * - One credential chain (env -> secrets -> subscription): boot applies
15
+ * secrets-backed keys; every secrets write/delete re-registers builtins
16
+ * LIVE (no restart) — badges/picker/chat read the same instances.
17
+ */
18
+ import { join } from 'node:path';
19
+ import { resolveMemoryVectorDbPath } from '@pellux/goodvibes-sdk/platform/state';
20
+ import { StoreSnapshotScheduler } from '@pellux/goodvibes-sdk/platform/state/store-snapshots';
21
+ import { UserPermissionRuleStore } from '@pellux/goodvibes-sdk/platform/permissions';
22
+ import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
23
+
24
+ export interface DurabilityServicesInput {
25
+ readonly configManager: { getControlPlaneConfigDir(): string };
26
+ readonly secretsManager: { onDidChange(listener: (key: string) => void): () => void };
27
+ readonly providerRegistry: { refreshProviderCredentials(): Promise<void> };
28
+ readonly memoryDbPath: string;
29
+ readonly codeIndexDbPath: string;
30
+ }
31
+
32
+ export interface DurabilityServices {
33
+ readonly storeSnapshotScheduler: StoreSnapshotScheduler;
34
+ readonly userPermissionRuleStore: UserPermissionRuleStore;
35
+ }
36
+
37
+ export function createDurabilityServices(input: DurabilityServicesInput): DurabilityServices {
38
+ const { configManager, secretsManager, providerRegistry } = input;
39
+ secretsManager.onDidChange(() => void providerRegistry.refreshProviderCredentials().catch((error) => logger.warn('live credential refresh failed', { error: summarizeError(error) })));
40
+ void providerRegistry.refreshProviderCredentials().catch((error) => logger.warn('boot credential refresh failed', { error: summarizeError(error) }));
41
+
42
+ const storeSnapshotScheduler = new StoreSnapshotScheduler({
43
+ stores: [
44
+ { name: 'memory store', dbPath: input.memoryDbPath },
45
+ { name: 'memory vector index', dbPath: resolveMemoryVectorDbPath(input.memoryDbPath) },
46
+ { name: 'code index store', dbPath: input.codeIndexDbPath },
47
+ ],
48
+ });
49
+ storeSnapshotScheduler.start();
50
+
51
+ const userPermissionRuleStore = new UserPermissionRuleStore(join(configManager.getControlPlaneConfigDir(), 'permission-rules.json'));
52
+ void userPermissionRuleStore.init().catch((error) => logger.warn('user permission rule store init failed; asks will prompt', { error: summarizeError(error) }));
53
+
54
+ return { storeSnapshotScheduler, userPermissionRuleStore };
55
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * embedded-service-factories.ts — the factories startExternalServices uses to
3
+ * construct the interactive session's embedded DaemonServer + HttpListener
4
+ * (extracted from bootstrap.ts for file-size hygiene).
5
+ *
6
+ * The embedded DaemonServer suppresses the facade's internal auto-updater:
7
+ * that loop compares the sdk package's version — not this binary's — against
8
+ * this repo's releases, so inside the interactive session it would find an
9
+ * "update" hourly and exit the user's process on "restart". Clients update at
10
+ * launch (cli/launch-auto-update.ts); the embedded daemon runs no update loop
11
+ * at all. See daemon/lifecycle.ts.
12
+ */
13
+ import { DaemonServer, HttpListener } from '@pellux/goodvibes-sdk/platform/daemon';
14
+ import { createSafeHostServeFactory } from '../daemon/safe-serve.ts';
15
+ import type { startExternalServices } from '@/runtime/index.ts';
16
+
17
+ export type ExternalServiceFactories = NonNullable<Parameters<typeof startExternalServices>[4]>;
18
+
19
+ export function createEmbeddedServiceFactories(sharedDaemonToken: string): ExternalServiceFactories {
20
+ return {
21
+ sharedDaemonToken,
22
+ createDaemonServer: (bus, userAuth, runtimeServices) => {
23
+ // No updateArtifact: the embedded daemon keeps updates host-managed (the
24
+ // TUI client updates itself), so the facade runs no self-update loop.
25
+ const daemonServer = new DaemonServer({
26
+ runtimeBus: bus,
27
+ userAuth,
28
+ runtimeServices,
29
+ serveFactory: createSafeHostServeFactory('Embedded daemon'),
30
+ });
31
+ return daemonServer;
32
+ },
33
+ createHttpListener: (dispatcher, userAuth, configManager) => new HttpListener({
34
+ hookDispatcher: dispatcher,
35
+ userAuth,
36
+ configManager,
37
+ serveFactory: createSafeHostServeFactory('Embedded HTTP listener'),
38
+ }),
39
+ };
40
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * feature-settings — TUI-side helpers over the SDK's FEATURE_SETTINGS surface.
3
+ *
4
+ * Every platform capability is configured through a first-class settings key
5
+ * in its natural domain (behavior.compactionStrategy, sandbox.enabled,
6
+ * surfaces.slack.enabled, ...). There is no separate enablement namespace.
7
+ * These helpers answer the recurring TUI questions:
8
+ * - which feature a given id names, and whether a stock config has it on,
9
+ * - whether a raw config value means the feature is currently on,
10
+ * - which single settings write turns a feature on or off.
11
+ *
12
+ * Enablement kinds (from the SDK binding layer):
13
+ * - boolean : the key's boolean value is the enablement.
14
+ * - enum : active while the key's value is in enabledValues (several
15
+ * features can share one key, e.g. behavior.compactionStrategy).
16
+ * - constant: the capability has no separate off switch. When its key is a
17
+ * boolean (the surface enabled keys) that value is the honest
18
+ * user-facing switch; a non-boolean constant key (e.g.
19
+ * fetch.sanitizeMode) has no off position at all.
20
+ */
21
+
22
+ import { CONFIG_SCHEMA, type ConfigKey, type ConfigManager, type ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
23
+ import {
24
+ FEATURE_SETTINGS,
25
+ deriveFeatureState,
26
+ type FeatureSetting,
27
+ } from '@pellux/goodvibes-sdk/platform/runtime/state';
28
+
29
+ export type { FeatureSetting } from '@pellux/goodvibes-sdk/platform/runtime/state';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Lookups
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /** Every capability by id, in SDK declaration order. */
36
+ export const FEATURE_SETTINGS_BY_ID: ReadonlyMap<string, FeatureSetting> = new Map(
37
+ FEATURE_SETTINGS.map((feature) => [feature.id, feature]),
38
+ );
39
+
40
+ const SCHEMA_BY_KEY: ReadonlyMap<string, ConfigSetting> = new Map(
41
+ CONFIG_SCHEMA.map((setting) => [setting.key, setting]),
42
+ );
43
+
44
+ export function getFeatureSetting(featureId: string): FeatureSetting | null {
45
+ return FEATURE_SETTINGS_BY_ID.get(featureId) ?? null;
46
+ }
47
+
48
+ /** The CONFIG_SCHEMA descriptor for a settings key, or null for TUI-local synthetics. */
49
+ export function getConfigSchemaSetting(key: string): ConfigSetting | null {
50
+ return SCHEMA_BY_KEY.get(key) ?? null;
51
+ }
52
+
53
+ /** Whether a stock configuration has the feature active. */
54
+ export function isFeatureDefaultEnabled(featureId: string): boolean {
55
+ return getFeatureSetting(featureId)?.defaultEnabled === true;
56
+ }
57
+
58
+ /** All features whose enablement key is `key` (several features can share one). */
59
+ export function featuresForEnablementKey(key: string): readonly FeatureSetting[] {
60
+ return FEATURE_SETTINGS.filter((feature) => feature.enablement.key === key);
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Enablement derivation
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Whether a raw config value means the feature is on, as a settings surface
69
+ * should display it. Boolean/enum kinds derive exactly as the runtime gate
70
+ * does; constant kinds fall back to the boolean key value when there is one
71
+ * (the honest user-facing switch for the surface adapters) and read as always
72
+ * on otherwise.
73
+ */
74
+ export function isFeatureValueEnabled(feature: FeatureSetting, value: unknown): boolean {
75
+ const { key, kind, enabledValues } = feature.enablement;
76
+ if (kind === 'constant') {
77
+ const schema = getConfigSchemaSetting(key);
78
+ if (schema?.type === 'boolean') return value === true;
79
+ return true;
80
+ }
81
+ return deriveFeatureState(
82
+ {
83
+ featureId: feature.id,
84
+ key,
85
+ kind,
86
+ ...(enabledValues !== undefined ? { enabledValues } : {}),
87
+ },
88
+ value,
89
+ ) === 'enabled';
90
+ }
91
+
92
+ /** Whether the live config currently has the feature on (see isFeatureValueEnabled). */
93
+ export function isFeatureConfigEnabled(
94
+ configManager: Pick<ConfigManager, 'get'>,
95
+ featureId: string,
96
+ ): boolean {
97
+ const feature = getFeatureSetting(featureId);
98
+ if (!feature) return false;
99
+ return isFeatureValueEnabled(feature, configManager.get(feature.enablement.key));
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Enablement writes
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export interface FeatureEnablementWrite {
107
+ readonly key: ConfigKey;
108
+ readonly value: boolean | string;
109
+ }
110
+
111
+ /**
112
+ * The single settings write that turns a feature on or off, or null when the
113
+ * feature has no off position (a constant-kind capability on a non-boolean
114
+ * key). For enum kinds the schema default is preferred whenever it sits on
115
+ * the requested side, so "enable" lands on the feature's stock mode and
116
+ * "disable" lands on the stock off mode rather than an arbitrary value.
117
+ */
118
+ export function featureEnablementWrite(
119
+ featureId: string,
120
+ enabled: boolean,
121
+ ): FeatureEnablementWrite | null {
122
+ const feature = getFeatureSetting(featureId);
123
+ if (!feature) return null;
124
+ const { key, kind, enabledValues } = feature.enablement;
125
+
126
+ if (kind === 'boolean') return { key, value: enabled };
127
+
128
+ const schema = getConfigSchemaSetting(key);
129
+ if (kind === 'constant') {
130
+ return schema?.type === 'boolean' ? { key, value: enabled } : null;
131
+ }
132
+
133
+ // enum
134
+ const active = enabledValues ?? [];
135
+ const allValues = schema?.enumValues ?? [];
136
+ const schemaDefault = typeof schema?.default === 'string' ? schema.default : undefined;
137
+ if (enabled) {
138
+ const target = schemaDefault !== undefined && active.includes(schemaDefault)
139
+ ? schemaDefault
140
+ : active[0];
141
+ return target !== undefined ? { key, value: target } : null;
142
+ }
143
+ const offTarget = schemaDefault !== undefined && !active.includes(schemaDefault)
144
+ ? schemaDefault
145
+ : allValues.find((value) => !active.includes(value));
146
+ return offTarget !== undefined ? { key, value: offTarget } : null;
147
+ }
@@ -13,10 +13,6 @@ import {
13
13
  shell,
14
14
  transport,
15
15
  } from '@pellux/goodvibes-sdk/platform/runtime';
16
- import {
17
- createFeatureFlagManager as createSdkFeatureFlagManager,
18
- FEATURE_FLAG_MAP,
19
- } from '@pellux/goodvibes-sdk/platform/runtime/state';
20
16
  import { existsSync, statSync } from 'node:fs';
21
17
  import { join, resolve } from 'node:path';
22
18
  import type {
@@ -26,7 +22,6 @@ import type {
26
22
  shell as Shell,
27
23
  transport as Transport,
28
24
  } from '@pellux/goodvibes-sdk/platform/runtime';
29
- import type { FeatureFlagManager, FlagConfig } from '@pellux/goodvibes-sdk/platform/runtime/state';
30
25
 
31
26
  // Local runtime entry points.
32
27
  export { bootstrapRuntime } from './bootstrap.ts';
@@ -35,22 +30,10 @@ export type { BootstrapContext } from './bootstrap.ts';
35
30
  export { createUiRuntimeServices } from './ui-services.ts';
36
31
  export type { UiRuntimeServices } from './ui-services.ts';
37
32
 
38
- export function createFeatureFlagManager(): FeatureFlagManager {
39
- const manager = createSdkFeatureFlagManager();
40
- const originalLoadFromConfig = manager.loadFromConfig.bind(manager);
41
-
42
- manager.loadFromConfig = (config: FlagConfig): void => {
43
- originalLoadFromConfig({
44
- flags: Object.fromEntries(
45
- Object.entries(config.flags).filter(([id]) => FEATURE_FLAG_MAP.has(id)),
46
- ) as FlagConfig['flags'],
47
- });
48
- };
49
-
50
- return manager;
51
- }
52
-
53
- // Public SDK runtime seams.
33
+ // Public SDK runtime seams. createFeatureFlagManager comes straight from the
34
+ // SDK state seam below: gate states derive from domain settings keys
35
+ // (deriveFeatureStates), so the old TUI wrapper that filtered unknown ids out
36
+ // of persisted `featureFlags` overrides has nothing left to filter.
54
37
  export * from '@pellux/goodvibes-sdk/platform/runtime/state';
55
38
  export * from '@pellux/goodvibes-sdk/platform/runtime/store';
56
39
  export * from '@pellux/goodvibes-sdk/platform/runtime/ui';
@@ -1,9 +1,17 @@
1
1
  /**
2
- * The legacy `goodvibes-daemon.service` detect/migrate engine, shared
2
+ * The `goodvibes-daemon.service` detect/migrate engine, shared
3
3
  * by the daemon CLI (`src/daemon/service-commands.ts`, the `migrate-service`
4
4
  * subcommand) and the interactive TUI's onboarding guided UX
5
5
  * (`src/input/handler-onboarding-daemon-adopt.ts`).
6
6
  *
7
+ * NAMING, load-bearing: this module's identifiers say "legacy" because the
8
+ * engine migrates AWAY from the `goodvibes-daemon.service` unit name toward
9
+ * the runtime-managed unit — but that same name is what scripts/install.sh
10
+ * actively creates for curl-installed hosts TODAY. It is a parallel,
11
+ * first-class install path, not an obsolete one. User-facing copy therefore
12
+ * describes it as "the install-script unit" and never labels it legacy or
13
+ * implies it should be removed unless the user is explicitly migrating.
14
+ *
7
15
  * This lives under `src/runtime/` — not `src/daemon/` — specifically so the
8
16
  * `input` layer can consume it directly: the architecture gate's
9
17
  * `input-no-entrypoints` rule forbids `src/input/**` from importing
@@ -250,13 +258,15 @@ export function resolveManagedUnitName(status: ManagedServiceStatus): string {
250
258
  return basename(status.path).replace(/\.(service|plist)$/, '');
251
259
  }
252
260
 
253
- /** Honest one-line disclosure of the legacy unit's presence/state plus a manual migration hint — never auto-acted-on. */
261
+ /** Honest one-line disclosure of the install-script unit's presence/state plus a manual migration hint — never auto-acted-on. */
254
262
  export function legacyUnitNote(legacy: LegacyUnitInfo, trackedServiceName: string): string {
255
263
  const stateWord = legacy.active ? 'installed and RUNNING' : 'installed (not currently active)';
256
264
  return (
257
- `note: a service under the legacy name ${LEGACY_SERVICE_UNIT_NAME}.service is ${stateWord} at ${legacy.path} — ` +
258
- `this tool manages a different unit name (${trackedServiceName}.service) and will not touch the legacy one automatically. ` +
259
- `Remove it yourself when ready: systemctl --user disable --now ${LEGACY_SERVICE_UNIT_NAME}.service && rm ${legacy.path} && systemctl --user daemon-reload`
265
+ `note: a separate service named ${LEGACY_SERVICE_UNIT_NAME}.service is ${stateWord} at ${legacy.path} — ` +
266
+ `that unit name is managed by the goodvibes install script (older installs used it too), while this tool manages ` +
267
+ `${trackedServiceName}.service and will not touch the other unit automatically. Keep whichever one you use; running ` +
268
+ `both would start two daemons competing for the same port. To retire the install-script unit in favor of this ` +
269
+ `tool's: systemctl --user disable --now ${LEGACY_SERVICE_UNIT_NAME}.service && rm ${legacy.path} && systemctl --user daemon-reload`
260
270
  );
261
271
  }
262
272
 
@@ -317,7 +327,7 @@ function assertUnitIsNotLegacy(status: ManagedServiceStatus, legacy: LegacyUnitI
317
327
  if (status.path === legacy.path || resolveManagedUnitName(status) === LEGACY_SERVICE_UNIT_NAME) {
318
328
  throw new Error(
319
329
  `refusing to ${action}: the resolved managed unit (${resolveManagedUnitName(status)} at ${status.path}) is the ` +
320
- `legacy ${LEGACY_SERVICE_UNIT_NAME}.service unit — this should already have been caught by the pre-flight ` +
330
+ `install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit — this should already have been caught by the pre-flight ` +
321
331
  'collision check in runLegacyDaemonMigration',
322
332
  );
323
333
  }
@@ -356,7 +366,7 @@ export async function runLegacyDaemonMigration(
356
366
  ok: false,
357
367
  exitCode: 1,
358
368
  lines: [
359
- `migrate-service: no legacy ${LEGACY_SERVICE_UNIT_NAME}.service unit was found, but something is already ` +
369
+ `migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found, but something is already ` +
360
370
  `listening on ${params.host}:${params.port}.`,
361
371
  "That looks like a process this tool doesn't manage (for example, a manually-started `nohup` daemon) rather " +
362
372
  'than a systemd unit — there is nothing here to stop or disable, and this tool will not attempt to kill an ' +
@@ -371,7 +381,7 @@ export async function runLegacyDaemonMigration(
371
381
  ok: true,
372
382
  exitCode: 0,
373
383
  lines: [
374
- `migrate-service: no legacy ${LEGACY_SERVICE_UNIT_NAME}.service unit was found and ${params.host}:${params.port} ` +
384
+ `migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found and ${params.host}:${params.port} ` +
375
385
  'is free — there is nothing to migrate.',
376
386
  `Run install-service to set up the managed ${resolvedUnitName}.service directly.`,
377
387
  ],
@@ -385,8 +395,8 @@ export async function runLegacyDaemonMigration(
385
395
  exitCode: 1,
386
396
  lines: [
387
397
  `migrate-service: this host's detected service platform is '${currentStatus.platform}', not systemd, but a ` +
388
- `legacy unit file exists at ${legacy.path}.`,
389
- 'The legacy unit is systemd-specific and this tool only knows how to migrate a systemd legacy unit today — ' +
398
+ `unit file with the install-script name exists at ${legacy.path}.`,
399
+ 'That unit is systemd-specific and this tool only knows how to migrate a systemd unit today — ' +
390
400
  'nothing was changed.',
391
401
  ],
392
402
  status: currentStatus,
@@ -409,9 +419,9 @@ export async function runLegacyDaemonMigration(
409
419
  exitCode: 1,
410
420
  lines: [
411
421
  `migrate-service aborted: this host's 'service.serviceName' config key resolves to '${resolvedUnitName}', which ` +
412
- `is the exact legacy unit name (${LEGACY_SERVICE_UNIT_NAME}.service at ${legacy.path}) this migration is ` +
422
+ `is the exact install-script unit name (${LEGACY_SERVICE_UNIT_NAME}.service at ${legacy.path}) this migration is ` +
413
423
  'supposed to retire.',
414
- 'Installing or rolling back a unit under that name would overwrite or delete the legacy unit instead of ' +
424
+ 'Installing or rolling back a unit under that name would overwrite or delete the install-script unit instead of ' +
415
425
  'managing a separate one, so nothing has been changed.',
416
426
  `Fix: set the 'service.serviceName' config key to something other than '${LEGACY_SERVICE_UNIT_NAME}' (for ` +
417
427
  `example, the default '${trackedServiceName}') and re-run migrate-service.`,
@@ -429,7 +439,7 @@ export async function runLegacyDaemonMigration(
429
439
  'migrate-service (dry run — re-run with confirmation to execute): this would',
430
440
  ` 1. install and start the new ${resolvedUnitName}.service unit`,
431
441
  ' 2. verify it comes up healthy (a fresh, honest systemd is-active check)',
432
- ` 3. only if that succeeds, stop, disable, and remove the legacy ${LEGACY_SERVICE_UNIT_NAME}.service unit ` +
442
+ ` 3. only if that succeeds, stop, disable, and remove the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit ` +
433
443
  'and run `systemctl --user daemon-reload`',
434
444
  'Nothing has been changed. Nothing is migrated automatically — re-run with explicit confirmation ' +
435
445
  "(the CLI's -y/--yes flag) to execute this plan.",
@@ -452,7 +462,7 @@ export async function runLegacyDaemonMigration(
452
462
  exitCode: 1,
453
463
  lines: [
454
464
  `migrate-service aborted: could not write the new ${resolvedUnitName}.service unit (${installed.actionError}).`,
455
- `The legacy ${LEGACY_SERVICE_UNIT_NAME}.service unit was never touched.`,
465
+ `The install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was never touched.`,
456
466
  ],
457
467
  status: installed,
458
468
  };
@@ -473,7 +483,7 @@ export async function runLegacyDaemonMigration(
473
483
  `migrate-service aborted: the new ${resolvedUnitName}.service unit did not come up healthy` +
474
484
  (started.actionError ? ` (${started.actionError}).` : '.'),
475
485
  rollbackNote,
476
- `The legacy ${LEGACY_SERVICE_UNIT_NAME}.service unit was never touched and should still be running as before.`,
486
+ `The install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was never touched and should still be running as before.`,
477
487
  ],
478
488
  status: healthCheck,
479
489
  };
@@ -496,20 +506,20 @@ export async function runLegacyDaemonMigration(
496
506
  const lines = [`migrated: the new ${resolvedUnitName}.service unit is installed, enabled, and running.`];
497
507
  if ((stopResult.status ?? 1) !== 0) {
498
508
  lines.push(
499
- `note: stopping the legacy unit reported a non-zero exit (${stopResult.stderr ?? stopResult.stdout ?? 'no output'}); ` +
509
+ `note: stopping the install-script unit reported a non-zero exit (${stopResult.stderr ?? stopResult.stdout ?? 'no output'}); ` +
500
510
  'it may already have been stopped.',
501
511
  );
502
512
  }
503
513
  if ((disableResult.status ?? 1) !== 0) {
504
514
  lines.push(
505
- `note: disabling the legacy unit reported a non-zero exit (${disableResult.stderr ?? disableResult.stdout ?? 'no output'}); ` +
515
+ `note: disabling the install-script unit reported a non-zero exit (${disableResult.stderr ?? disableResult.stdout ?? 'no output'}); ` +
506
516
  'it may already have been disabled.',
507
517
  );
508
518
  }
509
519
  if (removeError) {
510
- lines.push(`note: could not remove the legacy unit file at ${legacy.path}: ${removeError} — remove it by hand.`);
520
+ lines.push(`note: could not remove the install-script unit file at ${legacy.path}: ${removeError} — remove it by hand.`);
511
521
  } else {
512
- lines.push(`the legacy ${LEGACY_SERVICE_UNIT_NAME}.service unit has been stopped, disabled, and removed.`);
522
+ lines.push(`the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit has been stopped, disabled, and removed.`);
513
523
  }
514
524
  lines.push('ran `systemctl --user daemon-reload`.');
515
525
  return { ok: true, exitCode: 0, lines, status: healthCheck };
@@ -21,7 +21,7 @@ export async function foldTuiLegacyMemory(
21
21
  ): Promise<MemoryFoldReport> {
22
22
  const legacyTuiProject = join(workingDirectory, '.goodvibes', 'tui', 'memory.sqlite');
23
23
  const sources: LegacyMemorySource[] = [
24
- { label: `tui:${workingDirectory} (pre-E6)`, dbPath: legacyTuiProject },
24
+ { label: `tui:${workingDirectory} (legacy per-project store)`, dbPath: legacyTuiProject },
25
25
  ];
26
26
  return foldMemoryStores(memoryStore, sources, { embeddingRegistry: memoryEmbeddingRegistry });
27
27
  }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Notification dispatch — the production wiring that finally gives the
3
+ * panel_only notification target a live producer.
4
+ *
5
+ * The SDK's NotificationRouter decides where each domain notification goes
6
+ * (conversation / status_bar / panel_only) and collapses bursts and batches.
7
+ * Nothing in the running app routed real notifications before this: the
8
+ * panel_only feed (panels/notifications-feed.ts) and its panel existed with no
9
+ * upstream. This module builds the router, records every panel_only decision
10
+ * (including burst-collapsed ones) into the shared feed, and bridges the
11
+ * runtime event bus so real domain events become notifications.
12
+ */
13
+
14
+ import { createNotificationRouter, type NotificationRouter } from '@pellux/goodvibes-sdk/platform/runtime/ui';
15
+ import type { Notification, RoutingDecision, RuntimeEventBus, RuntimeEventDomain } from '@/runtime/index.ts';
16
+ import type { ConfigManager } from '../config/index.ts';
17
+ import { getSharedNotificationFeed, type PanelNotificationFeed } from '../panels/notifications-feed.ts';
18
+
19
+ export interface NotificationDispatcher {
20
+ /** Route a notification; a panel_only (or burst-collapsed) decision lands in the feed. Returns the decision. */
21
+ dispatch(notification: Notification): RoutingDecision;
22
+ /** Surface batch-held notifications (call on a timer / when quiet-typing ends). */
23
+ flush(): void;
24
+ readonly router: NotificationRouter;
25
+ }
26
+
27
+ /**
28
+ * The runtime event domains whose events surface as operational notifications.
29
+ * Deliberately a curated set of user-relevant, completion/attention-shaped
30
+ * domains — not every domain — so the panel reflects meaningful operational
31
+ * activity rather than raw event churn. The router's per-domain verbosity and
32
+ * burst/batch policies still collapse floods within these.
33
+ */
34
+ export const NOTIFICATION_BRIDGE_DOMAINS: readonly RuntimeEventDomain[] = [
35
+ 'agents',
36
+ 'tasks',
37
+ 'workflows',
38
+ 'automation',
39
+ 'deliveries',
40
+ 'security',
41
+ ];
42
+
43
+ /** Turn an UPPER_SNAKE event type into a short human title ("AGENT_COMPLETED" → "Agent completed"). */
44
+ export function humanizeEventType(type: string): string {
45
+ const words = type.toLowerCase().split(/[_\s]+/).filter(Boolean);
46
+ if (words.length === 0) return type;
47
+ return words.map((word, index) => (index === 0 ? word[0]!.toUpperCase() + word.slice(1) : word)).join(' ');
48
+ }
49
+
50
+ /** Derive a notification severity from an event type — errors/failures warn, the rest are informational. */
51
+ export function levelForEventType(type: string): Notification['level'] {
52
+ const upper = type.toUpperCase();
53
+ if (/(FAILED|ERROR|CRASH|DENIED|BLOCKED)/.test(upper)) return 'warning';
54
+ if (/(CRITICAL|FATAL|BREACH)/.test(upper)) return 'critical';
55
+ return 'info';
56
+ }
57
+
58
+ export function createNotificationDispatcher(
59
+ configManager: Pick<ConfigManager, 'get'>,
60
+ feed: PanelNotificationFeed = getSharedNotificationFeed(),
61
+ ): NotificationDispatcher {
62
+ const router = createNotificationRouter(undefined, undefined, configManager);
63
+ const recordIfPanel = (notification: Notification, decision: RoutingDecision): void => {
64
+ if (decision.suppressed) return;
65
+ if (decision.target === 'panel_only') feed.record(notification, decision);
66
+ };
67
+ return {
68
+ router,
69
+ dispatch(notification) {
70
+ const decision = router.route(notification);
71
+ recordIfPanel(notification, decision);
72
+ return decision;
73
+ },
74
+ flush() {
75
+ for (const { notification } of router.flush()) {
76
+ // A flushed batch head surfaces as a batch-collapsed panel entry; the
77
+ // feed folds all sharing this batch key into one running-count row.
78
+ feed.record(notification, {
79
+ target: 'panel_only',
80
+ reasonCode: 'batch_window_collapsed',
81
+ batchKey: `${notification.domain}:${notification.level}`,
82
+ });
83
+ }
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Bridge the runtime event bus to the dispatcher: each event in a curated
90
+ * domain becomes a notification and is routed. Returns an unsubscribe function
91
+ * that detaches every domain listener.
92
+ */
93
+ export function wireRuntimeNotificationBridge(
94
+ runtimeBus: RuntimeEventBus,
95
+ dispatcher: Pick<NotificationDispatcher, 'dispatch'>,
96
+ domains: readonly RuntimeEventDomain[] = NOTIFICATION_BRIDGE_DOMAINS,
97
+ ): () => void {
98
+ const unsubscribes = domains.map((domain) =>
99
+ runtimeBus.onDomain(domain, (envelope) => {
100
+ dispatcher.dispatch({
101
+ id: envelope.traceId ?? `${domain}-${envelope.type}-${envelope.ts}`,
102
+ domain,
103
+ level: levelForEventType(envelope.type),
104
+ title: humanizeEventType(envelope.type),
105
+ timestamp: envelope.ts,
106
+ });
107
+ }),
108
+ );
109
+ return () => { for (const unsubscribe of unsubscribes) unsubscribe(); };
110
+ }
@@ -2,7 +2,6 @@ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
2
2
  import { atomicWriteFileSync } from '../../config/atomic-write.ts';
3
3
  import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config';
4
4
  import { CONFIG_SCHEMA, DEFAULT_CONFIG } from '../../config/index.ts';
5
- import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
6
5
  import {
7
6
  getOnboardingRuntimeStatePath,
8
7
  readOnboardingRuntimeState,
@@ -133,39 +132,14 @@ function isMalformedGoodVibesSecretReferenceValue(value: string): boolean {
133
132
  return normalized.startsWith('goodvibes://') && !isGoodVibesSecretReferenceValue(normalized);
134
133
  }
135
134
 
136
- function isFeatureFlagConfigKey(key: string): key is FeatureFlagConfigKey {
137
- return key === 'featureFlags' || key.startsWith('featureFlags.');
138
- }
139
-
140
- function validateFeatureFlagConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): boolean {
141
- if (!isFeatureFlagConfigKey(operation.key)) return false;
142
-
143
- if (operation.key === 'featureFlags') {
144
- if (!isPlainObject(operation.value)) throw new Error('featureFlags expects an object value.');
145
- for (const [flagId, state] of Object.entries(operation.value)) {
146
- if (flagId.trim().length === 0) throw new Error('featureFlags cannot contain an empty feature id.');
147
- if (state !== 'enabled' && state !== 'disabled') {
148
- throw new Error(`featureFlags.${flagId} expects enabled or disabled.`);
149
- }
150
- }
151
- return true;
152
- }
153
-
154
- const flagId = operation.key.slice('featureFlags.'.length);
155
- if (flagId.trim().length === 0) throw new Error('featureFlags requires a feature id.');
156
- if (operation.value !== 'enabled' && operation.value !== 'disabled') {
157
- throw new Error(`Config key ${operation.key} expects enabled or disabled.`);
158
- }
159
- return true;
160
- }
161
-
162
135
  function validateConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): void {
163
136
  if (typeof operation.value === 'string' && isMalformedGoodVibesSecretReferenceValue(operation.value)) {
164
137
  throw new Error(`Config key ${operation.key} only accepts goodvibes://secrets/... secret references.`);
165
138
  }
166
139
 
167
- if (validateFeatureFlagConfigValue(operation)) return;
168
-
140
+ // Feature enablement arrives as plain domain settings writes (e.g.
141
+ // sandbox.enabled, controlPlane.gateway), validated by the schema path below
142
+ // like any other key — there is no separate enablement namespace.
169
143
  const schema = CONFIG_SCHEMA.find((entry) => entry.key === operation.key);
170
144
  if (!schema) {
171
145
  const defaultValue = operation.key.split('.').reduce<unknown>((cursor, part) => (