@pellux/goodvibes-tui 0.19.27 → 0.19.29

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 (41) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +3 -2
  6. package/src/cli/entrypoint.ts +2 -2
  7. package/src/cli/help.ts +1 -1
  8. package/src/cli/status.ts +9 -9
  9. package/src/cli/surface-command.ts +46 -11
  10. package/src/cli/tui-startup.ts +4 -4
  11. package/src/daemon/cli.ts +7 -0
  12. package/src/input/handler-interactions.ts +14 -1
  13. package/src/input/handler-onboarding.ts +161 -118
  14. package/src/input/handler.ts +1 -1
  15. package/src/input/onboarding/handler-onboarding-routes.ts +35 -15
  16. package/src/input/onboarding/onboarding-wizard-apply.ts +35 -25
  17. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -5
  18. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +93 -5
  19. package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -3
  20. package/src/input/onboarding/onboarding-wizard-rules.ts +40 -8
  21. package/src/input/onboarding/onboarding-wizard-state.ts +19 -8
  22. package/src/input/onboarding/onboarding-wizard-steps.ts +226 -93
  23. package/src/input/onboarding/onboarding-wizard-types.ts +15 -0
  24. package/src/input/onboarding/onboarding-wizard.ts +123 -6
  25. package/src/input/settings-modal-types.ts +2 -1
  26. package/src/input/settings-modal.ts +4 -0
  27. package/src/main.ts +35 -27
  28. package/src/renderer/compositor.ts +3 -3
  29. package/src/renderer/onboarding/onboarding-wizard.ts +141 -57
  30. package/src/renderer/settings-modal-helpers.ts +9 -0
  31. package/src/renderer/settings-modal.ts +3 -0
  32. package/src/runtime/bootstrap.ts +15 -0
  33. package/src/runtime/onboarding/apply.ts +45 -90
  34. package/src/runtime/onboarding/derivation.ts +7 -7
  35. package/src/runtime/onboarding/markers.ts +41 -55
  36. package/src/runtime/onboarding/snapshot.ts +1 -0
  37. package/src/runtime/onboarding/state.ts +6 -6
  38. package/src/runtime/onboarding/types.ts +24 -27
  39. package/src/runtime/onboarding/verify.ts +3 -65
  40. package/src/runtime/surface-feature-flags.ts +67 -0
  41. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.29] — 2026-04-24
8
+
9
+ ### Changes
10
+ - 617b8860 feat: improve onboarding and surface setup
11
+ - 5e39b0f8 docs: add onboarding wizard WIP notice to README top
12
+
13
+ ## [0.19.28] — 2026-04-24
14
+
15
+ ### Changes
16
+ - 89221c1 fix: mark onboarding checked when opened
17
+
7
18
  ## [0.19.27] — 2026-04-24
8
19
 
9
20
  ### Changes
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
+ > **ATTENTION:** Currently updating the Onboarding Wizard - Expect problems with starting the TUI until this work is complete!
2
+
1
3
  # goodvibes-tui
2
4
 
3
5
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.27-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
7
+ [![Version](https://img.shields.io/badge/version-0.19.29-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
8
 
7
9
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
10
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.25.1"
6
+ "version": "0.25.4"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.27",
3
+ "version": "0.19.29",
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",
@@ -91,7 +91,7 @@
91
91
  "@anthropic-ai/vertex-sdk": "^0.16.0",
92
92
  "@ast-grep/napi": "^0.42.0",
93
93
  "@aws/bedrock-token-generator": "^1.1.0",
94
- "@pellux/goodvibes-sdk": "^0.25.1",
94
+ "@pellux/goodvibes-sdk": "^0.25.4",
95
95
  "bash-language-server": "^5.6.0",
96
96
  "fuse.js": "^7.1.0",
97
97
  "graphql": "^16.13.2",
@@ -5,6 +5,7 @@ import { createShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/s
5
5
  import { listProviderRuntimeSnapshots } from '@pellux/goodvibes-sdk/platform/providers/runtime-snapshot';
6
6
  import { createRuntimeServices } from '../runtime/services.ts';
7
7
  import { createRuntimeStore } from '../runtime/store/index.ts';
8
+ import { getOnboardingCheckMarkerPath } from '../runtime/onboarding/index.ts';
8
9
  import { CONFIG_SCHEMA } from '../config/index.ts';
9
10
  import { SecretsManager } from '../config/secrets.ts';
10
11
  import type { ConfigKey } from '../config/index.ts';
@@ -178,8 +179,8 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
178
179
  },
179
180
  secrets: await secrets.inspect(),
180
181
  onboarding: {
181
- projectMarker: existsSync(shellPaths.resolveProjectPath('tui', 'onboarding.json')),
182
- userMarker: existsSync(shellPaths.resolveUserPath('tui', 'onboarding.json')),
182
+ userMarker: existsSync(getOnboardingCheckMarkerPath(shellPaths, 'user')),
183
+ projectMarker: existsSync(getOnboardingCheckMarkerPath(shellPaths, 'project')),
183
184
  },
184
185
  };
185
186
  const targetPath = shellPaths.resolveWorkspacePath(outputPath);
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ConfigManager } from '../config/index.ts';
4
- import { readOnboardingCompletionMarkers } from '../runtime/onboarding/index.ts';
4
+ import { readOnboardingCheckMarkers } from '../runtime/onboarding/index.ts';
5
5
  import { GlobalNetworkTransportInstaller } from '@pellux/goodvibes-sdk/platform/runtime/network/index';
6
6
  import { createShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
7
7
  import { configureActivityLogger } from '@pellux/goodvibes-sdk/platform/utils/logger';
@@ -125,7 +125,7 @@ export async function prepareShellCliRuntime(
125
125
  const userStorePath = shellPaths.resolveUserPath('tui', 'auth-users.json');
126
126
  const bootstrapCredentialPath = shellPaths.resolveUserPath('tui', 'auth-bootstrap.txt');
127
127
  const operatorTokenPath = join(bootstrapHomeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
128
- const onboardingMarkers = readOnboardingCompletionMarkers(shellPaths);
128
+ const onboardingMarkers = readOnboardingCheckMarkers(shellPaths);
129
129
  const service = await buildCliServicePosture({
130
130
  configManager,
131
131
  workingDirectory: bootstrapWorkingDir,
package/src/cli/help.ts CHANGED
@@ -124,7 +124,7 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
124
124
  },
125
125
  onboarding: {
126
126
  usage: ['onboarding', 'setup', 'onboarding status'],
127
- summary: 'Open the setup wizard on the next TUI startup, or inspect onboarding marker status from the CLI.',
127
+ summary: 'Open the setup wizard, or inspect whether onboarding has already been shown for this user.',
128
128
  examples: ['onboarding', 'onboarding status'],
129
129
  },
130
130
  status: {
package/src/cli/status.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
2
- import type { OnboardingCompletionMarkersState } from '../runtime/onboarding/index.ts';
2
+ import type { OnboardingCheckMarkersState } from '../runtime/onboarding/index.ts';
3
3
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
4
4
  import { isNetworkFacing } from './network-posture.ts';
5
5
  import type { GoodVibesCliOutputFormat } from './types.ts';
@@ -9,7 +9,7 @@ export interface CliStatusOptions {
9
9
  readonly configManager: Pick<ConfigManager, 'get'>;
10
10
  readonly workingDirectory: string;
11
11
  readonly homeDirectory: string;
12
- readonly onboardingMarkers?: OnboardingCompletionMarkersState;
12
+ readonly onboardingMarkers?: OnboardingCheckMarkersState;
13
13
  readonly auth?: CliAuthStatus;
14
14
  readonly service?: CliServicePosture;
15
15
  readonly doctor?: boolean;
@@ -63,7 +63,7 @@ export interface CliStatusSnapshot {
63
63
  readonly web: ReturnType<typeof resolveRuntimeEndpointBinding> & { readonly enabled: unknown };
64
64
  };
65
65
  readonly onboarding: {
66
- readonly completed: boolean;
66
+ readonly checked: boolean;
67
67
  readonly scope: string;
68
68
  readonly updatedAt: number | null;
69
69
  };
@@ -167,13 +167,13 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
167
167
  }
168
168
  }
169
169
 
170
- if (!marker?.payload) {
170
+ if (!marker?.exists) {
171
171
  findings.push({
172
172
  id: 'onboarding-incomplete',
173
173
  area: 'onboarding',
174
174
  severity: 'warning',
175
- summary: 'Onboarding has not been completed for this user/project.',
176
- cause: 'No effective onboarding completion marker was found.',
175
+ summary: 'Onboarding has not been shown for this user.',
176
+ cause: 'No global user onboarding check marker was found.',
177
177
  impact: 'Important service, network, provider, auth, or permission choices may still be implicit defaults.',
178
178
  action: 'Run /onboarding in the TUI or goodvibes onboarding status to review setup state.',
179
179
  });
@@ -277,7 +277,7 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
277
277
  web: { enabled: config.get('web.enabled'), ...webBinding },
278
278
  },
279
279
  onboarding: {
280
- completed: Boolean(marker?.payload),
280
+ checked: Boolean(marker?.exists),
281
281
  scope: marker?.scope ?? 'none',
282
282
  updatedAt: marker?.payload?.updatedAt ?? null,
283
283
  },
@@ -344,7 +344,7 @@ export function renderCliStatus(options: CliStatusOptions): string {
344
344
  bindLine('web', webEnabled, webBinding),
345
345
  '',
346
346
  'Onboarding:',
347
- ` completed: ${marker?.payload ? 'yes' : 'no'}`,
347
+ ` checked: ${marker?.exists ? 'yes' : 'no'}`,
348
348
  ` scope: ${marker?.scope ?? 'none'}`,
349
349
  ` updatedAt: ${marker?.payload ? new Date(marker.payload.updatedAt).toISOString() : 'n/a'}`,
350
350
  ];
@@ -372,7 +372,7 @@ export function renderOnboardingCliStatus(options: CliStatusOptions): string {
372
372
  const marker = options.onboardingMarkers?.effective;
373
373
  return [
374
374
  'GoodVibes onboarding status',
375
- ` completed: ${marker?.payload ? 'yes' : 'no'}`,
375
+ ` checked: ${marker?.exists ? 'yes' : 'no'}`,
376
376
  ` scope: ${marker?.scope ?? 'none'}`,
377
377
  ` source: ${marker?.payload?.source ?? 'n/a'}`,
378
378
  ` mode: ${marker?.payload?.mode ?? 'n/a'}`,
@@ -1,4 +1,11 @@
1
1
  import type { ConfigKey } from '../config/index.ts';
2
+ import {
3
+ GOODVIBES_NTFY_AGENT_TOPIC,
4
+ GOODVIBES_NTFY_CHAT_TOPIC,
5
+ GOODVIBES_NTFY_REMOTE_TOPIC,
6
+ resolveGoodVibesNtfyTopics,
7
+ } from '@pellux/goodvibes-sdk/platform/integrations/ntfy';
8
+ import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags } from '../runtime/surface-feature-flags.ts';
2
9
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
3
10
  import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
4
11
  import type { CliCommandRuntime } from './management.ts';
@@ -18,7 +25,7 @@ export const SURFACE_CONFIGS = [
18
25
  ['discord', 'Discord', ['surfaces.discord.publicKey', 'surfaces.discord.botToken', 'surfaces.discord.applicationId']],
19
26
  ['telegram', 'Telegram', ['surfaces.telegram.botToken']],
20
27
  ['webhook', 'Webhook', ['surfaces.webhook.secret']],
21
- ['ntfy', 'ntfy', ['surfaces.ntfy.baseUrl', 'surfaces.ntfy.topic']],
28
+ ['ntfy', 'ntfy', ['surfaces.ntfy.baseUrl']],
22
29
  ['googleChat', 'Google Chat', ['surfaces.googleChat.webhookUrl']],
23
30
  ['signal', 'Signal', ['surfaces.signal.bridgeUrl', 'surfaces.signal.account']],
24
31
  ['whatsapp', 'WhatsApp', ['surfaces.whatsapp.accessToken', 'surfaces.whatsapp.phoneNumberId']],
@@ -39,6 +46,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
39
46
  if (target === 'web') {
40
47
  runtime.configManager.setDynamic('web.enabled', enabled);
41
48
  if (enabled) {
49
+ enableFeatureFlags(runtime.configManager, getServerSurfaceFeatureFlags({ serverBacked: true, web: true }));
42
50
  runtime.configManager.setDynamic('danger.daemon', true);
43
51
  runtime.configManager.setDynamic('controlPlane.enabled', true);
44
52
  const webError = applyTargetEndpointFlagsOrDefault(runtime, 'web');
@@ -71,6 +79,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
71
79
  else if (SURFACE_CONFIGS.some(([id]) => id === target)) {
72
80
  runtime.configManager.setDynamic(`surfaces.${target}.enabled` as ConfigKey, enabled);
73
81
  if (enabled) {
82
+ enableFeatureFlags(runtime.configManager, getServerSurfaceFeatureFlags({ serverBacked: true, externalSurfaces: [target] }));
74
83
  runtime.configManager.setDynamic('danger.httpListener', true);
75
84
  enableEndpointLanDefault(runtime.configManager, 'httpListener');
76
85
  }
@@ -88,34 +97,45 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
88
97
  const web = resolveRuntimeEndpointBinding(config, 'web');
89
98
  const httpListener = resolveRuntimeEndpointBinding(config, 'httpListener');
90
99
  const includeProbe = sub === 'check';
100
+ const targetExternalSurface = target && SURFACE_CONFIGS.some(([id]) => id === target);
101
+ const shouldProbeControlPlane = includeProbe && !target;
102
+ const shouldProbeWeb = includeProbe && !target;
103
+ const shouldProbeListener = includeProbe && (!target || targetExternalSurface);
91
104
  const [controlPlaneReachable, webReachable, listenerReachable] = includeProbe
92
105
  ? await Promise.all([
93
- probeTcp(controlPlane.host, controlPlane.port),
94
- probeTcp(web.host, web.port),
95
- probeTcp(httpListener.host, httpListener.port),
106
+ shouldProbeControlPlane ? probeTcp(controlPlane.host, controlPlane.port) : Promise.resolve(undefined),
107
+ shouldProbeWeb ? probeTcp(web.host, web.port) : Promise.resolve(undefined),
108
+ shouldProbeListener ? probeTcp(httpListener.host, httpListener.port) : Promise.resolve(undefined),
96
109
  ])
97
110
  : [undefined, undefined, undefined];
98
111
  const externalSurfaces = SURFACE_CONFIGS.map(([id, label, requiredKeys]) => {
99
112
  const enabled = config.get(`surfaces.${id}.enabled` as ConfigKey);
100
113
  const missing = requiredKeys.filter((key) => !isPresentConfigValue(config.get(key as ConfigKey)));
114
+ const missingFeatureFlags = enabled === true ? getMissingSurfaceFeatureFlags(config, id) : [];
101
115
  return {
102
116
  id,
103
117
  label,
104
118
  enabled,
105
- ready: !enabled || missing.length === 0,
119
+ ready: !enabled || (missing.length === 0 && missingFeatureFlags.length === 0),
106
120
  missing,
121
+ missingFeatureFlags,
107
122
  };
108
123
  });
109
124
  const filteredSurfaces = target ? externalSurfaces.filter((surface) => surface.id === target) : externalSurfaces;
110
125
  if (target && filteredSurfaces.length === 0) return { output: `Unknown surface: ${target}`, exitCode: 1 };
126
+ const ntfyTopics = resolveGoodVibesNtfyTopics({
127
+ chatTopic: String(config.get('surfaces.ntfy.chatTopic' as ConfigKey) || GOODVIBES_NTFY_CHAT_TOPIC),
128
+ agentTopic: String(config.get('surfaces.ntfy.agentTopic' as ConfigKey) || GOODVIBES_NTFY_AGENT_TOPIC),
129
+ remoteTopic: String(config.get('surfaces.ntfy.remoteTopic' as ConfigKey) || GOODVIBES_NTFY_REMOTE_TOPIC),
130
+ });
111
131
  const readinessIssues: string[] = [];
112
- if (includeProbe && config.get('controlPlane.enabled') === true && !controlPlaneReachable) {
132
+ if (shouldProbeControlPlane && config.get('controlPlane.enabled') === true && !controlPlaneReachable) {
113
133
  readinessIssues.push(`Control plane is enabled but not reachable on ${controlPlane.host}:${controlPlane.port}.`);
114
134
  }
115
- if (includeProbe && config.get('web.enabled') === true && !webReachable) {
135
+ if (shouldProbeWeb && config.get('web.enabled') === true && !webReachable) {
116
136
  readinessIssues.push(`Web surface is enabled but not reachable on ${web.host}:${web.port}.`);
117
137
  }
118
- if (includeProbe && config.get('danger.httpListener') === true && !listenerReachable) {
138
+ if (shouldProbeListener && config.get('danger.httpListener') === true && !listenerReachable) {
119
139
  readinessIssues.push(`HTTP listener is enabled but not reachable on ${httpListener.host}:${httpListener.port}.`);
120
140
  }
121
141
  for (const surface of filteredSurfaces) {
@@ -126,6 +146,9 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
126
146
  if (surface.missing.length > 0) {
127
147
  readinessIssues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
128
148
  }
149
+ if (surface.missingFeatureFlags.length > 0) {
150
+ readinessIssues.push(`${surface.label} is enabled but feature gates are disabled: ${surface.missingFeatureFlags.join(', ')}.`);
151
+ }
129
152
  }
130
153
  const value = {
131
154
  controlPlane: {
@@ -162,7 +185,15 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
162
185
  ` http-listener: ${yesNo(value.httpListener.enabled)} (${value.httpListener.hostMode} ${value.httpListener.host}:${value.httpListener.port})${includeProbe ? ` reachable=${yesNo(value.httpListener.reachable)}` : ''}`,
163
186
  '',
164
187
  'External surfaces:',
165
- ...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(',')}` : ''}`),
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(',')}` : ''}`),
189
+ ...(filteredSurfaces.some((surface) => surface.id === 'ntfy') ? [
190
+ '',
191
+ 'ntfy inbound topics:',
192
+ ` chat: ${ntfyTopics.chatTopic}`,
193
+ ` agent: ${ntfyTopics.agentTopic}`,
194
+ ` daemon-only remote: ${ntfyTopics.remoteTopic}`,
195
+ ` default delivery topic: ${String(config.get('surfaces.ntfy.topic') || '(none)')}`,
196
+ ] : []),
166
197
  ...(includeProbe ? [
167
198
  readinessIssues.length === 0 ? 'Readiness: ready' : 'Readiness: needs attention',
168
199
  ...readinessIssues.map((issue) => ` - ${issue}`),
@@ -191,6 +222,7 @@ export interface ListenerTestResult {
191
222
  readonly enabled: unknown;
192
223
  readonly ready: boolean;
193
224
  readonly missing: readonly string[];
225
+ readonly missingFeatureFlags: readonly string[];
194
226
  }[];
195
227
  readonly issues: readonly string[];
196
228
  }
@@ -209,12 +241,14 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
209
241
  const surfaces = SURFACE_CONFIGS.map(([id, label, requiredKeys]) => {
210
242
  const surfaceEnabled = runtime.configManager.get(`surfaces.${id}.enabled` as ConfigKey);
211
243
  const missing = requiredKeys.filter((key) => !isPresentConfigValue(runtime.configManager.get(key as ConfigKey)));
244
+ const missingFeatureFlags = surfaceEnabled === true ? getMissingSurfaceFeatureFlags(runtime.configManager, id) : [];
212
245
  return {
213
246
  id,
214
247
  label,
215
248
  enabled: surfaceEnabled,
216
- ready: surfaceEnabled !== true || missing.length === 0,
249
+ ready: surfaceEnabled !== true || (missing.length === 0 && missingFeatureFlags.length === 0),
217
250
  missing,
251
+ missingFeatureFlags,
218
252
  };
219
253
  }).filter((surface) => surface.enabled === true);
220
254
  const issues: string[] = [];
@@ -226,6 +260,7 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
226
260
  if (isNetworkFacing(enabled, binding) && auth.bootstrapCredentialPresent) issues.push('Network-facing listener still has a bootstrap credential file.');
227
261
  for (const surface of surfaces) {
228
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(', ')}.`);
229
264
  }
230
265
  return { enabled, ...binding, posture, reachable, service, auth, surfaces, issues };
231
266
  }
@@ -241,7 +276,7 @@ export function formatListenerTestResult(runtime: CliCommandRuntime, value: List
241
276
  ` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
242
277
  ` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
243
278
  value.surfaces.length === 0 ? ' enabled webhook surfaces: none' : ' enabled webhook surfaces:',
244
- ...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}`),
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(',')}` : ''}`),
245
280
  value.issues.length === 0 ? ' readiness: ready' : ' readiness: needs attention',
246
281
  ...value.issues.map((issue) => ` - ${issue}`),
247
282
  ].join('\n'));
@@ -1,6 +1,6 @@
1
1
  import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
2
2
  import type { InputHandler } from '../input/handler.ts';
3
- import { readOnboardingCompletionMarkers } from '../runtime/onboarding/index.ts';
3
+ import { readOnboardingCheckMarker } from '../runtime/onboarding/index.ts';
4
4
  import type { GoodVibesCliParseResult } from './types.ts';
5
5
 
6
6
  export function applyInitialTuiCliState(options: {
@@ -8,11 +8,11 @@ export function applyInitialTuiCliState(options: {
8
8
  readonly input: InputHandler;
9
9
  readonly commandRegistry: CommandRegistry;
10
10
  readonly commandContext: CommandContext;
11
- readonly shellPaths: Parameters<typeof readOnboardingCompletionMarkers>[0];
11
+ readonly shellPaths: Parameters<typeof readOnboardingCheckMarker>[0];
12
12
  readonly render: () => void;
13
13
  }): void {
14
14
  const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
15
- const onboardingMarkers = readOnboardingCompletionMarkers(shellPaths);
15
+ const globalOnboardingMarker = readOnboardingCheckMarker(shellPaths, 'user');
16
16
  if (cli.command === 'onboarding') {
17
17
  input.openOnboardingWizard({ mode: 'edit', reset: true });
18
18
  } else if (cli.command === 'sessions' && cli.commandArgs[0] === 'resume') {
@@ -20,7 +20,7 @@ export function applyInitialTuiCliState(options: {
20
20
  if (target) {
21
21
  void commandRegistry.execute('session', ['resume', target], commandContext).then(() => render());
22
22
  }
23
- } else if (!onboardingMarkers.effective?.payload) {
23
+ } else if (!globalOnboardingMarker.exists) {
24
24
  input.openOnboardingWizard({ mode: 'new', reset: true });
25
25
  }
26
26
 
package/src/daemon/cli.ts CHANGED
@@ -3,6 +3,8 @@ import { join } from 'node:path';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
5
5
  import { RuntimeEventBus } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
6
+ import { createFeatureFlagManager } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/index';
7
+ import type { FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
6
8
  import { createRuntimeStore } from '../runtime/store/index.ts';
7
9
  import { createRuntimeServices } from '../runtime/services.ts';
8
10
  import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon/server';
@@ -155,8 +157,13 @@ async function main(): Promise<void> {
155
157
  }
156
158
  const runtimeBus = new RuntimeEventBus();
157
159
  const runtimeStore = createRuntimeStore();
160
+ const featureFlags = createFeatureFlagManager();
161
+ featureFlags.loadFromConfig({
162
+ flags: (config.getCategory('featureFlags') as Record<string, FlagState>) ?? {},
163
+ });
158
164
  const runtimeServices = createRuntimeServices({
159
165
  configManager: config,
166
+ featureFlags,
160
167
  runtimeBus,
161
168
  runtimeStore,
162
169
  getConversationTitle: () => 'goodvibes daemon',
@@ -1,6 +1,6 @@
1
1
  import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
2
2
  import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
3
- import { collectOnboardingSnapshot } from '../runtime/onboarding/index.ts';
3
+ import { collectOnboardingSnapshot, readOnboardingCheckMarker, writeOnboardingCheckMarker } from '../runtime/onboarding/index.ts';
4
4
  import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy, handleBlockRerun, handleBlockSave, handleBlockToggle, handleBookmark, handleClipboardPaste, handleCopy, handleCtrlC, handleDiffApply, registerPaste } from './handler-content-actions.ts';
5
5
  import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
6
6
  import { openOnboardingWizardState, type OpenOnboardingWizardOptions } from './handler-ui-state.ts';
@@ -11,6 +11,19 @@ export function openOnboardingWizardForHandler(
11
11
  modeOrOptions: OnboardingWizardMode | OpenOnboardingWizardOptions = 'new',
12
12
  ): void {
13
13
  const options = typeof modeOrOptions === 'string' ? { mode: modeOrOptions } : modeOrOptions;
14
+ const userMarker = readOnboardingCheckMarker(handler.uiServices.environment.shellPaths, 'user');
15
+ if (!userMarker.payload) {
16
+ try {
17
+ writeOnboardingCheckMarker(handler.uiServices.environment.shellPaths, {
18
+ scope: 'user',
19
+ source: 'wizard',
20
+ mode: options.mode ?? 'new',
21
+ });
22
+ } catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ handler.commandContext?.print?.(`Onboarding check marker could not be written: ${message}`);
25
+ }
26
+ }
14
27
  if (!handler.modalStack.includes('onboarding')) handler.modalOpened('onboarding');
15
28
  handler.clearOnboardingModelPickerCancelState();
16
29
  openOnboardingWizardState(handler.onboardingWizard, options);