@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -104,8 +104,8 @@
104
104
  "@anthropic-ai/vertex-sdk": "^0.16.0",
105
105
  "@ast-grep/napi": "^0.42.0",
106
106
  "@aws/bedrock-token-generator": "^1.1.0",
107
- "@pellux/goodvibes-sdk": "1.7.1",
108
- "@pellux/goodvibes-terminal-shell": "1.7.1",
107
+ "@pellux/goodvibes-sdk": "1.8.0",
108
+ "@pellux/goodvibes-terminal-shell": "1.8.0",
109
109
  "bash-language-server": "^5.6.0",
110
110
  "fuse.js": "^7.1.0",
111
111
  "graphql": "^16.13.2",
@@ -131,10 +131,10 @@
131
131
  "typescript": "^5.9.3"
132
132
  },
133
133
  "optionalDependencies": {
134
- "@pellux/goodvibes-tui-linux-x64": "1.16.1",
135
- "@pellux/goodvibes-tui-linux-arm64": "1.16.1",
136
- "@pellux/goodvibes-tui-darwin-x64": "1.16.1",
137
- "@pellux/goodvibes-tui-darwin-arm64": "1.16.1"
134
+ "@pellux/goodvibes-tui-linux-x64": "1.17.0",
135
+ "@pellux/goodvibes-tui-linux-arm64": "1.17.0",
136
+ "@pellux/goodvibes-tui-darwin-x64": "1.17.0",
137
+ "@pellux/goodvibes-tui-darwin-arm64": "1.17.0"
138
138
  },
139
139
  "trustedDependencies": [],
140
140
  "overrides": {
@@ -93,8 +93,8 @@ export const GLOBAL_FLAGS: readonly CompletionFlag[] = [
93
93
  takesValue: true,
94
94
  description: 'Override a config value (key=value)',
95
95
  },
96
- { name: '--enable', takesValue: true, description: 'Enable a feature flag for this launch' },
97
- { name: '--disable', takesValue: true, description: 'Disable a feature flag for this launch' },
96
+ { name: '--enable', takesValue: true, description: 'Enable a platform feature for this launch (writes its domain setting)' },
97
+ { name: '--disable', takesValue: true, description: 'Disable a platform feature for this launch (writes its domain setting)' },
98
98
  {
99
99
  name: '--prompt',
100
100
  short: '-p',
@@ -139,6 +139,7 @@ export const GLOBAL_FLAGS: readonly CompletionFlag[] = [
139
139
  { name: '--fork', takesValue: false, description: 'Fork session when supported' },
140
140
  { name: '--yes', short: '-y', takesValue: false, description: 'Auto-confirm prompts (non-interactive)' },
141
141
  { name: '--non-interactive', takesValue: false, description: 'Disable all interactive prompts (implies --yes)' },
142
+ { name: '--strict', takesValue: false, description: 'doctor: also fail on advisory findings, for CI' },
142
143
  ] as const;
143
144
 
144
145
  // ---------------------------------------------------------------------------
@@ -504,8 +505,8 @@ export function generateZsh(surface: CompletionSurface): string {
504
505
  lines.push(` '--working-dir[Alias for --cd]:dir:_files -/' \\`);
505
506
  lines.push(` '--daemon-home[Override daemon home]:dir:_files -/' \\`);
506
507
  lines.push(` '(-c --config)'{-c,--config}'[Override config value]:key=value:' \\`);
507
- lines.push(` '--enable[Enable feature flag]:feature:' \\`);
508
- lines.push(` '--disable[Disable feature flag]:feature:' \\`);
508
+ lines.push(` '--enable[Enable platform feature]:feature:' \\`);
509
+ lines.push(` '--disable[Disable platform feature]:feature:' \\`);
509
510
  lines.push(` '(-p --prompt)'{-p,--prompt}'[Run non-interactive prompt]:text:' \\`);
510
511
  lines.push(` '--print[Non-interactive run mode]' \\`);
511
512
  lines.push(` '(-o --output --output-format)'{-o,--output}'[Output format]:format:(text json stream-json)' \\`);
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import type { ConfigKey, ConfigManager, ConfigSetting, GoodVibesConfig, PersistedFlagState } from '../config/index.ts';
2
+ import type { ConfigKey, ConfigManager, ConfigSetting, GoodVibesConfig } from '../config/index.ts';
3
3
  import { CONFIG_SCHEMA, ConfigError } from '../config/index.ts';
4
+ import { featureEnablementWrite, getFeatureSetting } from '../runtime/feature-settings.ts';
4
5
  import type { GoodVibesCliCommand, GoodVibesCliFlags } from './types.ts';
5
6
  import { RUNTIME_ENDPOINT_CONFIG_KEYS, hostModeForHostname } from './endpoints.ts';
6
7
  import type { RuntimeEndpointId } from './endpoints.ts';
@@ -130,6 +131,32 @@ export function applyRuntimeConfigDefault(configManager: ConfigManager, key: Con
130
131
  applyRuntimeConfigValue(configManager, key, defaultValue);
131
132
  }
132
133
 
134
+ /**
135
+ * Every TUI-side config default applied at startup, in one place. Currently:
136
+ * show token speed ON (the SDK schema default is false). applyRuntimeConfigDefault
137
+ * reads the global + project settings files and only applies the default
138
+ * in-memory when the key is absent from both (e.g. a new install); an explicit
139
+ * user value is respected. No disk write.
140
+ */
141
+ export function applyTuiRuntimeConfigDefaults(configManager: ConfigManager): void {
142
+ applyRuntimeConfigDefault(configManager, 'display.showTokenSpeed', true);
143
+ }
144
+
145
+ /**
146
+ * Applies the persisted `behavior.hitlMode` config value to the live mode
147
+ * manager at startup, ignoring unset or unrecognized values. Extracted from
148
+ * main() verbatim (a plain move, no behavior change).
149
+ */
150
+ export function applyConfiguredHitlMode(
151
+ configManager: ConfigManager,
152
+ modeManager: { setHITLMode(mode: 'quiet' | 'balanced' | 'operator'): void },
153
+ ): void {
154
+ const hitlMode = configManager.get('behavior.hitlMode');
155
+ if (hitlMode === 'quiet' || hitlMode === 'balanced' || hitlMode === 'operator') {
156
+ modeManager.setHITLMode(hitlMode);
157
+ }
158
+ }
159
+
133
160
  export function applyRuntimeConfigValue(configManager: ConfigManager, key: ConfigKey, value: unknown): void {
134
161
  const setting = CONFIG_SCHEMA_BY_KEY.get(key);
135
162
  if (!setting) {
@@ -161,23 +188,36 @@ export function applyRuntimeConfigOverrides(
161
188
  return errors;
162
189
  }
163
190
 
191
+ /**
192
+ * Session-only feature overrides (--enable-feature / --disable-feature).
193
+ * Each feature is switched through its real domain settings key (e.g.
194
+ * sandbox.enabled, behavior.compactionStrategy) in the runtime config layer;
195
+ * features without an off position (constant capabilities on non-boolean
196
+ * keys) and unknown ids are reported as errors rather than silently ignored.
197
+ */
164
198
  export function applyRuntimeFeatureFlagOverrides(
165
199
  configManager: ConfigManager,
166
200
  options: {
167
201
  readonly enableFeatures: readonly string[];
168
202
  readonly disableFeatures: readonly string[];
169
203
  },
170
- ): void {
171
- if (options.enableFeatures.length === 0 && options.disableFeatures.length === 0) return;
204
+ ): readonly string[] {
205
+ if (options.enableFeatures.length === 0 && options.disableFeatures.length === 0) return [];
172
206
  const config = getRuntimeConfig(configManager);
173
- const flags = { ...config.featureFlags };
174
- for (const feature of options.enableFeatures) {
175
- flags[feature] = 'enabled' satisfies PersistedFlagState;
176
- }
177
- for (const feature of options.disableFeatures) {
178
- flags[feature] = 'disabled' satisfies PersistedFlagState;
179
- }
180
- config.featureFlags = flags;
207
+ const errors: string[] = [];
208
+ const apply = (feature: string, enabled: boolean, flagName: string): void => {
209
+ const write = featureEnablementWrite(feature, enabled);
210
+ if (!write) {
211
+ errors.push(getFeatureSetting(feature)
212
+ ? `${flagName} ${feature}: this capability has no ${enabled ? 'on' : 'off'} switch (its domain settings govern it directly).`
213
+ : `${flagName} ${feature}: unknown feature id.`);
214
+ return;
215
+ }
216
+ setNestedConfigValue(config, write.key, write.value);
217
+ };
218
+ for (const feature of options.enableFeatures) apply(feature, true, '--enable-feature');
219
+ for (const feature of options.disableFeatures) apply(feature, false, '--disable-feature');
220
+ return errors;
181
221
  }
182
222
 
183
223
  export function applyRuntimeEndpointFlagOverrides(
package/src/cli/doctor.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
2
2
  import { PermissionManager, createPermissionConfigReader } from '@pellux/goodvibes-sdk/platform/permissions';
3
3
  import type { PermissionCategory, PermissionCheckResult } from '@pellux/goodvibes-sdk/platform/permissions';
4
- import { PolicyRuntimeState, createFeatureFlagManager } from '@/runtime/index.ts';
5
- import type { FlagState } from '@/runtime/index.ts';
4
+ import { PolicyRuntimeState, createFeatureFlagManager, deriveFeatureStates } from '@/runtime/index.ts';
6
5
  import { getProviderIdFromModel } from '../config/provider-model.ts';
7
6
  import type { CliCommandOutput } from './types.ts';
8
7
  import type { GoodVibesCliOutputFormat } from './types.ts';
@@ -76,7 +75,7 @@ function resolveExplainTarget(args: readonly string[]): { tool: string; args: Re
76
75
  const LAYER_NAMES: readonly string[] = [
77
76
  'Auto-approve override (behavior.autoApprove / --no-worries-just-vibes)',
78
77
  'Session permission mode (permissions.mode)',
79
- 'Runtime policy engine (feature flag: permissions-policy-engine)',
78
+ 'Runtime policy engine (permissions.engine = policy-engine)',
80
79
  'Per-tool config rule (permissions.tools.<tool>)',
81
80
  'Default read auto-approve (prompt mode)',
82
81
  'Session approval cache (remembered [A] decisions)',
@@ -164,7 +163,7 @@ async function explain(options: DoctorSubcommandOptions): Promise<CliCommandOutp
164
163
  // Feature flags come from live config so the policy engine layer is walked
165
164
  // exactly when the running app would walk it.
166
165
  const featureFlags = createFeatureFlagManager();
167
- featureFlags.loadFromConfig({ flags: (config.getCategory('featureFlags') as Record<string, FlagState>) ?? {} });
166
+ featureFlags.loadFromConfig({ flags: deriveFeatureStates(config) });
168
167
  const policyEngineOn = featureFlags.isEnabled('permissions-policy-engine');
169
168
  const policyRuntimeState = new PolicyRuntimeState();
170
169
  const manager = new PermissionManager(
@@ -214,7 +213,7 @@ async function explain(options: DoctorSubcommandOptions): Promise<CliCommandOutp
214
213
  'Current settings:',
215
214
  ` mode : ${mode} (${permissionModeLabel(mode)})`,
216
215
  ` autoApprove : ${autoApprove ? 'yes (behavior.autoApprove)' : 'no'}`,
217
- ` policyEngine: ${policyEngineOn ? 'enabled' : 'disabled'} (feature flag permissions-policy-engine)`,
216
+ ` policyEngine: ${policyEngineOn ? 'enabled' : 'disabled'} (permissions.engine)`,
218
217
  '',
219
218
  'Analysis (from the platform analyzer):',
220
219
  ` risk : ${a.riskLevel.toUpperCase()} (${a.classification})`,
@@ -21,6 +21,7 @@ import {
21
21
  renderGoodVibesHelp,
22
22
  renderGoodVibesVersion,
23
23
  renderOnboardingCliStatus,
24
+ resolveDoctorExitCode,
24
25
  } from './index.ts';
25
26
  import { buildCliServicePosture, getGoodVibesPackageRoot, resolveGoodVibesDaemonExecutable } from './service-posture.ts';
26
27
  import { runInstallSelfCheck } from '../runtime/install-self-check.ts';
@@ -28,8 +29,7 @@ import { readPersistedWorkspaceTrust } from '../runtime/trust/workspace-trust.ts
28
29
  import { WorkspaceRegistrationManager } from '../runtime/trust/workspace-registration.ts';
29
30
  import type { CliWorkspaceStatus, CliSandboxStatus, CliRelayStatus } from './status.ts';
30
31
  import { detectSandboxAvailability, probeSandboxHost } from '@pellux/goodvibes-sdk/platform/tools/exec/sandbox';
31
- import { createFeatureFlagManager } from '@/runtime/index.ts';
32
- import type { FlagState } from '@/runtime/index.ts';
32
+ import { createFeatureFlagManager, deriveFeatureStates } from '@/runtime/index.ts';
33
33
  import { ensureGoodvibesGitignore } from './ensure-goodvibes-gitignore.ts';
34
34
 
35
35
  type ShellEntrypointOwnership = {
@@ -117,10 +117,14 @@ export async function prepareShellCliRuntime(
117
117
  console.error(overrideErrors.join('\n'));
118
118
  process.exit(2);
119
119
  }
120
- applyRuntimeFeatureFlagOverrides(configManager, {
120
+ const featureOverrideErrors = applyRuntimeFeatureFlagOverrides(configManager, {
121
121
  enableFeatures: cli.flags.enableFeatures,
122
122
  disableFeatures: cli.flags.disableFeatures,
123
123
  });
124
+ if (featureOverrideErrors.length > 0) {
125
+ console.error(featureOverrideErrors.join('\n'));
126
+ process.exit(2);
127
+ }
124
128
 
125
129
  if (cli.flags.provider !== undefined || cli.flags.model !== undefined) {
126
130
  const currentModel = configManager.get('provider.model');
@@ -181,9 +185,7 @@ export async function prepareShellCliRuntime(
181
185
  // Linux, a no-op elsewhere) is what makes "available" trustworthy.
182
186
  const sandboxAvailability = detectSandboxAvailability(probeSandboxHost());
183
187
  const sandboxFeatureFlags = createFeatureFlagManager();
184
- sandboxFeatureFlags.loadFromConfig({
185
- flags: (configManager.getCategory('featureFlags') as Record<string, FlagState>) ?? {},
186
- });
188
+ sandboxFeatureFlags.loadFromConfig({ flags: deriveFeatureStates(configManager) });
187
189
  const sandboxStatus: CliSandboxStatus = {
188
190
  configEnabled: Boolean(configManager.getCategory('sandbox').enabled),
189
191
  featureEnabled: sandboxFeatureFlags.isEnabled('exec-sandbox'),
@@ -192,7 +194,7 @@ export async function prepareShellCliRuntime(
192
194
  reason: sandboxAvailability.reason,
193
195
  networkIsolationGuaranteed: sandboxAvailability.networkIsolationGuaranteed,
194
196
  };
195
- // Reuses sandboxFeatureFlags (already loaded from the same featureFlags config category)
197
+ // Reuses sandboxFeatureFlags (already derived from the same domain settings keys)
196
198
  // for the relay-connect gate rather than constructing a second FeatureFlagManager.
197
199
  const relayCategory = configManager.getCategory('relay');
198
200
  const relayStatus: CliRelayStatus = {
@@ -232,7 +234,10 @@ export async function prepareShellCliRuntime(
232
234
  console.log(cli.command === 'onboarding'
233
235
  ? renderOnboardingCliStatus(statusOptions)
234
236
  : renderCliStatus(statusOptions));
235
- process.exit(cli.command === 'doctor' && snapshot.findings.length > 0 ? 1 : 0);
237
+ // Advisory findings are notes on an otherwise-usable install and must
238
+ // never make a healthy install report failure — only a must-fix finding
239
+ // exits non-zero; --strict (for CI) flips advisories to failures too.
240
+ process.exit(cli.command === 'doctor' ? resolveDoctorExitCode(snapshot.findings, cli.flags.strict) : 0);
236
241
  }
237
242
 
238
243
  const cliCommandResult = await handleGoodVibesCliCommand({
package/src/cli/help.ts CHANGED
@@ -68,8 +68,8 @@ export function renderGoodVibesHelp(binary = 'goodvibes'): string {
68
68
  ' --working-dir <dir> Alias for --cd',
69
69
  ' --daemon-home <dir> Override daemon home for daemon-backed commands',
70
70
  ' -c, --config <key=value> Override a config value for this launch',
71
- ' --enable <feature> Enable a feature flag for this launch',
72
- ' --disable <feature> Disable a feature flag for this launch',
71
+ ' --enable <feature> Enable a platform feature for this launch',
72
+ ' --disable <feature> Disable a platform feature for this launch',
73
73
  ' -p, --prompt <text> Run a non-interactive prompt',
74
74
  ' --print Alias for non-interactive run mode',
75
75
  ' -o, --output <format> text, json, or stream-json',
@@ -85,6 +85,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes'): string {
85
85
  ' --fork [id] Fork session (current or specific id) when supported',
86
86
  ' -y, --yes Auto-confirm prompts (non-interactive)',
87
87
  ' --non-interactive Disable all interactive prompts (implies --yes)',
88
+ ' --strict doctor: also fail on advisory findings, for CI',
88
89
  ' -h, --help Print help',
89
90
  ' -v, --version Print version',
90
91
  '',
@@ -139,9 +140,9 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
139
140
  examples: ['status', 'status --json'],
140
141
  },
141
142
  doctor: {
142
- usage: ['doctor', 'doctor --json', 'doctor explain <tool|command>', 'doctor routing', 'doctor hooks'],
143
- summary: 'Print status plus setup warnings, or explain a platform decision: explain (why a tool/command would be allowed/asked/denied under the current mode), routing (which model/provider serves which role), hooks (registered hooks, their source, and validation status).',
144
- examples: ['doctor', 'doctor explain "rm -rf build"', 'doctor explain write ./src/app.ts', 'doctor routing', 'doctor hooks --json'],
143
+ usage: ['doctor', 'doctor --json', 'doctor --strict', 'doctor explain <tool|command>', 'doctor routing', 'doctor hooks'],
144
+ summary: 'Print status plus setup warnings, or explain a platform decision: explain (why a tool/command would be allowed/asked/denied under the current mode), routing (which model/provider serves which role), hooks (registered hooks, their source, and validation status). Exits 0 when the install is usable (advisory findings render as notes); non-zero only for a must-fix finding. --strict also fails on advisory findings, for CI.',
145
+ examples: ['doctor', 'doctor --strict', 'doctor explain "rm -rf build"', 'doctor explain write ./src/app.ts', 'doctor routing', 'doctor hooks --json'],
145
146
  },
146
147
  providers: {
147
148
  usage: ['providers [list]', 'providers current', 'providers inspect <provider>', 'providers use <provider> [modelRegistryKey]'],
@@ -311,7 +312,7 @@ export function renderGoodVibesDaemonHelp(binary = 'goodvibes-daemon'): string {
311
312
  ' install-service Install + enable the daemon as a systemd user service (survives reboots)',
312
313
  ' uninstall-service Disable + remove the daemon systemd user service',
313
314
  ' service-status Show whether the daemon service is installed / enabled / active',
314
- ' migrate-service Guided takeover of a legacy goodvibes-daemon.service unit; prints a plan',
315
+ ' migrate-service Guided migration from an install-script goodvibes-daemon.service unit; prints a plan',
315
316
  ' unless run with -y/--yes (never auto-migrates)',
316
317
  '',
317
318
  'Options:',
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Launch-time self-update — clients land on the newest release at startup so
3
+ * an installed binary never drifts behind. At TUI launch (before any runtime
4
+ * bootstrap or terminal mode change) this runs a quick version check and, when
5
+ * a newer release exists, installs it through the SAME checksum-verified
6
+ * download/verify/swap path `/update apply` and scripts/install.sh use
7
+ * (src/input/commands/update-runtime.ts — there is deliberately no second
8
+ * updater), then asks the caller to restart onto the new binary.
9
+ *
10
+ * Honesty rules, in both directions:
11
+ * - the check gets a short timeout; when it cannot complete (offline, slow
12
+ * network) the CURRENT version starts with exactly one line saying the
13
+ * check was skipped — launch is never held hostage by the network.
14
+ * - a successful update restarts into the new binary and the restarted
15
+ * process prints a receipt naming both versions, so the swap is never
16
+ * silent.
17
+ * - every swap keeps the outgoing file at `<path>.previous`; rollback is
18
+ * one command (`/update rollback`).
19
+ *
20
+ * The feature is a real configurable setting (`update.autoUpdateAtLaunch` in
21
+ * settings.json, read via readUpdateSettings): default ON, off with an
22
+ * explicit false. Only binary installs self-update — package-manager and
23
+ * from-source runs are skipped silently because a swap there would fight the
24
+ * package manager (see detectInstallKind).
25
+ *
26
+ * Everything effectful is injectable; tests drive the decision logic with a
27
+ * stubbed fetch, a stubbed apply, and pinned fixture versions — never the
28
+ * live build VERSION and never the real network.
29
+ */
30
+ import { spawnSync } from 'node:child_process';
31
+ import { detectInstallKind, normalizeVersion, type UpdateFetchLike } from '../runtime/update-check.ts';
32
+ import { applyUpdate, checkForUpdate, type ApplyUpdateOptions } from '../input/commands/update-runtime.ts';
33
+ import { readUpdateSettings, type UpdateSettings } from '../config/tui-extension-settings.ts';
34
+ import type { ConfigManager } from '../config/index.ts';
35
+ import { VERSION } from '../version.ts';
36
+
37
+ /**
38
+ * Set on the restarted process by restartOntoUpdatedBinary, carrying the
39
+ * version that performed the update. Its presence means "an update already
40
+ * happened this launch": the restarted process prints the receipt and skips
41
+ * its own check, which also makes an update-that-didn't-change-the-version
42
+ * structurally unable to restart-loop.
43
+ */
44
+ export const LAUNCH_UPDATED_FROM_ENV = 'GOODVIBES_LAUNCH_UPDATED_FROM';
45
+
46
+ /** Default budget for the launch-time version check; user-tunable via update.launchCheckTimeoutMs. */
47
+ export const LAUNCH_UPDATE_CHECK_TIMEOUT_MS = 2500;
48
+
49
+ export type LaunchAutoUpdateOutcome =
50
+ | {
51
+ readonly action: 'continue';
52
+ readonly reason: 'just-updated' | 'disabled' | 'not-swappable-install' | 'already-current' | 'check-skipped' | 'update-failed';
53
+ }
54
+ | { readonly action: 'restart'; readonly latestTag: string };
55
+
56
+ export interface RunLaunchAutoUpdateOptions {
57
+ readonly fetchImpl: UpdateFetchLike;
58
+ readonly execPath: string;
59
+ readonly platform: NodeJS.Platform;
60
+ readonly arch: string;
61
+ readonly currentVersion: string;
62
+ readonly settings: UpdateSettings;
63
+ readonly env: NodeJS.ProcessEnv;
64
+ readonly print: (line: string) => void;
65
+ readonly configManager: { get(key: string): unknown };
66
+ /** Injectable so tests observe the install step instead of swapping real files. */
67
+ readonly apply?: (options: ApplyUpdateOptions) => Promise<void>;
68
+ readonly timeoutMs?: number;
69
+ }
70
+
71
+ /** Resolves to 'timeout' when `promise` does not settle within `ms`; the timer never keeps the process alive. */
72
+ async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | 'timeout'> {
73
+ let timer: ReturnType<typeof setTimeout> | undefined;
74
+ try {
75
+ return await Promise.race([
76
+ promise,
77
+ new Promise<'timeout'>((resolve) => {
78
+ timer = setTimeout(() => resolve('timeout'), ms);
79
+ timer.unref?.();
80
+ }),
81
+ ]);
82
+ } finally {
83
+ clearTimeout(timer);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * The launch-time decision: receipt-and-continue (restarted process), skip
89
+ * (disabled / unswappable / current / unreachable), install-and-restart, or
90
+ * fail-and-continue on the current version. Never throws.
91
+ */
92
+ export async function runLaunchAutoUpdate(options: RunLaunchAutoUpdateOptions): Promise<LaunchAutoUpdateOutcome> {
93
+ const updatedFrom = options.env[LAUNCH_UPDATED_FROM_ENV];
94
+ if (typeof updatedFrom === 'string' && updatedFrom.length > 0) {
95
+ // Consumed here so sessions spawned from inside this one never inherit it.
96
+ delete options.env[LAUNCH_UPDATED_FROM_ENV];
97
+ options.print(`auto-update: updated from v${normalizeVersion(updatedFrom)} to v${normalizeVersion(options.currentVersion)} at launch`);
98
+ return { action: 'continue', reason: 'just-updated' };
99
+ }
100
+
101
+ if (!(options.settings.autoUpdateAtLaunch ?? true)) {
102
+ return { action: 'continue', reason: 'disabled' };
103
+ }
104
+
105
+ if (detectInstallKind(options.execPath) !== 'binary') {
106
+ return { action: 'continue', reason: 'not-swappable-install' };
107
+ }
108
+
109
+ const timeoutMs = options.timeoutMs ?? options.settings.launchCheckTimeoutMs ?? LAUNCH_UPDATE_CHECK_TIMEOUT_MS;
110
+ let check: Awaited<ReturnType<typeof checkForUpdate>> | 'timeout';
111
+ try {
112
+ check = await withTimeout(checkForUpdate(options.fetchImpl, options.currentVersion), timeoutMs);
113
+ } catch {
114
+ check = 'timeout';
115
+ }
116
+ if (check === 'timeout') {
117
+ options.print('update check skipped: offline');
118
+ return { action: 'continue', reason: 'check-skipped' };
119
+ }
120
+ if (check.isCurrent) {
121
+ return { action: 'continue', reason: 'already-current' };
122
+ }
123
+
124
+ try {
125
+ const apply = options.apply ?? applyUpdate;
126
+ await apply({
127
+ fetchImpl: options.fetchImpl,
128
+ execPath: options.execPath,
129
+ platform: options.platform,
130
+ arch: options.arch,
131
+ currentVersion: options.currentVersion,
132
+ print: options.print,
133
+ configManager: options.configManager,
134
+ });
135
+ options.print(`auto-update: ${check.latestTag} installed — restarting onto the new version`);
136
+ return { action: 'restart', latestTag: check.latestTag };
137
+ } catch (error) {
138
+ options.print(
139
+ `auto-update failed: ${error instanceof Error ? error.message : String(error)} — starting the current version v${normalizeVersion(options.currentVersion)}`,
140
+ );
141
+ return { action: 'continue', reason: 'update-failed' };
142
+ }
143
+ }
144
+
145
+ export interface SelfUpdateAtLaunchParams {
146
+ readonly configManager: Pick<ConfigManager, 'get' | 'getRaw'>;
147
+ readonly stdout: { write(chunk: string): unknown };
148
+ }
149
+
150
+ /**
151
+ * The complete launch wiring for main(), with real host inputs: run the
152
+ * decision above; when an update was installed, restart onto the swapped
153
+ * binary and EXIT THIS PROCESS with the new instance's exit code (this call
154
+ * never returns in that case); otherwise return the honest lines that were
155
+ * printed (receipt / skipped check / failed update) so the caller can
156
+ * re-surface them through the system message router once it exists — the
157
+ * stdout copies written here are wiped by the TUI's alternate screen.
158
+ */
159
+ export async function selfUpdateAtLaunch(params: SelfUpdateAtLaunchParams): Promise<readonly string[]> {
160
+ const lines: string[] = [];
161
+ const outcome = await runLaunchAutoUpdate({
162
+ fetchImpl: fetch as UpdateFetchLike,
163
+ execPath: process.execPath,
164
+ platform: process.platform,
165
+ arch: process.arch,
166
+ currentVersion: VERSION,
167
+ settings: readUpdateSettings(params.configManager),
168
+ env: process.env,
169
+ configManager: params.configManager,
170
+ print: (line) => {
171
+ lines.push(line);
172
+ params.stdout.write(`${line}\n`);
173
+ },
174
+ });
175
+ if (outcome.action === 'restart') {
176
+ process.exit(
177
+ restartOntoUpdatedBinary({
178
+ execPath: process.execPath,
179
+ argv: process.argv.slice(2),
180
+ env: process.env,
181
+ fromVersion: VERSION,
182
+ }),
183
+ );
184
+ }
185
+ return lines;
186
+ }
187
+
188
+ export interface RestartOntoUpdatedBinaryOptions {
189
+ readonly execPath: string;
190
+ readonly argv: readonly string[];
191
+ readonly env: NodeJS.ProcessEnv;
192
+ /** The version that performed the update — the restarted process prints it in its receipt line. */
193
+ readonly fromVersion: string;
194
+ /** Injectable so tests observe the restart instead of spawning a process. */
195
+ readonly spawn?: (execPath: string, argv: readonly string[], env: NodeJS.ProcessEnv) => number | null;
196
+ }
197
+
198
+ /**
199
+ * Runs the (just-swapped) binary at execPath with the original CLI arguments
200
+ * and an inherited terminal, blocking until it exits, and returns its exit
201
+ * code for the caller to process.exit with. The receipt marker travels in the
202
+ * child's environment, never on the command line.
203
+ */
204
+ export function restartOntoUpdatedBinary(options: RestartOntoUpdatedBinaryOptions): number {
205
+ const spawn =
206
+ options.spawn ??
207
+ ((execPath: string, argv: readonly string[], env: NodeJS.ProcessEnv) =>
208
+ spawnSync(execPath, [...argv], { stdio: 'inherit', env }).status);
209
+ const status = spawn(options.execPath, options.argv, {
210
+ ...options.env,
211
+ [LAUNCH_UPDATED_FROM_ENV]: options.fromVersion,
212
+ });
213
+ return status ?? 0;
214
+ }
package/src/cli/parser.ts CHANGED
@@ -87,6 +87,7 @@ function createDefaultFlags(): GoodVibesCliFlags {
87
87
  fork: undefined,
88
88
  yes: false,
89
89
  nonInteractive: false,
90
+ strict: false,
90
91
  };
91
92
  }
92
93
 
@@ -260,6 +261,10 @@ export function parseGoodVibesCli(
260
261
  flags = withFlag(withFlag(flags, 'nonInteractive', true), 'yes', true);
261
262
  continue;
262
263
  }
264
+ if (name === '--strict') {
265
+ flags = withFlag(flags, 'strict', true);
266
+ continue;
267
+ }
263
268
 
264
269
  if (name === '--provider') {
265
270
  const consumed = getValue(argv, index, inlineValue, name, errors);
package/src/cli/status.ts CHANGED
@@ -502,6 +502,19 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
502
502
  return findings;
503
503
  }
504
504
 
505
+ /**
506
+ * The doctor command's exit code. Advisory findings (`severity: 'warning'`)
507
+ * are notes on an otherwise-usable install and must never make a healthy
508
+ * install report failure — only a must-fix (`severity: 'risk'`) finding
509
+ * exits non-zero. `strict` (for CI) flips warnings to failures too, so a
510
+ * pipeline can require a fully clean report instead of just "usable".
511
+ */
512
+ export function resolveDoctorExitCode(findings: readonly CliDoctorFinding[], strict = false): number {
513
+ if (findings.some((finding) => finding.severity === 'risk')) return 1;
514
+ if (strict && findings.length > 0) return 1;
515
+ return 0;
516
+ }
517
+
505
518
  export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnapshot {
506
519
  const config = options.configManager;
507
520
  const controlPlaneBinding = resolveRuntimeEndpointBinding(config, 'controlPlane');
@@ -5,7 +5,7 @@ import {
5
5
  GOODVIBES_NTFY_REMOTE_TOPIC,
6
6
  resolveGoodVibesNtfyTopics,
7
7
  } from '@pellux/goodvibes-sdk/platform/integrations';
8
- import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags } from '../runtime/surface-feature-flags.ts';
8
+ import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags, surfaceFeatureGateSettingsKeys } from '../runtime/surface-feature-flags.ts';
9
9
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
10
10
  import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
11
11
  import type { CliCommandRuntime } from './types.ts';
@@ -147,7 +147,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
147
147
  readinessIssues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
148
148
  }
149
149
  if (surface.missingFeatureFlags.length > 0) {
150
- readinessIssues.push(`${surface.label} is enabled but feature gates are disabled: ${surface.missingFeatureFlags.join(', ')}.`);
150
+ readinessIssues.push(`${surface.label} is enabled but these settings are off: ${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(', ')}.`);
151
151
  }
152
152
  }
153
153
  const value = {
@@ -185,7 +185,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
185
185
  ` http-listener: ${yesNo(value.httpListener.enabled)} (${value.httpListener.hostMode} ${value.httpListener.host}:${value.httpListener.port})${includeProbe ? ` reachable=${yesNo(value.httpListener.reachable)}` : ''}`,
186
186
  '',
187
187
  'External surfaces:',
188
- ...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? ` featureGates=${surface.missingFeatureFlags.join(',')}` : ''}`),
188
+ ...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? ` settingsOff=${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(',')}` : ''}`),
189
189
  ...(filteredSurfaces.some((surface) => surface.id === 'ntfy') ? [
190
190
  '',
191
191
  'ntfy inbound topics:',
@@ -260,7 +260,7 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
260
260
  if (isNetworkFacing(enabled, binding) && auth.bootstrapCredentialPresent) issues.push('Network-facing listener still has a bootstrap credential file.');
261
261
  for (const surface of surfaces) {
262
262
  if (surface.missing.length > 0) issues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
263
- if (surface.missingFeatureFlags.length > 0) issues.push(`${surface.label} is enabled but feature gates are disabled: ${surface.missingFeatureFlags.join(', ')}.`);
263
+ if (surface.missingFeatureFlags.length > 0) issues.push(`${surface.label} is enabled but these settings are off: ${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(', ')}.`);
264
264
  }
265
265
  return { enabled, ...binding, posture, reachable, service, auth, surfaces, issues };
266
266
  }
@@ -276,7 +276,7 @@ export function formatListenerTestResult(runtime: CliCommandRuntime, value: List
276
276
  ` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
277
277
  ` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
278
278
  value.surfaces.length === 0 ? ' enabled webhook surfaces: none' : ' enabled webhook surfaces:',
279
- ...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.missingFeatureFlags.length > 0 ? ` featureGates=${surface.missingFeatureFlags.join(',')}` : ''}`),
279
+ ...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.missingFeatureFlags.length > 0 ? ` settingsOff=${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(',')}` : ''}`),
280
280
  value.issues.length === 0 ? ' readiness: ready' : ' readiness: needs attention',
281
281
  ...value.issues.map((issue) => ` - ${issue}`),
282
282
  ].join('\n'));