@pellux/goodvibes-tui 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +95 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1091 -271
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/parser.ts +5 -0
  12. package/src/cli/status.ts +13 -0
  13. package/src/cli/surface-command.ts +5 -5
  14. package/src/cli/tui-startup.ts +60 -97
  15. package/src/cli/types.ts +2 -0
  16. package/src/config/index.ts +1 -1
  17. package/src/config/tui-extension-settings.ts +33 -0
  18. package/src/core/composer-state.ts +1 -1
  19. package/src/core/conversation-line-cache.ts +3 -3
  20. package/src/core/conversation-rendering.ts +4 -4
  21. package/src/core/conversation.ts +2 -2
  22. package/src/core/long-task-notifier.ts +7 -7
  23. package/src/core/stream-event-wiring.ts +8 -6
  24. package/src/core/system-message-noise.ts +1 -1
  25. package/src/core/system-message-router.ts +6 -6
  26. package/src/core/turn-cancellation.ts +1 -1
  27. package/src/core/turn-event-wiring.ts +8 -8
  28. package/src/daemon/cli.ts +20 -5
  29. package/src/daemon/lifecycle.ts +41 -0
  30. package/src/daemon/service-commands.ts +5 -5
  31. package/src/export/cost-utils.ts +128 -6
  32. package/src/input/autocomplete.ts +2 -2
  33. package/src/input/command-registry.ts +34 -9
  34. package/src/input/commands/ci-runtime.ts +5 -1
  35. package/src/input/commands/codebase-runtime.ts +6 -6
  36. package/src/input/commands/cost-runtime.ts +1 -1
  37. package/src/input/commands/health-runtime.ts +2 -2
  38. package/src/input/commands/intelligence-runtime.ts +1 -1
  39. package/src/input/commands/local-auth-runtime.ts +1 -1
  40. package/src/input/commands/local-provider-runtime.ts +49 -10
  41. package/src/input/commands/local-runtime.ts +53 -5
  42. package/src/input/commands/memory.ts +2 -2
  43. package/src/input/commands/operator-panel-runtime.ts +5 -5
  44. package/src/input/commands/permissions-provenance.ts +1 -1
  45. package/src/input/commands/permissions-runtime.ts +99 -8
  46. package/src/input/commands/planning-runtime.ts +3 -3
  47. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  48. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  49. package/src/input/commands/provider.ts +7 -5
  50. package/src/input/commands/recall-files-git.ts +13 -51
  51. package/src/input/commands/recall-files.ts +8 -11
  52. package/src/input/commands/recall-review.ts +2 -2
  53. package/src/input/commands/relay-runtime.ts +1 -1
  54. package/src/input/commands/remote-runtime.ts +1 -1
  55. package/src/input/commands/runtime-services.ts +4 -4
  56. package/src/input/commands/schedule-runtime.ts +14 -7
  57. package/src/input/commands/services-runtime.ts +1 -1
  58. package/src/input/commands/session-workflow.ts +2 -2
  59. package/src/input/commands/settings-sync-runtime.ts +2 -2
  60. package/src/input/commands/share-runtime.ts +1 -1
  61. package/src/input/commands/shell-core.ts +46 -68
  62. package/src/input/commands/subscription-runtime.ts +1 -1
  63. package/src/input/commands/update-runtime.ts +153 -96
  64. package/src/input/commands/work-plan-runtime.ts +1 -1
  65. package/src/input/commands/workstream-runtime.ts +2 -2
  66. package/src/input/commands.ts +0 -2
  67. package/src/input/config-modal-types.ts +3 -3
  68. package/src/input/config-modal.ts +10 -11
  69. package/src/input/feature-unit-layout.ts +93 -82
  70. package/src/input/feed-context-factory.ts +2 -2
  71. package/src/input/handler-command-route.ts +2 -2
  72. package/src/input/handler-content-actions.ts +1 -1
  73. package/src/input/handler-feed-routes.ts +7 -7
  74. package/src/input/handler-feed.ts +4 -4
  75. package/src/input/handler-interactions.ts +3 -2
  76. package/src/input/handler-modal-routes.ts +12 -4
  77. package/src/input/handler-modal-token-routes.ts +1 -1
  78. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  79. package/src/input/handler-onboarding.ts +3 -2
  80. package/src/input/handler-picker-routes.ts +26 -13
  81. package/src/input/handler-shortcuts.ts +6 -6
  82. package/src/input/handler.ts +2 -2
  83. package/src/input/keybindings.ts +2 -2
  84. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  85. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  86. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  87. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  88. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  89. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  90. package/src/input/panel-integration-actions.ts +2 -2
  91. package/src/input/panel-mouse-geometry.ts +1 -1
  92. package/src/input/panel-paste-flood-guard.ts +1 -1
  93. package/src/input/provider-key-intake.ts +167 -0
  94. package/src/input/sandbox-exec-config.ts +2 -2
  95. package/src/input/selection-modal.ts +1 -1
  96. package/src/input/settings-modal-activation.ts +5 -0
  97. package/src/input/settings-modal-adjustment.ts +4 -31
  98. package/src/input/settings-modal-data.ts +22 -21
  99. package/src/input/settings-modal-mutations.ts +12 -68
  100. package/src/input/settings-modal-types.ts +23 -16
  101. package/src/input/settings-modal.ts +74 -55
  102. package/src/main.ts +48 -48
  103. package/src/panels/agent-inspector-shared.ts +4 -4
  104. package/src/panels/base-panel.ts +2 -2
  105. package/src/panels/builtin/agent.ts +4 -4
  106. package/src/panels/builtin/development.ts +4 -4
  107. package/src/panels/builtin/knowledge.ts +1 -1
  108. package/src/panels/builtin/operations.ts +29 -10
  109. package/src/panels/builtin/session.ts +5 -5
  110. package/src/panels/builtin/shared.ts +4 -4
  111. package/src/panels/builtin-modals.ts +10 -10
  112. package/src/panels/builtin-panels.ts +1 -1
  113. package/src/panels/cost-tracker-panel.ts +124 -7
  114. package/src/panels/diff-panel.ts +5 -5
  115. package/src/panels/eval-registry.ts +2 -2
  116. package/src/panels/fleet-deep-link.ts +1 -1
  117. package/src/panels/fleet-panel-format.ts +44 -8
  118. package/src/panels/fleet-panel.ts +16 -16
  119. package/src/panels/fleet-read-model.ts +52 -13
  120. package/src/panels/fleet-steer.ts +2 -2
  121. package/src/panels/fleet-stop.ts +3 -3
  122. package/src/panels/fleet-tabs.ts +13 -14
  123. package/src/panels/fleet-transcript.ts +5 -5
  124. package/src/panels/git-panel.ts +2 -2
  125. package/src/panels/index.ts +4 -1
  126. package/src/panels/local-auth-panel.ts +1 -1
  127. package/src/panels/modals/hooks-modal.ts +1 -1
  128. package/src/panels/modals/keybindings-modal.ts +2 -2
  129. package/src/panels/modals/knowledge-modal.ts +1 -1
  130. package/src/panels/modals/marketplace-modal.ts +4 -4
  131. package/src/panels/modals/memory-modal.ts +4 -4
  132. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  133. package/src/panels/modals/modal-theme.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +2 -2
  135. package/src/panels/modals/planning-modal.ts +2 -2
  136. package/src/panels/modals/plugins-modal.ts +1 -1
  137. package/src/panels/modals/policy-modal.ts +1 -1
  138. package/src/panels/modals/security-modal.ts +1 -1
  139. package/src/panels/modals/skills-modal.ts +1 -1
  140. package/src/panels/modals/work-plan-modal.ts +3 -3
  141. package/src/panels/notifications-feed.ts +127 -0
  142. package/src/panels/notifications-panel.ts +160 -0
  143. package/src/panels/panel-manager.ts +9 -9
  144. package/src/panels/plugins-panel.ts +4 -4
  145. package/src/panels/project-planning-answer-actions.ts +1 -1
  146. package/src/panels/provider-health-routes.ts +2 -2
  147. package/src/panels/skills-panel.ts +4 -5
  148. package/src/panels/token-budget-panel.ts +11 -3
  149. package/src/panels/types.ts +5 -5
  150. package/src/permissions/broker-approval-card.ts +201 -0
  151. package/src/permissions/hunk-selection.ts +28 -4
  152. package/src/permissions/prompt.ts +244 -29
  153. package/src/renderer/autocomplete-overlay.ts +107 -54
  154. package/src/renderer/code-block.ts +2 -2
  155. package/src/renderer/compaction-history-modal.ts +1 -1
  156. package/src/renderer/compaction-preview.ts +1 -1
  157. package/src/renderer/compaction-quality.ts +1 -1
  158. package/src/renderer/compositor.ts +2 -2
  159. package/src/renderer/config-modal.ts +1 -1
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/footer-tips.ts +3 -3
  162. package/src/renderer/hint-grammar.ts +1 -1
  163. package/src/renderer/modal-factory.ts +1 -1
  164. package/src/renderer/model-picker-overlay.ts +1 -1
  165. package/src/renderer/model-workspace.ts +7 -2
  166. package/src/renderer/onboarding/onboarding-wizard.ts +22 -8
  167. package/src/renderer/panel-composite.ts +2 -2
  168. package/src/renderer/panel-workspace-bar.ts +1 -1
  169. package/src/renderer/process-indicator.ts +1 -1
  170. package/src/renderer/selection-modal-overlay.ts +102 -24
  171. package/src/renderer/session-picker-modal.ts +2 -2
  172. package/src/renderer/settings-modal-helpers.ts +14 -5
  173. package/src/renderer/settings-modal.ts +127 -39
  174. package/src/renderer/shell-surface.ts +3 -0
  175. package/src/renderer/terminal-bg-probe.ts +1 -1
  176. package/src/renderer/theme.ts +3 -3
  177. package/src/renderer/tool-call.ts +1 -1
  178. package/src/renderer/tool-result-summary.ts +1 -1
  179. package/src/renderer/turn-injection.ts +7 -6
  180. package/src/renderer/ui-factory.ts +6 -1
  181. package/src/runtime/bootstrap-command-context.ts +4 -1
  182. package/src/runtime/bootstrap-command-parts.ts +6 -4
  183. package/src/runtime/bootstrap-core.ts +35 -35
  184. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  185. package/src/runtime/bootstrap-shell.ts +6 -5
  186. package/src/runtime/bootstrap.ts +34 -32
  187. package/src/runtime/channel-composition.ts +46 -0
  188. package/src/runtime/code-index-services.ts +3 -4
  189. package/src/runtime/daemon-attach-notices.ts +120 -0
  190. package/src/runtime/durability-services.ts +55 -0
  191. package/src/runtime/embedded-service-factories.ts +40 -0
  192. package/src/runtime/feature-settings.ts +147 -0
  193. package/src/runtime/index.ts +4 -21
  194. package/src/runtime/legacy-daemon-migration.ts +29 -19
  195. package/src/runtime/memory-fold.ts +1 -1
  196. package/src/runtime/notification-dispatch.ts +110 -0
  197. package/src/runtime/onboarding/apply.ts +3 -29
  198. package/src/runtime/onboarding/fast-path.ts +39 -6
  199. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  200. package/src/runtime/onboarding/snapshot.ts +0 -1
  201. package/src/runtime/onboarding/types.ts +9 -3
  202. package/src/runtime/provider-fallback.ts +55 -0
  203. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  204. package/src/runtime/release-artifacts.ts +19 -103
  205. package/src/runtime/resume-notice.ts +1 -1
  206. package/src/runtime/services.ts +81 -82
  207. package/src/runtime/session-continuity-hints.ts +38 -0
  208. package/src/runtime/surface-feature-flags.ts +40 -9
  209. package/src/runtime/terminal-output-guard.ts +2 -2
  210. package/src/runtime/trust/workspace-registration.ts +10 -2
  211. package/src/runtime/trust/workspace-trust.ts +58 -31
  212. package/src/runtime/ui-services.ts +3 -1
  213. package/src/runtime/update-check.ts +15 -69
  214. package/src/runtime/workstream-services.ts +2 -2
  215. package/src/shell/blocking-input.ts +102 -6
  216. package/src/shell/ui-openers.ts +38 -6
  217. package/src/utils/format-duration.ts +1 -1
  218. package/src/verification/verification-ledger.ts +21 -4
  219. package/src/version.ts +1 -1
  220. package/src/input/commands/flags-runtime.ts +0 -328
@@ -1,6 +1,7 @@
1
1
  import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
2
2
  import type { InputHandler } from '../input/handler.ts';
3
3
  import type { SelectionItem } from '../input/selection-modal.ts';
4
+ import type { WorkspaceRegistrationManager } from '../runtime/trust/workspace-registration.ts';
4
5
  import { hasResumableWizardProgress, readOnboardingCheckMarker, readWizardProgress } from '../runtime/onboarding/index.ts';
5
6
  import { startOnboardingFastPath } from '../runtime/onboarding/fast-path.ts';
6
7
  import { readLastSessionPointer } from '@/runtime/index.ts';
@@ -13,111 +14,62 @@ export type TuiStartupShellPaths = Parameters<typeof readOnboardingCheckMarker>[
13
14
 
14
15
  export type FirstOpenTrustLevel = 'trusted' | 'restricted';
15
16
 
16
- /** The decisions a first-open prompt choice resolves to. Absent halves were not asked. */
17
- export interface FirstOpenDecision {
18
- readonly trust?: FirstOpenTrustLevel;
19
- readonly register?: 'yes' | 'no';
20
- }
21
-
22
17
  /**
23
- * decodeFirstOpenChoice — pure mapping from a chosen selection-item id (or null
24
- * on Escape/enter-through) to the trust + registration decisions, given which
25
- * halves the prompt actually offered. Kept pure so the combined-prompt semantics
26
- * are unit-testable without a live modal.
27
- *
28
- * Escape/enter-through (id === null) is the safe default: an offered trust half
29
- * settles to 'restricted' (matching the standalone trust prompt), and an offered
30
- * register half defaults to NO — recorded as a subtree-scoped decline so the
31
- * directory is not re-asked every startup.
18
+ * decodeFirstOpenChoice — pure mapping from a chosen selection-item id (or
19
+ * null on Escape/enter-through) to the trust decision. Kept pure so the
20
+ * consequence-time trust prompt's semantics (raised by trustGatedAsk, wired
21
+ * in main.ts's trustPromptRef.requestTrustDecision) are unit-testable
22
+ * without a live modal. Escape/enter-through (id === null) is the safe
23
+ * default: 'restricted'.
32
24
  */
33
- export function decodeFirstOpenChoice(
34
- id: string | null,
35
- offered: { readonly trustNeeded: boolean; readonly registerNeeded: boolean },
36
- ): FirstOpenDecision {
37
- const decision: { trust?: FirstOpenTrustLevel; register?: 'yes' | 'no' } = {};
38
- if (offered.trustNeeded && offered.registerNeeded) {
39
- // Combined 2x2 choice.
40
- const trust: FirstOpenTrustLevel = id === 'trust-register' || id === 'trust-only' ? 'trusted' : 'restricted';
41
- const register: 'yes' | 'no' = id === 'trust-register' || id === 'restrict-register' ? 'yes' : 'no';
42
- decision.trust = trust;
43
- decision.register = register;
44
- } else if (offered.trustNeeded) {
45
- decision.trust = id === 'trusted' ? 'trusted' : 'restricted';
46
- } else if (offered.registerNeeded) {
47
- decision.register = id === 'register' ? 'yes' : 'no';
48
- }
49
- return decision;
25
+ export function decodeFirstOpenChoice(id: string | null): FirstOpenTrustLevel {
26
+ return id === 'trusted' ? 'trusted' : 'restricted';
50
27
  }
51
28
 
52
- /** Build the selection rows for a first-open prompt given which halves apply. */
53
- export function buildFirstOpenItems(offered: {
54
- readonly trustNeeded: boolean;
55
- readonly registerNeeded: boolean;
56
- }): { readonly title: string; readonly items: SelectionItem[] } {
57
- if (offered.trustNeeded && offered.registerNeeded) {
58
- return {
59
- title: 'New workspace — trust level and registration',
60
- items: [
61
- { id: 'trust-register', label: 'Trust & register this workspace', detail: 'Full capability, and track it in your workspace registry', primaryAction: 'select' },
62
- { id: 'trust-only', label: 'Trust, don\'t register', detail: 'Full capability; leave it out of the registry', primaryAction: 'select' },
63
- { id: 'restrict-register', label: 'Keep restricted, register', detail: 'Read-only for now; still track it in the registry', primaryAction: 'select' },
64
- { id: 'restrict-only', label: 'Keep restricted, don\'t register', detail: 'Read-only exploration; nothing recorded (default)', primaryAction: 'select' },
65
- ],
66
- };
67
- }
68
- if (offered.trustNeeded) {
69
- return {
70
- title: 'New workspace — choose a trust level',
71
- items: [
72
- { id: 'trusted', label: 'Trust this workspace', detail: 'Full capability — all tools may run', primaryAction: 'select' },
73
- { id: 'restricted', label: 'Keep restricted (read-only)', detail: 'Explore safely; writes and commands are denied until trusted', primaryAction: 'select' },
74
- ],
75
- };
76
- }
29
+ /**
30
+ * Build the selection rows for the trust prompt. This used to also offer a
31
+ * "register this directory?" half (a combined 2x2 when both were needed) —
32
+ * that half is gone: registration self-records (see
33
+ * selfRecordWorkspaceRegistration below) rather than ever asking a question
34
+ * about a registry the user hasn't met yet.
35
+ */
36
+ export function buildFirstOpenItems(): { readonly title: string; readonly items: SelectionItem[] } {
77
37
  return {
78
- title: 'Register this directory as a workspace?',
38
+ title: 'New workspace — choose a trust level',
79
39
  items: [
80
- { id: 'register', label: 'Register this workspace', detail: 'Track this project root in your workspace registry', primaryAction: 'select' },
81
- { id: 'skip', label: 'Don\'t register', detail: 'Leave it out; not asked again for this directory (default)', primaryAction: 'select' },
40
+ { id: 'trusted', label: 'Trust this workspace', detail: 'Full capability — all tools may run', primaryAction: 'select' },
41
+ { id: 'restricted', label: 'Keep restricted (read-only)', detail: 'Explore safely; writes and commands are denied until trusted', primaryAction: 'select' },
82
42
  ],
83
43
  };
84
44
  }
85
45
 
86
46
  /**
87
- * First-open workspace prompt — one surface folding the TUI-local trust gate and
88
- * the platform-wide registration half. When GoodVibes opens a workspace it has
89
- * no trust decision for, and/or one whose registration resolves UNKNOWN, ask
90
- * once (never two stacked modals): trust level AND "register this directory?".
47
+ * Registration self-records: a workspace the user actually works in becomes
48
+ * its own registry entry with no question ever asked (the former "Register
49
+ * this directory as a workspace?" prompt — sometimes a four-option 2x2 when
50
+ * trust was undecided too — is gone entirely).
51
+ *
52
+ * "Actually works in" is anchored to TRUST, not mere directory-open: this is
53
+ * only ever called for a workspace that is (or has just become) trusted,
54
+ * never for one that's merely been glanced at read-only. Labeled 'via TUI'
55
+ * in the shared registry so its provenance is honest.
91
56
  *
92
- * The two records are independent: a grandfathered-trusted workspace skips the
93
- * trust half but may still see the register half once; a covered/declined/broad
94
- * directory skips the register half. Escape/enter-through takes the safe default
95
- * (restricted, and a recorded decline for the register half).
57
+ * OWNER-BOUNDARY RIDER (binding): self-recording must not widen the
58
+ * checkpoint-owning consumer's boundary. That boundary now reads the SAME
59
+ * shared registration store this writes to, gated by each record's
60
+ * `checkpointEligible` flag (absent means false). So the separation is no
61
+ * longer "a separate list the agent owns" — it is one store, and this self
62
+ * record stays out of scope by stamping its `origin` ('tui') for honest
63
+ * provenance while NEVER setting `checkpointEligible`. Only the consumer that
64
+ * owns checkpointing re-stamps its own roots eligible on boot.
96
65
  */
97
- async function promptWorkspaceFirstOpen(commandContext: CommandContext, render: () => void): Promise<void> {
98
- const trustManager = commandContext.workspace?.workspaceTrustManager;
99
- const registrationManager = commandContext.workspace?.workspaceRegistrationManager;
100
- const trustNeeded = Boolean(trustManager && !trustManager.isDecided());
101
- const evaluation = registrationManager ? await registrationManager.evaluate() : null;
102
- const registerNeeded = Boolean(evaluation?.offerRegister);
103
- if (!trustNeeded && !registerNeeded) return;
104
-
105
- const offered = { trustNeeded, registerNeeded };
106
- const { title, items } = buildFirstOpenItems(offered);
107
- commandContext.openSelection?.(
108
- title,
109
- items,
110
- { allowSearch: false, primaryVerbLabel: 'Choose' },
111
- (result) => {
112
- const decision = decodeFirstOpenChoice(result?.item.id ?? null, offered);
113
- const apply = async () => {
114
- if (decision.trust && trustManager) await trustManager.setLevel(decision.trust);
115
- if (decision.register === 'yes' && registrationManager) await registrationManager.register();
116
- else if (decision.register === 'no' && registrationManager) await registrationManager.decline();
117
- };
118
- void apply().finally(render);
119
- },
120
- );
66
+ export async function selfRecordWorkspaceRegistration(
67
+ registrationManager: Pick<WorkspaceRegistrationManager, 'evaluate' | 'register'> | undefined,
68
+ ): Promise<void> {
69
+ if (!registrationManager) return;
70
+ const evaluation = await registrationManager.evaluate();
71
+ if (!evaluation.offerRegister) return;
72
+ await registrationManager.register('via TUI', 'tui');
121
73
  }
122
74
 
123
75
  export function applyInitialTuiCliState(options: {
@@ -131,6 +83,16 @@ export function applyInitialTuiCliState(options: {
131
83
  const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
132
84
  const globalOnboardingMarker = readOnboardingCheckMarker(shellPaths, 'user');
133
85
 
86
+ // Registration self-records on every launch of an already-trusted
87
+ // workspace (fire-and-forget — never blocks or gates startup, never a
88
+ // modal). A workspace that is undecided or restricted is never
89
+ // self-registered here; for one that becomes trusted just now (via the
90
+ // consequence-time trust prompt), main.ts's trustPromptRef wiring calls
91
+ // this same helper right after the decision.
92
+ if (commandContext.workspace?.workspaceTrustManager?.isTrusted()) {
93
+ void selfRecordWorkspaceRegistration(commandContext.workspace.workspaceRegistrationManager);
94
+ }
95
+
134
96
  // Seeded prompt is always applied synchronously, regardless of session branch.
135
97
  const seededPrompt = cli.flags.prompt ?? (cli.rawCommand === undefined && cli.positionals.length > 0 ? cli.positionals.join(' ') : undefined);
136
98
  if (seededPrompt) {
@@ -183,6 +145,9 @@ export function applyInitialTuiCliState(options: {
183
145
  } else if (!globalOnboardingMarker.exists) {
184
146
  // Fast path: get a brand-new user to a working session in the fewest steps.
185
147
  // Falls back to the full wizard when the surface can't detect providers.
148
+ // Trust stays undecided here on purpose — the first non-read tool
149
+ // request is what raises the trust question now (see main.ts's
150
+ // trustPromptRef), not this startup branch.
186
151
  startOnboardingFastPath({ input, commandContext, shellPaths, render });
187
152
  } else {
188
153
  // User has completed onboarding before but left a wizard session in progress.
@@ -202,11 +167,9 @@ export function applyInitialTuiCliState(options: {
202
167
  },
203
168
  });
204
169
  }
205
- } else {
206
- // Returning user, no wizard to resume: this is the moment to ask, in one
207
- // surface, about a genuinely new workspace's trust level and whether to
208
- // register it — before they run anything here.
209
- void promptWorkspaceFirstOpen(commandContext, render);
210
170
  }
171
+ // Returning user, no wizard to resume, nothing else to do at startup:
172
+ // trust (if still undecided) is asked at the first non-read tool
173
+ // request, not here — see main.ts's trustPromptRef wiring.
211
174
  }
212
175
  }
package/src/cli/types.ts CHANGED
@@ -60,6 +60,8 @@ export interface GoodVibesCliFlags {
60
60
  readonly fork: string | true | undefined;
61
61
  readonly yes: boolean;
62
62
  readonly nonInteractive: boolean;
63
+ /** `doctor --strict`: advisory findings also fail (exit 1), for CI. Ignored by every other command. */
64
+ readonly strict: boolean;
63
65
  }
64
66
 
65
67
  export interface GoodVibesCliParseResult {
@@ -8,7 +8,7 @@
8
8
 
9
9
  export { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
10
10
  export type { DeepReadonly } from '@pellux/goodvibes-sdk/platform/config';
11
- export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig, PersistedFlagState } from '@pellux/goodvibes-sdk/platform/config';
11
+ export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig } from '@pellux/goodvibes-sdk/platform/config';
12
12
  export { DEFAULT_CONFIG, CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config';
13
13
  export { ConfigError } from '@pellux/goodvibes-sdk/platform/types';
14
14
 
@@ -99,6 +99,39 @@ export function readStatuslineSettings(configManager: Pick<ConfigManager, 'getRa
99
99
  return out;
100
100
  }
101
101
 
102
+ // ─── update.* — launch-time self-update behavior ─────────────────────────────
103
+
104
+ /**
105
+ * User-settable launch-time self-update behavior. The feature itself (check at
106
+ * launch, install via the checksum-verified download/verify/swap path, restart
107
+ * onto the new binary) defaults ON for binary installs; the defaults live in
108
+ * the consumer (src/cli/launch-auto-update.ts), matching this file's rule that
109
+ * readers return only what the user actually set.
110
+ */
111
+ export interface UpdateSettings {
112
+ /** Check for a newer release at TUI launch and install it before starting. Default: true. */
113
+ readonly autoUpdateAtLaunch?: boolean;
114
+ /** How long the launch-time version check may take before it is skipped. Defaults to 2500; clamped to [250, 30000]. */
115
+ readonly launchCheckTimeoutMs?: number;
116
+ }
117
+
118
+ const LAUNCH_CHECK_MIN_TIMEOUT_MS = 250;
119
+ const LAUNCH_CHECK_MAX_TIMEOUT_MS = 30_000;
120
+
121
+ /** Read `update.*` from settings.json, validating and clamping the timeout. */
122
+ export function readUpdateSettings(configManager: Pick<ConfigManager, 'getRaw'>): UpdateSettings {
123
+ const src = readNamespace(configManager, 'update');
124
+ if (!src) return {};
125
+ const out: { autoUpdateAtLaunch?: boolean; launchCheckTimeoutMs?: number } = {};
126
+ const autoUpdateAtLaunch = readBoolean(src, 'autoUpdateAtLaunch');
127
+ if (autoUpdateAtLaunch !== undefined) out.autoUpdateAtLaunch = autoUpdateAtLaunch;
128
+ const launchCheckTimeoutMs = readPositiveInt(src, 'launchCheckTimeoutMs');
129
+ if (launchCheckTimeoutMs !== undefined) {
130
+ out.launchCheckTimeoutMs = Math.min(LAUNCH_CHECK_MAX_TIMEOUT_MS, Math.max(LAUNCH_CHECK_MIN_TIMEOUT_MS, launchCheckTimeoutMs));
131
+ }
132
+ return out;
133
+ }
134
+
102
135
  // ─── checkpoints.* — workspace checkpoint root-guard options ─────────────────
103
136
 
104
137
  /**
@@ -24,7 +24,7 @@ export function deriveComposerState(input: ComposerStateInput): ComposerState {
24
24
  // status tag: `risk:approval-wait`. It is therefore NOT also duplicated as a
25
25
  // `approval` flag, and (below) it suppresses the competing `state:` turn tag —
26
26
  // the turn is blocked on the user, not really streaming. Precedence:
27
- // approval-wait > live turn state. (UX-B item 4.)
27
+ // approval-wait > live turn state. (item 4.)
28
28
  const approvalWait = input.pendingApproval === true;
29
29
  if (approvalWait) {
30
30
  pendingRisk = 'approval-wait';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * conversation-line-cache.ts — per-message Line[] production cache (WO-209).
2
+ * conversation-line-cache.ts — per-message Line[] production cache.
3
3
  *
4
4
  * The measured defect (perf baseline 2026-07-03, transcript.build_1k): appending
5
5
  * ONE message to an N-message conversation re-rendered all N messages
@@ -98,7 +98,7 @@ interface KeyMeta {
98
98
  /**
99
99
  * True when this (assistant) message has a tool call with no result yet — it
100
100
  * renders as pending rather than done. Keyed so the entry invalidates and
101
- * re-renders ✓ once the tool result arrives. (UX-B item 2c.)
101
+ * re-renders ✓ once the tool result arrives. (item 2c.)
102
102
  */
103
103
  readonly hasPendingTool: boolean;
104
104
  }
@@ -286,7 +286,7 @@ export class MessageLineCache {
286
286
  };
287
287
 
288
288
  // Tool calls with no matching tool-result message are still pending; the
289
- // render context and the cache key both depend on this. (UX-B item 2c.)
289
+ // render context and the cache key both depend on this. (item 2c.)
290
290
  const completedToolCallIds = collectCompletedToolCallIds(messages);
291
291
  const renderContext: ConversationRenderContext = { ...context, completedToolCallIds };
292
292
 
@@ -46,7 +46,7 @@ type Message = ConversationMessageSnapshot;
46
46
  * Collect the ids of tool calls that have a matching tool-result message in the
47
47
  * given slice — i.e. the tools that actually ran. Used to decide whether an
48
48
  * assistant tool call still shows as pending (awaiting approval) or done.
49
- * (UX-B item 2c.)
49
+ * (item 2c.)
50
50
  */
51
51
  export function collectCompletedToolCallIds(messages: readonly Message[]): Set<string> {
52
52
  const ids = new Set<string>();
@@ -79,7 +79,7 @@ export interface ConversationRenderContext {
79
79
  * awaiting a decision (e.g. an approval prompt) and renders with a pending
80
80
  * glyph instead of the completed ✓. When undefined (single-message callers
81
81
  * without sibling context), every tool call renders as done — the prior
82
- * behaviour. (UX-B item 2c.)
82
+ * behaviour. (item 2c.)
83
83
  */
84
84
  readonly completedToolCallIds?: ReadonlySet<string>;
85
85
  }
@@ -273,7 +273,7 @@ export function renderConversationToolMessage(
273
273
  // Human one-line summary for tool results (write/read/exec/edit): shown as the
274
274
  // collapsed line so the transcript reads "wrote foo.txt (532 B)" instead of a
275
275
  // raw JSON blob; the full payload stays behind the expand toggle. Only for
276
- // 'tool' blocks (diffs render their own view). (UX-B item 3.)
276
+ // 'tool' blocks (diffs render their own view). (item 3.)
277
277
  const resultSummary = blockType === 'tool'
278
278
  ? summarizeToolResult(message.toolName, message.content)
279
279
  : null;
@@ -379,7 +379,7 @@ export function appendConversationMessages(
379
379
  const lineNumberMode = context.configManager?.get('display.lineNumbers') ?? 'off';
380
380
  const collapseThreshold = context.configManager?.get('display.collapseThreshold') ?? 30;
381
381
  // Derive pending vs done for tool calls from sibling tool-result messages,
382
- // unless the caller already supplied the set. (UX-B item 2c.)
382
+ // unless the caller already supplied the set. (item 2c.)
383
383
  const renderContext: ConversationRenderContext = context.completedToolCallIds !== undefined
384
384
  ? context
385
385
  : { ...context, completedToolCallIds: collectCompletedToolCallIds(messages) };
@@ -67,7 +67,7 @@ export interface OrchestratorUsageTotals {
67
67
  * TUI-side counterpart to SDK Orchestrator.usage: after a session resume
68
68
  * replays historical messages into a freshly-constructed Orchestrator (whose
69
69
  * `usage` starts at all zeros — bootstrap.ts always constructs a fresh
70
- * instance, see W0.9), this lets the caller hydrate the footer's token
70
+ * instance, see), this lets the caller hydrate the footer's token
71
71
  * counters from messages that already carry real usage data instead of
72
72
  * waiting for the first new turn to populate them.
73
73
  */
@@ -98,7 +98,7 @@ export class ConversationManager extends SdkConversationManager {
98
98
  /** When true the buffer needs to be rebuilt before the next display. */
99
99
  private dirty = true;
100
100
  /**
101
- * Per-message Line[] cache (WO-209). rebuildHistory() reuses cached lines for
101
+ * Per-message Line[] cache. rebuildHistory() reuses cached lines for
102
102
  * messages whose complete render inputs are unchanged, so appending one message
103
103
  * to an N-message conversation no longer re-renders all N. A cache-served
104
104
  * rebuild is byte-identical to a cold one (pure memoisation).
@@ -18,12 +18,12 @@
18
18
  * When neither target is available the function is an honest no-op (debug log
19
19
  * only; no user-facing error spam).
20
20
  *
21
- * Focus tracking (W2.3): when `focusTracker` is supplied, notifications are
21
+ * Focus tracking: when `focusTracker` is supplied, notifications are
22
22
  * gated the same way as the other unfocused-user alert classes (see
23
23
  * alert-gating.ts) — they fire when the terminal is unfocused or focus state
24
24
  * was never observed, and are suppressed when it's known to be focused,
25
25
  * unless `behavior.notifyOnlyWhenUnfocused` is turned off. `focusTracker` is
26
- * optional and defaults to "always fire" (the pre-W2.3 behavior) when
26
+ * optional and defaults to "always fire" (the behavior before focus gating existed) when
27
27
  * omitted, so existing callers that don't have a FocusTracker in scope are
28
28
  * unaffected.
29
29
  */
@@ -82,11 +82,11 @@ export interface MaybeNotifyLongTaskOptions {
82
82
  readonly webhookNotifier?: WebhookNotifier | null;
83
83
 
84
84
  /**
85
- * Terminal focus tracker (W2.3). When supplied together with `configGet`,
85
+ * Terminal focus tracker. When supplied together with `configGet`,
86
86
  * the notification additionally respects behavior.notifyOnlyWhenUnfocused
87
87
  * (default on): suppressed when the terminal is known to be focused, fires
88
88
  * when unfocused or when focus was never observed. Omit both to preserve
89
- * pre-W2.3 behavior (always fire once the threshold is met).
89
+ * the behavior before focus gating existed (always fire once the threshold is met).
90
90
  */
91
91
  readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
92
92
 
@@ -121,9 +121,9 @@ export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
121
121
  return false;
122
122
  }
123
123
 
124
- // Focus gate (W2.3): only applied when both a tracker and a config reader
125
- // are supplied. Absent either one, behavior is unchanged from pre-W2.3
126
- // (always fire once the threshold is met).
124
+ // Focus gate: only applied when both a tracker and a config reader
125
+ // are supplied. Absent either one, behavior is unchanged from before focus
126
+ // gating existed (always fire once the threshold is met).
127
127
  if (focusTracker && configGet && readNotifyOnlyWhenUnfocused(configGet) && !focusTracker.shouldAlertWhenUnfocused()) {
128
128
  logger.debug('long-task-notifier: suppressed — terminal focused');
129
129
  return false;
@@ -115,11 +115,11 @@ function isStreamRetryLikePayload(payload: unknown): payload is StreamRetryLikeP
115
115
 
116
116
  /**
117
117
  * Minimal cost lookup surface for attaching cost-delta information to failover notices.
118
- * Returns USD-per-1M-token pricing for the given model ID.
119
- * The implementation may consult a catalog; if the model is unknown both fields are 0.
118
+ * Returns USD-per-1M-token pricing for the given model ID, or null when the
119
+ * catalog honestly does not know the model (never a fabricated zero).
120
120
  */
121
121
  export interface FailoverCostLookup {
122
- getCostFromCatalog(modelId: string): { readonly input: number; readonly output: number };
122
+ getCostFromCatalog(modelId: string): { readonly input: number; readonly output: number } | null;
123
123
  }
124
124
 
125
125
  export interface WireStreamEventMetricsOptions {
@@ -219,9 +219,11 @@ function buildCostDeltaSuffix(
219
219
  // Registry key format: `provider:modelId` — modelId may itself contain `:`.
220
220
  const fromModelId = fromRegistryKey ? fromRegistryKey.split(':').slice(1).join(':') : '';
221
221
  const toModelId = toRegistryKey.split(':').slice(1).join(':');
222
- const fromCost = fromModelId ? lookup.getCostFromCatalog(fromModelId) : { input: 0, output: 0 };
223
- const toCost = lookup.getCostFromCatalog(toModelId);
224
- // Report unavailable when either side has zero pricing (unknown model).
222
+ // A null catalog answer means honestly unknown — treated exactly like the
223
+ // legacy zero-pricing sentinel below.
224
+ const fromCost = (fromModelId ? lookup.getCostFromCatalog(fromModelId) : null) ?? { input: 0, output: 0 };
225
+ const toCost = lookup.getCostFromCatalog(toModelId) ?? { input: 0, output: 0 };
226
+ // Report unavailable when either side has no pricing (unknown model).
225
227
  if (fromCost.input === 0 && fromCost.output === 0 && !fromModelId) {
226
228
  return ' [cost data unavailable]';
227
229
  }
@@ -4,7 +4,7 @@
4
4
  * source these rules catch is emitted (from the SDK) through
5
5
  * SystemMessageRouter, so this one funnel is where first-run plumbing is kept
6
6
  * out of the user's stream while the information stays reachable elsewhere
7
- * (fleet panel, /health, /model, activity log). (UX-B item 1.)
7
+ * (fleet panel, /health, /model, activity log). (item 1.)
8
8
  */
9
9
 
10
10
  export interface NoiseGateDeps {
@@ -9,7 +9,7 @@
9
9
  * that target — plus whether a panel is attached — into a { toPanel,
10
10
  * toConversation } decision.
11
11
  *
12
- * W6.1 (the purge): the SystemMessagesPanel this router used to optionally
12
+ * (the purge): the SystemMessagesPanel this router used to optionally
13
13
  * push into was DELETE-disposition (no surviving human surface — a picker
14
14
  * over the old panel registry, not something worth a dedicated console) and
15
15
  * has been removed entirely, so this router now always resolves with
@@ -61,7 +61,7 @@ export type {
61
61
  SystemMessageTarget,
62
62
  } from '@/runtime/index.ts';
63
63
 
64
- /** Panel emphasis level. Panel delivery was removed in W6.1 (see file doc); kept as the priority vocabulary for callers and for the SDK's delivery-resolution signature. */
64
+ /** Panel emphasis level. Panel delivery was removed in (see file doc); kept as the priority vocabulary for callers and for the SDK's delivery-resolution signature. */
65
65
  export type SystemMessagePriority = 'high' | 'low';
66
66
 
67
67
  /**
@@ -88,7 +88,7 @@ function mustReachConversation(message: string): boolean {
88
88
 
89
89
  /**
90
90
  * Routes system messages to the conversation based on priority level and
91
- * configured target. See file doc for the W6.1 panel removal.
91
+ * configured target. See file doc for the panel removal.
92
92
  */
93
93
  export class SystemMessageRouter {
94
94
  /** Buffered provider "from last session" replay lines, folded on a microtask. */
@@ -123,7 +123,7 @@ export class SystemMessageRouter {
123
123
  kind: SystemMessageKind,
124
124
  ): void {
125
125
  // Noise gate — keep first-run plumbing out of the transcript while the
126
- // information stays reachable via other live surfaces. (UX-B item 1.)
126
+ // information stays reachable via other live surfaces. (item 1.)
127
127
  const verdict = classifyNoise(message, this.noiseDeps);
128
128
  if (verdict.action === 'drop') return;
129
129
  if (verdict.action === 'foldProviderReplay') {
@@ -137,7 +137,7 @@ export class SystemMessageRouter {
137
137
  /** Post-noise-gate delivery: resolve target and append to the conversation. */
138
138
  private deliver(message: string, kind: SystemMessageKind): void {
139
139
  const target = this.getTargetForKind(kind);
140
- // hasPanel is always false post-W6.1 — resolveSystemMessageDelivery's own
140
+ // hasPanel is always false now that panel delivery was removed — resolveSystemMessageDelivery's own
141
141
  // contract means every target ('panel' | 'conversation' | 'both')
142
142
  // resolves toConversation: true in that case (see file doc).
143
143
  const delivery = resolveSystemMessageDelivery(target, false);
@@ -153,7 +153,7 @@ export class SystemMessageRouter {
153
153
  * Buffer a provider "from last session" replay line and schedule a microtask
154
154
  * flush. The SDK emits the whole persisted-provider burst synchronously, so a
155
155
  * single microtask captures the full burst and folds it to one quiet line.
156
- * (UX-B item 1b.)
156
+ * (item 1b.)
157
157
  */
158
158
  private bufferProviderReplay(message: string): void {
159
159
  this.providerReplayBuffer.push(message);
@@ -5,7 +5,7 @@ import type { SpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
5
5
  * Builds the shared "cancel the active turn" action: stops any in-flight
6
6
  * spoken output, then aborts the orchestrator's turn if one is actively
7
7
  * streaming. Extracted from main.ts so the file can stay under its 800-line
8
- * cap while still wiring `commandContext.isGenerating` (see W1.6 FIX 1).
8
+ * cap while still wiring `commandContext.isGenerating` (see FIX 1).
9
9
  *
10
10
  * Returns whether SPEECH was actively stopped — a Ctrl+C that silenced live
11
11
  * TTS is consumed by that job (an earlier replay fix), same as a press that
@@ -78,12 +78,12 @@ export interface WireTurnEventHandlersOptions {
78
78
  */
79
79
  readonly webhookNotifier?: WebhookNotifier | null;
80
80
  /**
81
- * Terminal focus tracker (W2.3). Gates the long-task, budget-breach, and
81
+ * Terminal focus tracker. Gates the long-task, budget-breach, and
82
82
  * agent/chain-failure desktop alerts wired in this module — see
83
- * alert-gating.ts. Optional; when absent, none of the new W2.3 alert
84
- * behavior is gated by focus (long-task keeps its pre-W2.3 always-fire
85
- * behavior, and budget-breach/failure alerts are skipped entirely, since
86
- * they are new-in-W2.3 and have no pre-existing unconditional-fire path
83
+ * alert-gating.ts. Optional; when absent, none of the new alert
84
+ * behavior is gated by focus (long-task keeps its always-fire
85
+ * behavior from before focus gating existed, and budget-breach/failure alerts are
86
+ * skipped entirely, since they have no pre-existing unconditional-fire path
87
87
  * to fall back to).
88
88
  */
89
89
  readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
@@ -152,7 +152,7 @@ export function wireTurnEventHandlers(
152
152
  // Track turn start time for long-task notification threshold.
153
153
  let turnStartTime: number | null = null;
154
154
 
155
- // Budget-breach edge-trigger checker (W2.3) — one instance per session,
155
+ // Budget-breach edge-trigger checker — one instance per session,
156
156
  // piggybacking on the same TURN_COMPLETED handler as the long-task
157
157
  // notification below rather than adding a second TURN_COMPLETED subscription.
158
158
  const budgetBreachNotifier: BudgetBreachNotifier | null = focusTracker
@@ -193,7 +193,7 @@ export function wireTurnEventHandlers(
193
193
  focusTracker,
194
194
  configGet,
195
195
  });
196
- // Budget-breach alert (W2.3): edge-triggered, piggybacks on this same
196
+ // Budget-breach alert: edge-triggered, piggybacks on this same
197
197
  // TURN_COMPLETED handler rather than a second subscription.
198
198
  if (budgetBreachNotifier) {
199
199
  const sessionModel = providerRegistry.getCurrentModel().id ?? 'unknown';
@@ -264,7 +264,7 @@ export function wireTurnEventHandlers(
264
264
  refreshGit();
265
265
  }));
266
266
 
267
- // Agent/chain-failure desktop alerts (W2.3). The SDK's WebhookNotifier and
267
+ // Agent/chain-failure desktop alerts. The SDK's WebhookNotifier and
268
268
  // Notifier already fire webhook/Slack/Discord notifications for these two
269
269
  // events unconditionally (attachToRuntimeBus in the SDK's
270
270
  // platform/integrations — pre-existing, not focus-gated: an out-of-band
package/src/daemon/cli.ts CHANGED
@@ -4,8 +4,7 @@ import { readFileSync } from 'node:fs';
4
4
  import { ConfigManager, resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
5
5
  import { formatProviderModel, getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
6
6
  import { RuntimeEventBus, GlobalNetworkTransportInstaller } from '@/runtime/index.ts';
7
- import { createFeatureFlagManager } from '@/runtime/index.ts';
8
- import type { FlagState } from '@/runtime/index.ts';
7
+ import { bindFeatureSettingsBridge, createFeatureFlagManager, deriveFeatureStates } from '@/runtime/index.ts';
9
8
  import { createRuntimeStore } from '../runtime/store/index.ts';
10
9
  import { createRuntimeServices } from '../runtime/services.ts';
11
10
  import { DaemonServer, HttpListener } from '@pellux/goodvibes-sdk/platform/daemon';
@@ -28,6 +27,8 @@ import {
28
27
  } from '@pellux/goodvibes-sdk/platform/discovery';
29
28
  import { createSafeHostServeFactory } from './safe-serve.ts';
30
29
  import { isDaemonServiceSubcommand, resolveInstalledDaemonBinary, runDaemonServiceCli } from './service-commands.ts';
30
+ import { resolveDaemonUpdateArtifact } from './lifecycle.ts';
31
+ import { VERSION } from '../version.ts';
31
32
 
32
33
  import {
33
34
  parseGoodVibesCli,
@@ -190,10 +191,11 @@ async function main(): Promise<void> {
190
191
 
191
192
  const runtimeBus = new RuntimeEventBus();
192
193
  const runtimeStore = createRuntimeStore();
194
+ // Gate states derive from the domain settings keys; the bridge keeps live
195
+ // config changes flowing into the manager for the daemon's lifetime.
193
196
  const featureFlags = createFeatureFlagManager();
194
- featureFlags.loadFromConfig({
195
- flags: (config.getCategory('featureFlags') as Record<string, FlagState>) ?? {},
196
- });
197
+ featureFlags.loadFromConfig({ flags: deriveFeatureStates(config) });
198
+ bindFeatureSettingsBridge(config, featureFlags);
197
199
  const runtimeServices = createRuntimeServices({
198
200
  configManager: config,
199
201
  featureFlags,
@@ -233,6 +235,11 @@ async function main(): Promise<void> {
233
235
  userAuth,
234
236
  runtimeServices,
235
237
  serveFactory: createSafeHostServeFactory('Standalone daemon'),
238
+ // Hand the facade this binary's real version + exec path so its self-update
239
+ // lifecycle compares the HOST's version (not the sdk package's). Resolves to
240
+ // undefined for a non-binary install, so a dev run stays host-managed and
241
+ // never swaps the interpreter. See lifecycle.ts.
242
+ updateArtifact: resolveDaemonUpdateArtifact({ version: VERSION }),
236
243
  });
237
244
  const listener = new HttpListener({
238
245
  hookDispatcher: runtimeServices.hookDispatcher,
@@ -268,6 +275,11 @@ async function main(): Promise<void> {
268
275
  config.get('danger.httpListener') ? listener.start() : Promise.resolve(),
269
276
  ]);
270
277
 
278
+ // The DaemonServer facade owns the hourly self-update loop now (fed this
279
+ // binary's version + exec path via updateArtifact above); it checks hourly,
280
+ // swaps only at an idle moment, keeps the outgoing binary at `<path>.previous`,
281
+ // and stops with the daemon. No separate host loop.
282
+
271
283
  const abortController = new AbortController();
272
284
 
273
285
  const shutdown = async (): Promise<void> => {
@@ -313,6 +325,9 @@ async function main(): Promise<void> {
313
325
  daemonUrl,
314
326
  token: companionTokenRecord.token,
315
327
  password: bootstrapPassword,
328
+ // Without this the SDK defaults the printed banner header to v0.0.0 —
329
+ // the visible version must be this binary's real version.
330
+ version: VERSION,
316
331
  surface: 'tui',
317
332
  });
318
333
  const payload = encodeConnectionPayload(connectionInfo);