@pellux/goodvibes-tui 1.13.1 → 1.15.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 (158) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +6 -6
  3. package/bin/goodvibes +6 -0
  4. package/bin/goodvibes-daemon +6 -0
  5. package/bin/launcher-support.js +34 -1
  6. package/docs/foundation-artifacts/operator-contract.json +8076 -2047
  7. package/package.json +15 -11
  8. package/scripts/postinstall.js +73 -48
  9. package/src/cli/bundle-command.ts +5 -5
  10. package/src/cli/completion.ts +1 -1
  11. package/src/cli/completions/generate.ts +15 -3
  12. package/src/cli/doctor.ts +385 -0
  13. package/src/cli/entrypoint.ts +72 -1
  14. package/src/cli/help.ts +29 -9
  15. package/src/cli/hooks-command.ts +74 -0
  16. package/src/cli/hooks-report.ts +167 -0
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/management.ts +13 -1
  19. package/src/cli/parser.ts +10 -2
  20. package/src/cli/plugin-bundles-command.ts +228 -0
  21. package/src/cli/plugin-command.ts +179 -0
  22. package/src/cli/service-posture.ts +10 -0
  23. package/src/cli/status.ts +275 -2
  24. package/src/cli/tui-startup.ts +119 -1
  25. package/src/cli/types.ts +3 -1
  26. package/src/config/index.ts +16 -0
  27. package/src/config/tui-extension-settings.ts +169 -0
  28. package/src/core/approval-alert.ts +15 -2
  29. package/src/core/compaction-receipt.ts +127 -0
  30. package/src/core/model-routing-chip.ts +82 -0
  31. package/src/core/permission-mode.ts +89 -0
  32. package/src/core/rewind-receipt.ts +83 -0
  33. package/src/core/rewind-turn-anchors.ts +160 -0
  34. package/src/core/scriptable-statusline.ts +141 -0
  35. package/src/core/session-auto-titler.ts +110 -0
  36. package/src/core/stream-event-wiring.ts +20 -0
  37. package/src/core/system-message-router.ts +1 -1
  38. package/src/core/terminal-notifier.ts +102 -0
  39. package/src/core/turn-event-wiring.ts +80 -1
  40. package/src/input/command-reference.ts +96 -0
  41. package/src/input/command-registry.ts +25 -0
  42. package/src/input/commands/channel-pairing.ts +245 -0
  43. package/src/input/commands/channel-runtime.ts +152 -6
  44. package/src/input/commands/checkin-runtime.ts +89 -0
  45. package/src/input/commands/checkpoint-runtime.ts +7 -179
  46. package/src/input/commands/ci-runtime.ts +207 -0
  47. package/src/input/commands/cost-attribution-format.ts +52 -0
  48. package/src/input/commands/cost-runtime.ts +44 -4
  49. package/src/input/commands/editor-runtime.ts +22 -0
  50. package/src/input/commands/flags-runtime.ts +328 -0
  51. package/src/input/commands/health-metrics-format.ts +67 -0
  52. package/src/input/commands/health-runtime.ts +55 -1
  53. package/src/input/commands/memory.ts +23 -0
  54. package/src/input/commands/operator-rpc.ts +95 -0
  55. package/src/input/commands/palette-runtime.ts +22 -0
  56. package/src/input/commands/permissions-provenance.ts +264 -0
  57. package/src/input/commands/permissions-runtime.ts +21 -0
  58. package/src/input/commands/planning-runtime.ts +28 -1
  59. package/src/input/commands/platform-access-runtime.ts +2 -97
  60. package/src/input/commands/principals-runtime.ts +212 -0
  61. package/src/input/commands/product-runtime.ts +43 -3
  62. package/src/input/commands/recall-files-config.ts +54 -0
  63. package/src/input/commands/recall-files-git.ts +57 -0
  64. package/src/input/commands/recall-files.ts +185 -0
  65. package/src/input/commands/recall-query.ts +8 -3
  66. package/src/input/commands/recall-review.ts +2 -2
  67. package/src/input/commands/recall-shared.ts +13 -0
  68. package/src/input/commands/relay-runtime.ts +86 -0
  69. package/src/input/commands/review-runtime.ts +160 -0
  70. package/src/input/commands/rewind-runtime.ts +362 -0
  71. package/src/input/commands/schedule-nl.ts +147 -0
  72. package/src/input/commands/schedule-runtime.ts +29 -10
  73. package/src/input/commands/secret-runtime.ts +55 -0
  74. package/src/input/commands/session-content.ts +19 -8
  75. package/src/input/commands/session-workflow.ts +10 -0
  76. package/src/input/commands/update-runtime.ts +412 -0
  77. package/src/input/commands/workstream-attempt-dependents.ts +103 -0
  78. package/src/input/commands/workstream-attempts.ts +244 -0
  79. package/src/input/commands/workstream-runtime.ts +164 -26
  80. package/src/input/commands/worktree-runtime.ts +56 -2
  81. package/src/input/commands.ts +135 -80
  82. package/src/input/composer-capture.ts +67 -0
  83. package/src/input/composer-editor.ts +134 -0
  84. package/src/input/concealed-input.ts +94 -0
  85. package/src/input/exec-env-scrub-config.ts +84 -0
  86. package/src/input/feature-unit-layout.ts +142 -0
  87. package/src/input/feed-context-factory.ts +2 -0
  88. package/src/input/handler-feed-routes.ts +17 -0
  89. package/src/input/handler-feed.ts +3 -0
  90. package/src/input/handler-modal-routes.ts +5 -2
  91. package/src/input/handler-shortcuts.ts +37 -0
  92. package/src/input/handler.ts +24 -2
  93. package/src/input/keybindings.ts +11 -2
  94. package/src/input/onboarding/onboarding-feature-units.ts +536 -0
  95. package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
  96. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
  97. package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
  98. package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
  99. package/src/input/relay-settings-descriptions.ts +35 -0
  100. package/src/input/sandbox-exec-config.ts +107 -0
  101. package/src/input/settings-modal-adjustment.ts +4 -7
  102. package/src/input/settings-modal-data.ts +178 -8
  103. package/src/input/settings-modal-mutations.ts +41 -13
  104. package/src/input/settings-modal-types.ts +42 -5
  105. package/src/input/settings-modal.ts +67 -15
  106. package/src/input/submission-router.ts +4 -1
  107. package/src/input/worktree-setup-config.ts +93 -0
  108. package/src/main.ts +43 -43
  109. package/src/panels/builtin/development.ts +14 -0
  110. package/src/panels/diff-review-model.ts +189 -0
  111. package/src/panels/diff-review-panel.ts +395 -0
  112. package/src/panels/fleet-panel-format.ts +114 -2
  113. package/src/panels/fleet-panel.ts +65 -78
  114. package/src/panels/fleet-read-model.ts +91 -11
  115. package/src/panels/fleet-stop.ts +19 -4
  116. package/src/panels/modals/memory-modal.ts +11 -2
  117. package/src/permissions/hunk-selection.ts +86 -5
  118. package/src/permissions/prompt.ts +108 -7
  119. package/src/permissions/sandbox-exec-gate.ts +162 -0
  120. package/src/renderer/settings-modal-helpers.ts +21 -1
  121. package/src/renderer/settings-modal.ts +70 -44
  122. package/src/renderer/shell-surface.ts +23 -1
  123. package/src/renderer/system-message.ts +4 -0
  124. package/src/renderer/tool-result-summary.ts +95 -5
  125. package/src/renderer/ui-factory.ts +22 -0
  126. package/src/runtime/bootstrap-command-context.ts +15 -1
  127. package/src/runtime/bootstrap-command-parts.ts +14 -2
  128. package/src/runtime/bootstrap-core.ts +38 -38
  129. package/src/runtime/bootstrap-notifier-sync.ts +41 -0
  130. package/src/runtime/bootstrap-shell.ts +8 -0
  131. package/src/runtime/bootstrap.ts +21 -23
  132. package/src/runtime/context-accounting-source.ts +103 -0
  133. package/src/runtime/conversation-rewind-port.ts +130 -0
  134. package/src/runtime/fleet-needs-input-push.ts +70 -0
  135. package/src/runtime/install-self-check-startup.ts +36 -0
  136. package/src/runtime/install-self-check.ts +149 -0
  137. package/src/runtime/onboarding/fast-path.ts +93 -0
  138. package/src/runtime/process-lifecycle.ts +32 -24
  139. package/src/runtime/relay-reachability-bridge.ts +53 -0
  140. package/src/runtime/release-artifacts.ts +109 -0
  141. package/src/runtime/services.ts +33 -33
  142. package/src/runtime/trust/workspace-registration.ts +184 -0
  143. package/src/runtime/trust/workspace-trust.ts +202 -0
  144. package/src/runtime/ui-services.ts +2 -1
  145. package/src/runtime/update-check.ts +113 -0
  146. package/src/runtime/workspace-checkpointing.ts +32 -0
  147. package/src/runtime/workstream-attempts-validation.ts +77 -0
  148. package/src/runtime/workstream-draft-edits.ts +95 -0
  149. package/src/runtime/workstream-draft-store.ts +101 -0
  150. package/src/runtime/workstream-draft-types.ts +42 -0
  151. package/src/runtime/workstream-services.ts +70 -39
  152. package/src/shell/blocking-input.ts +29 -0
  153. package/src/shell/ui-openers.ts +65 -0
  154. package/src/verification/verification-ledger.ts +56 -2
  155. package/src/version.ts +1 -1
  156. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
  157. package/src/runtime/gateway-verbs.ts +0 -47
  158. package/src/runtime/render-scheduler.ts +0 -90
@@ -0,0 +1,536 @@
1
+ /**
2
+ * Guided feature coverage for onboarding.
3
+ *
4
+ * Every feature flag that is NOT already reached through a surface/server
5
+ * capability selection (or the HITL experience step) is presented here as ONE
6
+ * unit — its enable toggle plus a meaningful subset of its config sub-options —
7
+ * grouped into thematic first-run steps (safety & sandboxing, memory & context,
8
+ * telemetry & observability, automation & initiative, provider & runtime, and a
9
+ * compact advanced-toggles step). Full sub-option depth lives in /settings; each
10
+ * unit points there.
11
+ *
12
+ * The wizard step builders and the apply builder both read from this single
13
+ * declarative table so the onboarding surface and the settings surface stay in
14
+ * lockstep with the SDK feature-flag set.
15
+ */
16
+
17
+ import type { ConfigKey } from '../../config/index.ts';
18
+ import { FEATURE_FLAG_MAP } from '@pellux/goodvibes-sdk/platform/runtime/state';
19
+ import type {
20
+ OnboardingWizardControllerLike,
21
+ OnboardingWizardFieldDefinition,
22
+ OnboardingWizardRadioOption,
23
+ OnboardingWizardStepDefinition,
24
+ OnboardingWizardStepId,
25
+ } from './onboarding-wizard-types.ts';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Declarative model
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** A single tunable sub-option of a feature unit, rendered as a radio and written when the feature is enabled. */
32
+ export interface FeatureSubOption {
33
+ /** Short id; forms the wizard field id `feature.<flagId>.<key>`. */
34
+ readonly key: string;
35
+ readonly configKey: ConfigKey;
36
+ readonly label: string;
37
+ readonly hint: string;
38
+ /** boolean sub-options render as a Yes/No radio and coerce to a boolean config value. */
39
+ readonly valueType: 'enum' | 'boolean' | 'text';
40
+ /** For enum: the option ids (valid schema enum values). For boolean: ignored (Yes/No). */
41
+ readonly options?: readonly string[];
42
+ /** Default option id (enum) / 'yes'|'no' (boolean) / default text. */
43
+ readonly defaultValue: string;
44
+ readonly placeholder?: string;
45
+ }
46
+
47
+ /** A feature unit: its gating flag, its enable label, and its onboarding sub-options. */
48
+ export interface FeatureUnit {
49
+ readonly flagId: string;
50
+ readonly label: string;
51
+ readonly hint: string;
52
+ /** Optional extra guidance rendered as a status row under the toggle. */
53
+ readonly note?: string;
54
+ /** Config written verbatim when the feature is enabled (e.g. sandbox.enabled=true). */
55
+ readonly impliedConfig?: ReadonlyArray<{ readonly key: ConfigKey; readonly value: unknown }>;
56
+ /** Prerequisite flags that must also be enabled for this feature to function. */
57
+ readonly requiresFlags?: readonly string[];
58
+ readonly subOptions?: readonly FeatureSubOption[];
59
+ }
60
+
61
+ export interface FeatureSection {
62
+ readonly stepId: OnboardingWizardStepId;
63
+ readonly title: string;
64
+ readonly shortLabel: string;
65
+ readonly description: string;
66
+ readonly units: readonly FeatureUnit[];
67
+ }
68
+
69
+ const YES_NO_OPTIONS: readonly OnboardingWizardRadioOption[] = [
70
+ { id: 'yes', label: 'Yes', hint: 'Turn this sub-option on.' },
71
+ { id: 'no', label: 'No', hint: 'Leave this sub-option off.' },
72
+ ];
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // The feature coverage table
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
79
+ {
80
+ stepId: 'features-safety',
81
+ title: 'Safety and sandboxing',
82
+ shortLabel: 'Safety',
83
+ description: 'Optional guardrails for command execution, outbound fetch, tokens, and permission policy. All off by default unless noted; full tuning lives in /settings.',
84
+ units: [
85
+ {
86
+ flagId: 'exec-sandbox',
87
+ label: 'Per-command exec sandbox',
88
+ hint: 'Run shell commands inside an OS-level boundary (bubblewrap on Linux): workspace writable, rest read-only, network off unless allowlisted. Reports unavailable and leaves exec unchanged where bubblewrap is absent.',
89
+ impliedConfig: [{ key: 'sandbox.enabled' as ConfigKey, value: true }],
90
+ },
91
+ {
92
+ flagId: 'sandbox-model-judgment',
93
+ label: 'Sandbox model-judgment tier',
94
+ hint: 'When the exec sandbox leaves a command on ask, a model pass annotates the ask with stated reasons. Never converts allow to deny; never touches the frozen catastrophic block.',
95
+ subOptions: [
96
+ {
97
+ key: 'auto-approve',
98
+ configKey: 'sandbox.judgmentAutoApprove' as ConfigKey,
99
+ label: 'Auto-approve looks-safe verdicts',
100
+ hint: 'Yes lets a looks-safe judgment auto-approve the ask; No keeps annotate-only (default).',
101
+ valueType: 'boolean',
102
+ defaultValue: 'no',
103
+ },
104
+ ],
105
+ },
106
+ {
107
+ flagId: 'fetch-sanitization',
108
+ label: 'Fetch response sanitization',
109
+ hint: 'Sanitize fetched HTTP content and block SSRF-risk hosts (private IPs, metadata endpoints, localhost).',
110
+ subOptions: [
111
+ {
112
+ key: 'mode',
113
+ configKey: 'fetch.sanitizeMode' as ConfigKey,
114
+ label: 'Default sanitize mode',
115
+ hint: 'none, safe-text (strip active/script content, default), or strict (aggressive text-only).',
116
+ valueType: 'enum',
117
+ options: ['none', 'safe-text', 'strict'],
118
+ defaultValue: 'safe-text',
119
+ },
120
+ ],
121
+ },
122
+ {
123
+ flagId: 'token-scope-rotation-audit',
124
+ label: 'Token scope and rotation audit',
125
+ hint: 'Audit API token scopes and rotation cadence, surfacing overdue or over-scoped tokens.',
126
+ subOptions: [
127
+ {
128
+ key: 'managed',
129
+ configKey: 'security.tokenAudit.managed' as ConfigKey,
130
+ label: 'Block violating tokens (managed)',
131
+ hint: 'Yes blocks over-scoped/overdue tokens from use; No is advisory reporting only (default).',
132
+ valueType: 'boolean',
133
+ defaultValue: 'no',
134
+ },
135
+ ],
136
+ },
137
+ {
138
+ flagId: 'permissions-policy-engine',
139
+ label: 'Permissions policy engine',
140
+ hint: 'Activate the granular tool-class and path-level permission model. Your permission posture from the Experience step still applies.',
141
+ },
142
+ {
143
+ flagId: 'permission-divergence-dashboard',
144
+ label: 'Divergence dashboard and enforce gate',
145
+ hint: 'Aggregate permission-simulation divergence and gate enforce-mode transitions when the divergence rate is too high.',
146
+ },
147
+ {
148
+ flagId: 'policy-as-code',
149
+ label: 'Policy-as-code',
150
+ hint: 'Versioned policy bundle registry with promote/rollback and a divergence gate. Exposes /policy commands.',
151
+ subOptions: [
152
+ {
153
+ key: 'source',
154
+ configKey: 'policy.bundleSource' as ConfigKey,
155
+ label: 'Startup bundle source',
156
+ hint: 'none (bundles supplied via commands) or file (load policy.bundlePath — set it in /settings).',
157
+ valueType: 'enum',
158
+ options: ['none', 'file'],
159
+ defaultValue: 'none',
160
+ },
161
+ ],
162
+ },
163
+ {
164
+ flagId: 'shell-ast-normalization',
165
+ label: 'Shell AST command analysis',
166
+ hint: 'Per-segment verdicts for compound commands with more specific denial explanations. Default on; falls back to the flat matcher on parser failure.',
167
+ },
168
+ ],
169
+ },
170
+ {
171
+ stepId: 'features-context',
172
+ title: 'Memory and context',
173
+ shortLabel: 'Context',
174
+ description: 'How the shell manages the conversation window and what it retrieves per turn. Several default on; turn any off here.',
175
+ units: [
176
+ {
177
+ flagId: 'session-compaction',
178
+ label: 'Structured session compaction',
179
+ hint: 'Compact the main session with semantic chunking and relevance scoring as the window fills.',
180
+ subOptions: [
181
+ {
182
+ key: 'strategy',
183
+ configKey: 'behavior.compactionStrategy' as ConfigKey,
184
+ label: 'Compaction strategy',
185
+ hint: 'structured (in-place summarization, default) or distiller (a fresh-context continuation brief). distiller also needs the distiller feature below.',
186
+ valueType: 'enum',
187
+ options: ['structured', 'distiller'],
188
+ defaultValue: 'structured',
189
+ },
190
+ ],
191
+ },
192
+ {
193
+ flagId: 'compaction-distiller-strategy',
194
+ label: 'Fresh-context distiller compaction',
195
+ hint: 'Enable the distiller strategy option (used only when the strategy above is set to distiller). Scored through the same quality gate as structured, with fallback.',
196
+ },
197
+ {
198
+ flagId: 'agent-context-window-awareness',
199
+ label: 'Agent context-window awareness',
200
+ hint: 'Sub-agents estimate token usage before each call and compact past 85% of the model window. Default on.',
201
+ },
202
+ {
203
+ flagId: 'agent-passive-knowledge-injection',
204
+ label: 'Passive knowledge injection',
205
+ hint: 'Re-retrieve project-memory knowledge each turn against the evolving conversation, under a hard token budget with a per-turn record. Default on.',
206
+ },
207
+ {
208
+ flagId: 'agent-passive-code-injection',
209
+ label: 'Passive code injection',
210
+ hint: 'Also inject ranked chunks from the repo source-tree code index, sharing the same budget. Off by default; needs a built code index (storage.codeIndexEnabled).',
211
+ },
212
+ {
213
+ flagId: 'local-provider-context-ingestion',
214
+ label: 'Local provider context ingestion',
215
+ hint: 'Read the real max context length from local/custom provider /v1/models endpoints for token budgeting. Default on.',
216
+ },
217
+ ],
218
+ },
219
+ {
220
+ stepId: 'features-telemetry',
221
+ title: 'Telemetry and observability',
222
+ shortLabel: 'Telemetry',
223
+ description: 'Instrumentation and tool-result integrity checks. Nothing leaves the machine unless you enable remote export.',
224
+ units: [
225
+ {
226
+ flagId: 'otel-foundation',
227
+ label: 'OpenTelemetry foundation',
228
+ hint: 'In-process span creation and instrumentation. Required before remote export.',
229
+ },
230
+ {
231
+ flagId: 'otel-remote-export',
232
+ label: 'OTLP remote export',
233
+ hint: 'Export spans over OTLP/gRPC to a collector. Enabling this also turns on the OpenTelemetry foundation.',
234
+ requiresFlags: ['otel-foundation'],
235
+ impliedConfig: [{ key: 'telemetry.decisionOtlpEnabled' as ConfigKey, value: true }],
236
+ subOptions: [
237
+ {
238
+ key: 'endpoint',
239
+ configKey: 'telemetry.decisionOtlpEndpoint' as ConfigKey,
240
+ label: 'Collector endpoint',
241
+ hint: 'OTLP collector URL (e.g. http://localhost:4317). Leave blank to set it later in /settings.',
242
+ valueType: 'text',
243
+ defaultValue: '',
244
+ placeholder: 'http://localhost:4317',
245
+ },
246
+ ],
247
+ },
248
+ {
249
+ flagId: 'output-schema-fingerprint',
250
+ label: 'Output schema fingerprints',
251
+ hint: 'Append schema fingerprints to find/analyze/inspect tool results for drift detection.',
252
+ },
253
+ {
254
+ flagId: 'tool-contract-verification',
255
+ label: 'Tool contract verification',
256
+ hint: 'Registration-time contract checks for every tool (schema, timeout, permission mapping). Default on; invalid tools fail closed.',
257
+ },
258
+ {
259
+ flagId: 'tool-result-reconciliation',
260
+ label: 'Tool result reconciliation',
261
+ hint: 'Inject synthetic error results for dangling tool calls at turn end to prevent silent conversation corruption. Default on.',
262
+ },
263
+ ],
264
+ },
265
+ {
266
+ stepId: 'features-automation',
267
+ title: 'Automation and initiative',
268
+ shortLabel: 'Automation',
269
+ description: 'Durable automation, watchers, delivery reliability, execution planning, and outbound reachability.',
270
+ units: [
271
+ {
272
+ flagId: 'automation-domain',
273
+ label: 'Automation domain',
274
+ hint: 'Durable automation jobs/runs, schedule evaluation, and run history. Enabling this turns on automation.enabled.',
275
+ impliedConfig: [{ key: 'automation.enabled' as ConfigKey, value: true }],
276
+ },
277
+ {
278
+ flagId: 'watcher-framework',
279
+ label: 'Watcher framework',
280
+ hint: 'Managed watcher/listener services with checkpointing and recovery. Enabling this turns on watchers.enabled.',
281
+ impliedConfig: [{ key: 'watchers.enabled' as ConfigKey, value: true }],
282
+ },
283
+ {
284
+ flagId: 'integration-delivery-slo',
285
+ label: 'Integration delivery SLO',
286
+ hint: 'Retry with backoff and a dead-letter queue for Slack/Discord/webhook delivery. Exposes /notify dlq and /notify replay.',
287
+ subOptions: [
288
+ {
289
+ key: 'slo',
290
+ configKey: 'integrations.delivery.sloEnforced' as ConfigKey,
291
+ label: 'Enforce delivery SLO',
292
+ hint: 'Yes logs dead-letter events at error level and surfaces them in diagnostics; No is warn-level only (default).',
293
+ valueType: 'boolean',
294
+ defaultValue: 'no',
295
+ },
296
+ ],
297
+ },
298
+ {
299
+ flagId: 'adaptive-execution-planner',
300
+ label: 'Adaptive execution planner',
301
+ hint: 'Score single/cohort/background/remote strategies each turn and pick the best. Exposes /plan commands.',
302
+ },
303
+ {
304
+ flagId: 'relay-connect',
305
+ label: 'Outbound zero-knowledge relay',
306
+ hint: 'Let the daemon reach out to a self-hostable, end-to-end-encrypted relay so surfaces can reach it from outside the LAN. Enabling this turns on relay.enabled; set the relay URL below or later in /settings.',
307
+ impliedConfig: [{ key: 'relay.enabled' as ConfigKey, value: true }],
308
+ subOptions: [
309
+ {
310
+ key: 'url',
311
+ configKey: 'relay.url' as ConfigKey,
312
+ label: 'Relay URL',
313
+ hint: 'Your self-hosted relay endpoint (e.g. wss://relay.example.com). Leave blank to set it later; the daemon stays LAN-only until a URL is set.',
314
+ valueType: 'text',
315
+ defaultValue: '',
316
+ placeholder: 'wss://relay.example.com',
317
+ },
318
+ ],
319
+ },
320
+ ],
321
+ },
322
+ {
323
+ stepId: 'features-provider',
324
+ title: 'Provider and runtime behavior',
325
+ shortLabel: 'Runtime',
326
+ description: 'Provider routing optimization, tool-execution budgets, overflow handling, and notification suppression.',
327
+ units: [
328
+ {
329
+ flagId: 'provider-optimizer',
330
+ label: 'Provider optimizer',
331
+ hint: 'Capability-contract-driven provider routing with explainable route decisions. Exposes /provider route and related commands.',
332
+ subOptions: [
333
+ {
334
+ key: 'mode',
335
+ configKey: 'provider.optimizerMode' as ConfigKey,
336
+ label: 'Routing mode',
337
+ hint: 'manual (default), auto (pick the best capable provider), or pinned (a fixed model; set it in /settings).',
338
+ valueType: 'enum',
339
+ options: ['manual', 'auto', 'pinned'],
340
+ defaultValue: 'manual',
341
+ },
342
+ ],
343
+ },
344
+ {
345
+ flagId: 'runtime-tools-budget-enforcement',
346
+ label: 'Runtime budget enforcement',
347
+ hint: 'Enforce per-phase wall-clock, token, and cost budgets on tool pipelines, terminating on hard breach. Limits are tuned in /settings.',
348
+ },
349
+ {
350
+ flagId: 'overflow-spill-backends',
351
+ label: 'Overflow spill backends',
352
+ hint: 'Choose where overflow content spills when a tool result is too large.',
353
+ subOptions: [
354
+ {
355
+ key: 'backend',
356
+ configKey: 'tools.overflowSpillBackend' as ConfigKey,
357
+ label: 'Spill backend',
358
+ hint: 'file (default), ledger, or diagnostics.',
359
+ valueType: 'enum',
360
+ options: ['file', 'ledger', 'diagnostics'],
361
+ defaultValue: 'file',
362
+ },
363
+ ],
364
+ },
365
+ {
366
+ flagId: 'adaptive-notification-suppression',
367
+ label: 'Adaptive notification suppression',
368
+ hint: 'Suppress operational churn in quiet/minimal modes and collapse notification bursts. Burst timings are tuned in /settings.',
369
+ },
370
+ ],
371
+ },
372
+ {
373
+ stepId: 'features-advanced',
374
+ title: 'Advanced runtime toggles',
375
+ shortLabel: 'Advanced',
376
+ description: 'Internal/plumbing features with no tuning knobs. Most people can skip this step; the honest defaults are shown.',
377
+ units: [
378
+ { flagId: 'unified-runtime-task', label: 'Unified RuntimeTask', hint: 'Replace ad-hoc task tracking with the unified RuntimeTask interface. Startup-only.' },
379
+ { flagId: 'plugin-lifecycle', label: 'Plugin lifecycle', hint: 'Structured plugin init/teardown phases with health integration. Startup-only.' },
380
+ { flagId: 'mcp-lifecycle', label: 'MCP lifecycle', hint: 'Structured MCP connect/disconnect phases with health integration. Startup-only.' },
381
+ { flagId: 'permissions-simulation', label: 'Permissions simulation mode', hint: 'Dual-evaluator simulation that tracks divergence without changing enforcement. Startup-only.' },
382
+ { flagId: 'policy-signing', label: 'Policy bundle signing', hint: 'HMAC-SHA256 signature validation on policy bundle load. Startup-only.' },
383
+ ],
384
+ },
385
+ ];
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // Helpers
389
+ // ---------------------------------------------------------------------------
390
+
391
+ /** Wizard field id for a feature unit's enable toggle. */
392
+ export function featureEnableFieldId(flagId: string): string {
393
+ return `feature.${flagId}`;
394
+ }
395
+
396
+ /** Wizard field id for a feature unit's sub-option. */
397
+ export function featureSubOptionFieldId(flagId: string, key: string): string {
398
+ return `feature.${flagId}.${key}`;
399
+ }
400
+
401
+ /** Whether a flag defaults to enabled (drives the toggle's default-checked state). */
402
+ export function isFeatureDefaultOn(flagId: string): boolean {
403
+ return FEATURE_FLAG_MAP.get(flagId)?.defaultState === 'enabled';
404
+ }
405
+
406
+ /** Every flag id reachable through the guided feature steps. */
407
+ export function getFeatureOnboardingFlagIds(): readonly string[] {
408
+ return FEATURE_ONBOARDING_SECTIONS.flatMap((section) => section.units.map((unit) => unit.flagId));
409
+ }
410
+
411
+ function radioOptionsForSubOption(sub: FeatureSubOption): readonly OnboardingWizardRadioOption[] {
412
+ if (sub.valueType === 'boolean') return YES_NO_OPTIONS;
413
+ return (sub.options ?? []).map((id) => ({ id, label: id, hint: `Use ${id}.` }));
414
+ }
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Step builder
418
+ // ---------------------------------------------------------------------------
419
+
420
+ export function buildFeatureUnitStep(
421
+ controller: OnboardingWizardControllerLike,
422
+ section: FeatureSection,
423
+ ): OnboardingWizardStepDefinition {
424
+ const fields: OnboardingWizardFieldDefinition[] = [];
425
+ let enabledCount = 0;
426
+
427
+ for (const unit of section.units) {
428
+ const defaultOn = isFeatureDefaultOn(unit.flagId);
429
+ const enabled = controller.getBooleanFieldValue(featureEnableFieldId(unit.flagId), defaultOn);
430
+ if (enabled) enabledCount += 1;
431
+
432
+ fields.push({
433
+ kind: 'checklist',
434
+ id: featureEnableFieldId(unit.flagId),
435
+ label: unit.label,
436
+ hint: `${unit.hint}${defaultOn ? ' (on by default)' : ''} Full options: /settings.`,
437
+ defaultValue: defaultOn,
438
+ });
439
+
440
+ if (unit.note) {
441
+ fields.push({
442
+ kind: 'status',
443
+ id: `${featureEnableFieldId(unit.flagId)}.note`,
444
+ label: unit.note,
445
+ hint: unit.note,
446
+ defaultValue: 'Info',
447
+ });
448
+ }
449
+
450
+ for (const sub of unit.subOptions ?? []) {
451
+ if (sub.valueType === 'text') {
452
+ fields.push({
453
+ kind: 'text',
454
+ id: featureSubOptionFieldId(unit.flagId, sub.key),
455
+ label: sub.label,
456
+ hint: sub.hint,
457
+ placeholder: sub.placeholder ?? '',
458
+ defaultValue: sub.defaultValue,
459
+ });
460
+ continue;
461
+ }
462
+ fields.push({
463
+ kind: 'radio',
464
+ id: featureSubOptionFieldId(unit.flagId, sub.key),
465
+ label: sub.label,
466
+ hint: sub.hint,
467
+ options: radioOptionsForSubOption(sub),
468
+ defaultValue: sub.defaultValue,
469
+ });
470
+ }
471
+ }
472
+
473
+ return {
474
+ id: section.stepId,
475
+ title: section.title,
476
+ shortLabel: section.shortLabel,
477
+ description: section.description,
478
+ summaryTitle: `${section.title} selections`,
479
+ summaryLines: [
480
+ `${enabledCount}/${section.units.length} feature(s) enabled in this step`,
481
+ 'Skippable — the honest defaults above apply if you make no change.',
482
+ 'Full sub-option depth for every feature lives in /settings.',
483
+ ],
484
+ fields,
485
+ };
486
+ }
487
+
488
+ /** Build every guided feature step (order matches FEATURE_ONBOARDING_SECTIONS). */
489
+ export function buildFeatureUnitSteps(
490
+ controller: OnboardingWizardControllerLike,
491
+ ): readonly OnboardingWizardStepDefinition[] {
492
+ return FEATURE_ONBOARDING_SECTIONS.map((section) => buildFeatureUnitStep(controller, section));
493
+ }
494
+
495
+ // ---------------------------------------------------------------------------
496
+ // Apply
497
+ // ---------------------------------------------------------------------------
498
+
499
+ /**
500
+ * Fold the guided feature selections into apply operations: for each unit, when
501
+ * the user's choice differs from the flag's default, record the flag override;
502
+ * when the feature is enabled, write its implied config, prerequisite flags, and
503
+ * chosen sub-options. Flag overrides are collected into `overrides` (flushed by
504
+ * the caller with the surface flags); config writes go straight to setConfig.
505
+ */
506
+ export function applyFeatureUnitOperations(
507
+ controller: OnboardingWizardControllerLike,
508
+ setConfig: (key: ConfigKey, value: unknown) => void,
509
+ overrides: Map<string, 'enabled' | 'disabled'>,
510
+ ): void {
511
+ for (const section of FEATURE_ONBOARDING_SECTIONS) {
512
+ for (const unit of section.units) {
513
+ const defaultOn = isFeatureDefaultOn(unit.flagId);
514
+ const enabled = controller.getBooleanFieldValue(featureEnableFieldId(unit.flagId), defaultOn);
515
+
516
+ // Persist the flag only when the user's choice diverges from the default,
517
+ // so a first run that accepts the defaults writes no flag override.
518
+ if (enabled !== defaultOn) overrides.set(unit.flagId, enabled ? 'enabled' : 'disabled');
519
+ if (!enabled) continue;
520
+
521
+ for (const prerequisite of unit.requiresFlags ?? []) overrides.set(prerequisite, 'enabled');
522
+ for (const implied of unit.impliedConfig ?? []) setConfig(implied.key, implied.value);
523
+
524
+ for (const sub of unit.subOptions ?? []) {
525
+ const raw = controller.getStringFieldValue(featureSubOptionFieldId(unit.flagId, sub.key), sub.defaultValue);
526
+ if (sub.valueType === 'boolean') {
527
+ setConfig(sub.configKey, raw === 'yes');
528
+ } else if (sub.valueType === 'text') {
529
+ if (raw.length > 0) setConfig(sub.configKey, raw);
530
+ } else {
531
+ setConfig(sub.configKey, raw);
532
+ }
533
+ }
534
+ }
535
+ }
536
+ }
@@ -15,11 +15,25 @@ import {
15
15
  getExternalSurfaceAutoStartFieldId,
16
16
  isExternalSurfaceSelectedByDefault,
17
17
  } from './onboarding-wizard-external-surfaces.ts';
18
+ import { applyFeatureUnitOperations } from './onboarding-feature-units.ts';
18
19
  import { buildGoodVibesSecretKey, buildGoodVibesSecretRef, isLoopbackAddress, isSecretReferenceValue } from './onboarding-wizard-helpers.ts';
19
20
  import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
20
21
 
22
+ /** SDK schema default for behavior.hitlMode (schema-domain-core DEFAULT_CONFIG). */
23
+ const HITL_MODE_SCHEMA_DEFAULT = 'balanced';
24
+ /** Feature flag that gates behavior.hitlMode preset application in ModeManager. */
25
+ const HITL_UX_MODES_FEATURE_FLAG = 'hitl-ux-modes';
26
+
21
27
  export function buildOnboardingApplyRequest(controller: OnboardingWizardControllerLike): OnboardingApplyRequest {
22
28
  const operations: OnboardingApplyOperation[] = [];
29
+ // Accumulates feature-flag state that onboarding selections require, so the
30
+ // written config actually takes effect (e.g. behavior.hitlMode is inert unless
31
+ // hitl-ux-modes is on) and so a default-on feature the user turned OFF in a
32
+ // feature step is persisted as a disabled override. Flushed once, after the
33
+ // surface flags, at the end of the apply build so every gating flag is set in
34
+ // one batch. A later write for the same flag id wins (surface enables are
35
+ // authoritative for the network-exposing flags they own).
36
+ const featureFlagOverrides = new Map<string, 'enabled' | 'disabled'>();
23
37
  const hasServers = controller.hasServerCapabilitiesSelected();
24
38
  const browserAccess = controller.shouldEnableBrowserSurface();
25
39
  const httpListener = controller.shouldEnableHttpListener();
@@ -34,9 +48,6 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
34
48
  ): void => {
35
49
  operations.push({ kind: 'set-config', key, value });
36
50
  };
37
- const enableFeatureFlags = (flagIds: readonly string[]): void => {
38
- for (const flagId of flagIds) setConfig(`featureFlags.${flagId}`, 'enabled');
39
- };
40
51
  const acknowledge = (target: OnboardingAcknowledgementTarget, fieldId: string): void => {
41
52
  operations.push({
42
53
  kind: 'acknowledge',
@@ -119,7 +130,18 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
119
130
  setConfig('provider.model', formatProviderModel(defaultModel.providerId, defaultModel.modelId));
120
131
  }
121
132
  setConfig('provider.reasoningEffort', controller.getStringFieldValue('default-model.reasoning', controller.runtimeSnapshot?.providerRouting.primaryReasoningEffort ?? 'medium'));
122
- setConfig('behavior.hitlMode', controller.getStringFieldValue('experience.hitl', controller.runtimeSnapshot?.runtimeDefaults.behavior.hitlMode ?? 'balanced'));
133
+ const hitlMode = controller.getStringFieldValue('experience.hitl', controller.runtimeSnapshot?.runtimeDefaults.behavior.hitlMode ?? 'balanced');
134
+ setConfig('behavior.hitlMode', hitlMode);
135
+ // behavior.hitlMode is gated by the hitl-ux-modes feature flag: ModeManager
136
+ // only applies the chosen preset to the notification router when the flag is
137
+ // on. Writing hitlMode without enabling the flag leaves the user's choice
138
+ // silently inert. When the user picks a non-default mode, enable the gating
139
+ // flag in the same apply batch so the selection actually takes effect. The
140
+ // SDK schema default is 'balanced' (schema-domain-core DEFAULT_CONFIG); a
141
+ // choice equal to the default is left to the flag's own default-off state.
142
+ if (hitlMode !== HITL_MODE_SCHEMA_DEFAULT) {
143
+ featureFlagOverrides.set(HITL_UX_MODES_FEATURE_FLAG, 'enabled');
144
+ }
123
145
  setConfig('behavior.guidanceMode', controller.getStringFieldValue('experience.guidance', controller.runtimeSnapshot?.runtimeDefaults.behavior.guidanceMode ?? 'minimal'));
124
146
  setConfig('permissions.mode', controller.getStringFieldValue('experience.permissions', controller.runtimeSnapshot?.runtimeDefaults.permissionsMode ?? 'prompt'));
125
147
  setConfig('storage.secretPolicy', controller.getStringFieldValue('external-services.secret-policy', controller.runtimeSnapshot?.runtimeDefaults.secretStoragePolicy ?? 'preferred_secure'));
@@ -167,11 +189,23 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
167
189
  else setConfig(setupField.configKey, value);
168
190
  }
169
191
  }
170
- enableFeatureFlags(getServerSurfaceFeatureFlags({
192
+ // Feature units from the guided feature steps (safety, memory & context,
193
+ // telemetry, automation, provider, advanced): enable/disable each flag the
194
+ // user changed from its default and write its chosen sub-options.
195
+ applyFeatureUnitOperations(controller, setConfig, featureFlagOverrides);
196
+
197
+ // Surface/server flags are authoritative for the network-exposing flags they
198
+ // own — apply them last so they win over any feature-step override.
199
+ for (const surfaceFlag of getServerSurfaceFeatureFlags({
171
200
  serverBacked: hasServers,
172
201
  web: browserAccess,
173
202
  externalSurfaces: enabledExternalSurfaceIds,
174
- }));
203
+ })) {
204
+ featureFlagOverrides.set(surfaceFlag, 'enabled');
205
+ }
206
+ for (const flagId of [...featureFlagOverrides.keys()].sort((left, right) => left.localeCompare(right))) {
207
+ setConfig(`featureFlags.${flagId}`, featureFlagOverrides.get(flagId)!);
208
+ }
175
209
 
176
210
  acknowledge('providers', 'providers.reviewed');
177
211
  acknowledge('subscriptions', 'accounts.subscriptions');