@pellux/goodvibes-tui 1.13.0 → 1.14.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 (157) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +6 -6
  3. package/bin/goodvibes +6 -0
  4. package/bin/goodvibes-daemon +6 -0
  5. package/bin/launcher-support.js +34 -1
  6. package/docs/foundation-artifacts/operator-contract.json +8076 -2047
  7. package/package.json +15 -11
  8. package/scripts/postinstall.js +73 -48
  9. package/src/cli/bundle-command.ts +5 -5
  10. package/src/cli/completion.ts +1 -1
  11. package/src/cli/completions/generate.ts +15 -3
  12. package/src/cli/doctor.ts +385 -0
  13. package/src/cli/entrypoint.ts +72 -1
  14. package/src/cli/help.ts +29 -9
  15. package/src/cli/hooks-command.ts +74 -0
  16. package/src/cli/hooks-report.ts +167 -0
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/management.ts +13 -1
  19. package/src/cli/parser.ts +10 -2
  20. package/src/cli/plugin-bundles-command.ts +228 -0
  21. package/src/cli/plugin-command.ts +179 -0
  22. package/src/cli/service-posture.ts +10 -0
  23. package/src/cli/status.ts +275 -2
  24. package/src/cli/tui-startup.ts +119 -1
  25. package/src/cli/types.ts +3 -1
  26. package/src/config/index.ts +16 -0
  27. package/src/config/tui-extension-settings.ts +169 -0
  28. package/src/core/approval-alert.ts +15 -2
  29. package/src/core/compaction-receipt.ts +127 -0
  30. package/src/core/model-routing-chip.ts +82 -0
  31. package/src/core/permission-mode.ts +89 -0
  32. package/src/core/rewind-receipt.ts +83 -0
  33. package/src/core/rewind-turn-anchors.ts +160 -0
  34. package/src/core/scriptable-statusline.ts +141 -0
  35. package/src/core/session-auto-titler.ts +110 -0
  36. package/src/core/stream-event-wiring.ts +20 -0
  37. package/src/core/system-message-router.ts +1 -1
  38. package/src/core/terminal-notifier.ts +102 -0
  39. package/src/core/turn-event-wiring.ts +80 -1
  40. package/src/input/command-reference.ts +96 -0
  41. package/src/input/command-registry.ts +25 -0
  42. package/src/input/commands/channel-pairing.ts +245 -0
  43. package/src/input/commands/channel-runtime.ts +152 -6
  44. package/src/input/commands/checkin-runtime.ts +89 -0
  45. package/src/input/commands/checkpoint-runtime.ts +7 -179
  46. package/src/input/commands/ci-runtime.ts +207 -0
  47. package/src/input/commands/cost-attribution-format.ts +52 -0
  48. package/src/input/commands/cost-runtime.ts +44 -4
  49. package/src/input/commands/editor-runtime.ts +22 -0
  50. package/src/input/commands/flags-runtime.ts +328 -0
  51. package/src/input/commands/health-metrics-format.ts +67 -0
  52. package/src/input/commands/health-runtime.ts +55 -1
  53. package/src/input/commands/memory.ts +23 -0
  54. package/src/input/commands/operator-rpc.ts +95 -0
  55. package/src/input/commands/palette-runtime.ts +22 -0
  56. package/src/input/commands/permissions-provenance.ts +264 -0
  57. package/src/input/commands/permissions-runtime.ts +21 -0
  58. package/src/input/commands/planning-runtime.ts +28 -1
  59. package/src/input/commands/platform-access-runtime.ts +2 -97
  60. package/src/input/commands/principals-runtime.ts +212 -0
  61. package/src/input/commands/product-runtime.ts +43 -3
  62. package/src/input/commands/recall-files-config.ts +54 -0
  63. package/src/input/commands/recall-files-git.ts +57 -0
  64. package/src/input/commands/recall-files.ts +185 -0
  65. package/src/input/commands/recall-query.ts +8 -3
  66. package/src/input/commands/recall-review.ts +2 -2
  67. package/src/input/commands/recall-shared.ts +13 -0
  68. package/src/input/commands/relay-runtime.ts +86 -0
  69. package/src/input/commands/review-runtime.ts +160 -0
  70. package/src/input/commands/rewind-runtime.ts +362 -0
  71. package/src/input/commands/schedule-nl.ts +147 -0
  72. package/src/input/commands/schedule-runtime.ts +29 -10
  73. package/src/input/commands/secret-runtime.ts +55 -0
  74. package/src/input/commands/session-content.ts +19 -8
  75. package/src/input/commands/session-workflow.ts +10 -0
  76. package/src/input/commands/update-runtime.ts +363 -0
  77. package/src/input/commands/workstream-attempt-dependents.ts +103 -0
  78. package/src/input/commands/workstream-attempts.ts +244 -0
  79. package/src/input/commands/workstream-runtime.ts +164 -26
  80. package/src/input/commands/worktree-runtime.ts +56 -2
  81. package/src/input/commands.ts +135 -80
  82. package/src/input/composer-capture.ts +67 -0
  83. package/src/input/composer-editor.ts +134 -0
  84. package/src/input/concealed-input.ts +94 -0
  85. package/src/input/exec-env-scrub-config.ts +84 -0
  86. package/src/input/feature-unit-layout.ts +142 -0
  87. package/src/input/feed-context-factory.ts +2 -0
  88. package/src/input/handler-feed-routes.ts +17 -0
  89. package/src/input/handler-feed.ts +3 -0
  90. package/src/input/handler-modal-routes.ts +5 -2
  91. package/src/input/handler-shortcuts.ts +37 -0
  92. package/src/input/handler.ts +24 -2
  93. package/src/input/keybindings.ts +11 -2
  94. package/src/input/onboarding/onboarding-feature-units.ts +536 -0
  95. package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
  96. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
  97. package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
  98. package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
  99. package/src/input/relay-settings-descriptions.ts +35 -0
  100. package/src/input/sandbox-exec-config.ts +107 -0
  101. package/src/input/settings-modal-adjustment.ts +4 -7
  102. package/src/input/settings-modal-data.ts +178 -8
  103. package/src/input/settings-modal-mutations.ts +41 -13
  104. package/src/input/settings-modal-types.ts +42 -5
  105. package/src/input/settings-modal.ts +67 -15
  106. package/src/input/submission-router.ts +4 -1
  107. package/src/input/worktree-setup-config.ts +93 -0
  108. package/src/main.ts +43 -43
  109. package/src/panels/builtin/development.ts +14 -0
  110. package/src/panels/diff-review-model.ts +189 -0
  111. package/src/panels/diff-review-panel.ts +395 -0
  112. package/src/panels/fleet-panel-format.ts +114 -2
  113. package/src/panels/fleet-panel.ts +65 -78
  114. package/src/panels/fleet-read-model.ts +91 -11
  115. package/src/panels/fleet-stop.ts +19 -4
  116. package/src/panels/modals/memory-modal.ts +11 -2
  117. package/src/permissions/hunk-selection.ts +86 -5
  118. package/src/permissions/prompt.ts +108 -7
  119. package/src/permissions/sandbox-exec-gate.ts +162 -0
  120. package/src/renderer/settings-modal-helpers.ts +21 -1
  121. package/src/renderer/settings-modal.ts +70 -44
  122. package/src/renderer/shell-surface.ts +23 -1
  123. package/src/renderer/system-message.ts +4 -0
  124. package/src/renderer/tool-result-summary.ts +95 -5
  125. package/src/renderer/ui-factory.ts +22 -0
  126. package/src/runtime/bootstrap-command-context.ts +15 -1
  127. package/src/runtime/bootstrap-command-parts.ts +14 -2
  128. package/src/runtime/bootstrap-core.ts +38 -38
  129. package/src/runtime/bootstrap-notifier-sync.ts +41 -0
  130. package/src/runtime/bootstrap-shell.ts +8 -0
  131. package/src/runtime/bootstrap.ts +21 -23
  132. package/src/runtime/context-accounting-source.ts +103 -0
  133. package/src/runtime/conversation-rewind-port.ts +130 -0
  134. package/src/runtime/fleet-needs-input-push.ts +70 -0
  135. package/src/runtime/install-self-check-startup.ts +36 -0
  136. package/src/runtime/install-self-check.ts +149 -0
  137. package/src/runtime/onboarding/fast-path.ts +93 -0
  138. package/src/runtime/process-lifecycle.ts +32 -24
  139. package/src/runtime/relay-reachability-bridge.ts +53 -0
  140. package/src/runtime/release-artifacts.ts +79 -0
  141. package/src/runtime/services.ts +38 -37
  142. package/src/runtime/trust/workspace-registration.ts +184 -0
  143. package/src/runtime/trust/workspace-trust.ts +202 -0
  144. package/src/runtime/ui-services.ts +2 -1
  145. package/src/runtime/update-check.ts +113 -0
  146. package/src/runtime/workspace-checkpointing.ts +32 -0
  147. package/src/runtime/workstream-attempts-validation.ts +77 -0
  148. package/src/runtime/workstream-draft-edits.ts +95 -0
  149. package/src/runtime/workstream-draft-store.ts +101 -0
  150. package/src/runtime/workstream-draft-types.ts +42 -0
  151. package/src/runtime/workstream-services.ts +70 -39
  152. package/src/shell/blocking-input.ts +29 -0
  153. package/src/shell/ui-openers.ts +65 -0
  154. package/src/verification/verification-ledger.ts +56 -2
  155. package/src/version.ts +1 -1
  156. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
  157. package/src/runtime/render-scheduler.ts +0 -90
@@ -0,0 +1,93 @@
1
+ /**
2
+ * fast-path.ts — minimum-steps first run.
3
+ *
4
+ * The full onboarding wizard is comprehensive but long (5+ steps for a brand
5
+ * new user). The fast path gets a new user to a working session in the fewest
6
+ * possible steps:
7
+ * - If a provider API key is already configured (env / secrets), skip
8
+ * onboarding entirely — write the check marker and drop straight into a
9
+ * working session. ZERO steps.
10
+ * - Otherwise ask exactly one question: start now on the free default model,
11
+ * or run the full guided setup. Dismissing (Esc) starts now — so it is
12
+ * always skippable.
13
+ *
14
+ * The full wizard remains one keystroke away (the "Full guided setup" choice,
15
+ * or `/onboarding` at any time). If the command context can't answer whether a
16
+ * provider is configured (e.g. a headless/test surface), we fall back to the
17
+ * classic behavior of opening the wizard, so nothing regresses.
18
+ */
19
+ import type { CommandContext } from '../../input/command-registry.ts';
20
+ import type { SelectionItem } from '../../input/selection-modal.ts';
21
+ import { writeOnboardingCheckMarker } from './markers.ts';
22
+ import type { WriteOnboardingCheckMarkerOptions } from './types.ts';
23
+
24
+ type OnboardingMarkerShellPaths = Parameters<typeof writeOnboardingCheckMarker>[0];
25
+
26
+ export interface OnboardingFastPathDeps {
27
+ readonly input: { openOnboardingWizard: (options: { mode: 'new'; reset: boolean }) => void };
28
+ readonly commandContext: CommandContext;
29
+ readonly shellPaths: OnboardingMarkerShellPaths;
30
+ readonly render: () => void;
31
+ }
32
+
33
+ /** Read configured provider ids defensively — returns [] if the surface can't answer. */
34
+ function readConfiguredProviderIds(commandContext: CommandContext): string[] | null {
35
+ const registry = commandContext.provider?.providerRegistry;
36
+ if (!registry || typeof registry.getConfiguredProviderIds !== 'function') return null;
37
+ try {
38
+ return registry.getConfiguredProviderIds();
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ const MARKER_OPTIONS: WriteOnboardingCheckMarkerOptions = { source: 'command' };
45
+
46
+ /**
47
+ * Run the fast-path first-run flow. Returns nothing; side effects (wizard open,
48
+ * marker write, modal) happen through the injected deps.
49
+ */
50
+ export function startOnboardingFastPath(deps: OnboardingFastPathDeps): void {
51
+ const { input, commandContext, shellPaths, render } = deps;
52
+ const openWizard = (): void => input.openOnboardingWizard({ mode: 'new', reset: true });
53
+
54
+ const configured = readConfiguredProviderIds(commandContext);
55
+ // Fall back to the classic wizard if we can't detect providers or can't prompt.
56
+ if (configured === null || !commandContext.openSelection) {
57
+ openWizard();
58
+ return;
59
+ }
60
+
61
+ if (configured.length > 0) {
62
+ // A provider key already exists — nothing to ask. Straight to a working session.
63
+ writeOnboardingCheckMarker(shellPaths, MARKER_OPTIONS);
64
+ commandContext.print?.(
65
+ `Ready — provider configured (${configured.join(', ')}). Run /onboarding anytime for full setup.`,
66
+ );
67
+ render();
68
+ return;
69
+ }
70
+
71
+ const items: SelectionItem[] = [
72
+ { id: 'start-now', label: 'Start now', detail: 'Use the free default model — no API key needed', primaryAction: 'select' },
73
+ { id: 'full-setup', label: 'Full guided setup', detail: 'Choose provider, model, channels, and more', primaryAction: 'select' },
74
+ ];
75
+ commandContext.openSelection(
76
+ 'Welcome to GoodVibes — get started',
77
+ items,
78
+ { allowSearch: false, primaryVerbLabel: 'Choose' },
79
+ (result) => {
80
+ if (result?.item.id === 'full-setup') {
81
+ openWizard();
82
+ return;
83
+ }
84
+ // Start now (or dismissed): mark first-run done and drop into a working
85
+ // session on the free default model.
86
+ writeOnboardingCheckMarker(shellPaths, MARKER_OPTIONS);
87
+ commandContext.print?.(
88
+ 'You\'re on the free default model. Add a provider key anytime with /provider, or run /onboarding for full setup.',
89
+ );
90
+ render();
91
+ },
92
+ );
93
+ }
@@ -19,6 +19,7 @@
19
19
  * `unsubs` registry is shared by reference and drained on exit.
20
20
  */
21
21
  import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
22
+ import { createTerminalLifecycle, TERMINAL_ESCAPES } from '@pellux/goodvibes-terminal-shell';
22
23
  import { formatUserFacingErrorLine } from '../core/format-user-error.ts';
23
24
  import { allowTerminalWrite } from './terminal-output-guard.ts';
24
25
  import { buildPersistedSessionContext, deleteRecoveryFile } from '@/runtime/index.ts';
@@ -129,30 +130,37 @@ export function installProcessLifecycle(deps: ProcessLifecycleDeps): ProcessLife
129
130
  render();
130
131
  };
131
132
 
132
- let terminalRestored = false;
133
+ // Terminal enter/restore sequencing is the byte-identical seam both daemon
134
+ // front-ends share, so it lives in @pellux/goodvibes-terminal-shell and this
135
+ // wiring delegates to it. Only the restore path is used here (the TUI's
136
+ // startup enter sequence stays in main.ts); the graceful shutdown below
137
+ // (draining services, persisting sessions, exit codes) is TUI-specific and
138
+ // stays local, calling restoreTerminal() for the synchronous hand-back.
139
+ //
140
+ // The injected `ansi` restore sequences are mapped onto the package's escape
141
+ // set so the exact bytes the TUI has always emitted are preserved. The no-alt
142
+ // exit deliberately uses the package's CLEAR_VIEWPORT_HOME ('\x1b[2J\x1b[H',
143
+ // no ESC[3J) — never ansi.CLEAR_SCREEN, which carries the scrollback-wiping 3J.
144
+ const terminalLifecycle = createTerminalLifecycle({
145
+ write: (data) => { stdout.write(data); },
146
+ noAltScreen,
147
+ guardedWrite: allowTerminalWrite,
148
+ disposeOutputGuard: () => getTerminalOutputGuard().dispose(),
149
+ setRawMode: (enabled) => stdin.setRawMode(enabled),
150
+ escapes: {
151
+ ...TERMINAL_ESCAPES,
152
+ ALT_SCREEN_EXIT: ansi.ALT_SCREEN_EXIT,
153
+ PASTE_DISABLE: ansi.PASTE_DISABLE,
154
+ KEYBOARD_EXT_DISABLE: ansi.KEYBOARD_EXT_DISABLE,
155
+ MOUSE_DISABLE: ansi.MOUSE_DISABLE,
156
+ CURSOR_SHOW: ansi.CURSOR_SHOW,
157
+ FOCUS_DISABLE: ansi.FOCUS_DISABLE,
158
+ },
159
+ });
133
160
  // Idempotent, synchronous-only terminal restore. Safe to call from process.on('exit'),
134
- // signal handlers, uncaughtException, and exitApp. Disposes the output guard FIRST so a
135
- // crash stack reaches the real stderr instead of being suppressed by the guard.
136
- const restoreTerminal = (): void => {
137
- if (terminalRestored) return;
138
- terminalRestored = true;
139
- // Alt-screen path: just leave the alt screen — 1049l restores the primary
140
- // screen and cursor exactly as they were at launch. Clearing first is
141
- // pointless (the alt screen is discarded) and actively harmful: the old
142
- // CLEAR_SCREEN included ESC[3J, which wipes the PRIMARY scrollback on
143
- // several emulators even when issued from the alt screen.
144
- // No-alt path: the compositor painted over the primary screen, so clear
145
- // the viewport and home the cursor — but WITHOUT 3J, the user's scrollback
146
- // is theirs. CURSOR_SHOW goes AFTER the screen switch so visibility
147
- // applies to the screen the shell prompt lands on.
148
- const exitScreen = noAltScreen ? '\x1b[2J\x1b[H' : ansi.ALT_SCREEN_EXIT;
149
- allowTerminalWrite(() => stdout.write(
150
- ansi.PASTE_DISABLE + ansi.KEYBOARD_EXT_DISABLE + ansi.MOUSE_DISABLE + ansi.FOCUS_DISABLE
151
- + exitScreen + ansi.CURSOR_SHOW,
152
- ));
153
- getTerminalOutputGuard().dispose();
154
- try { stdin.setRawMode(false); } catch { /* stdin may not be a TTY */ }
155
- };
161
+ // signal handlers, uncaughtException, and exitApp. Disposes the output guard AFTER the
162
+ // restore write so a crash stack reaches the real stderr instead of being suppressed.
163
+ const restoreTerminal = terminalLifecycle.restoreTerminal;
156
164
 
157
165
  const uncaughtExceptionHandler = (err: Error): void => {
158
166
  restoreTerminal();
@@ -220,7 +228,7 @@ export function installProcessLifecycle(deps: ProcessLifecycleDeps): ProcessLife
220
228
  return {
221
229
  exitApp,
222
230
  restoreTerminal,
223
- isTerminalRestored: () => terminalRestored,
231
+ isTerminalRestored: terminalLifecycle.isTerminalRestored,
224
232
  resizeHandler,
225
233
  sigintHandler,
226
234
  unhandledRejectionHandler,
@@ -0,0 +1,53 @@
1
+ /**
2
+ * relay-reachability-bridge.ts — reads the LIVE outbound-relay reachability
3
+ * state off the running embedded/standalone DaemonServer.
4
+ *
5
+ * The SDK's `DaemonServer` composes and owns a `RelayReachability` controller
6
+ * internally (gated by `relay.enabled` + the `relay-connect` feature flag) and
7
+ * exposes it via `getRelayReachability(): RelayReachability | null` — but that
8
+ * accessor is not on the narrow `DaemonService` interface `HostServicesHandle`
9
+ * types its `daemonServer` field with (see
10
+ * `@pellux/goodvibes-sdk/platform/runtime/bootstrap-services`), so the field
11
+ * needs a defensive structural check before use — same pattern
12
+ * `waitForConfigDrivenRestarts` in bootstrap.ts already uses for
13
+ * `waitForRestart`. `platform/relay` itself has no public export subpath
14
+ * (daemon-internal), so the RelayReachability shape is described locally as a
15
+ * minimal structural type rather than imported by name.
16
+ */
17
+
18
+ import type { RelayRegistrationStatus } from '@pellux/goodvibes-daemon-sdk';
19
+ import { encodeRelayPairingString, type RelayPairingPayload } from '@pellux/goodvibes-transport-core/relay';
20
+
21
+ /** Structural subset of the SDK's `RelayReachability` this bridge needs. */
22
+ interface RelayReachabilityLike {
23
+ readonly status: RelayRegistrationStatus | 'disabled';
24
+ mintPairing(): Promise<RelayPairingPayload | null>;
25
+ }
26
+
27
+ /** Structural subset of a live daemon that may expose relay reachability. */
28
+ interface RelayCapableDaemonService {
29
+ getRelayReachability?(): RelayReachabilityLike | null;
30
+ }
31
+
32
+ function getRelay(daemonServer: unknown): RelayReachabilityLike | null {
33
+ const candidate = daemonServer as RelayCapableDaemonService | null;
34
+ if (!candidate || typeof candidate.getRelayReachability !== 'function') return null;
35
+ return candidate.getRelayReachability() ?? null;
36
+ }
37
+
38
+ /** Two live-relay accessors, spread into `externalServices` from bootstrap.ts. */
39
+ export function buildRelayExternalServiceMethods(getDaemonServer: () => unknown): {
40
+ relayStatus(): RelayRegistrationStatus | 'disabled';
41
+ mintRelayPairing(): Promise<{ readonly payload: RelayPairingPayload; readonly encoded: string } | null>;
42
+ } {
43
+ return {
44
+ relayStatus: () => getRelay(getDaemonServer())?.status ?? 'disabled',
45
+ mintRelayPairing: async () => {
46
+ const relay = getRelay(getDaemonServer());
47
+ if (!relay) return null;
48
+ const payload = await relay.mintPairing();
49
+ if (!payload) return null;
50
+ return { payload, encoded: encodeRelayPairingString(payload) };
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shared release-artifact naming and checksum-verification logic.
3
+ *
4
+ * This is the one place that knows the release asset naming convention
5
+ * (`goodvibes[-daemon]-{platform}-{arch}`) and how to verify a downloaded
6
+ * artifact against SHA256SUMS.txt. Two callers share it and must stay in
7
+ * lockstep:
8
+ * - scripts/postinstall.js (npm install-time binary download)
9
+ * - src/input/commands/update-runtime.ts (the /update command's self-update)
10
+ *
11
+ * A downloaded artifact with NO entry in the checksum manifest is just as
12
+ * unverified as one with a mismatching entry — verifyChecksum() throws in
13
+ * both cases, never silently skips.
14
+ */
15
+ import { createHash } from 'node:crypto';
16
+
17
+ export const CHECKSUM_MANIFEST_NAME = 'SHA256SUMS.txt';
18
+
19
+ export type SupportedPlatform = 'linux' | 'darwin';
20
+ export type SupportedArch = 'x64' | 'arm64';
21
+
22
+ export interface ReleaseArtifactNames {
23
+ readonly app: string;
24
+ readonly daemon: string;
25
+ }
26
+
27
+ /** Release-asset platform tag as used in artifact filenames ("linux" | "macos"). */
28
+ const PLATFORM_TAGS: Record<SupportedPlatform, string> = {
29
+ linux: 'linux',
30
+ darwin: 'macos',
31
+ };
32
+
33
+ export function resolveArtifactNames(platform: string, arch: string): ReleaseArtifactNames | null {
34
+ const platformTag = PLATFORM_TAGS[platform as SupportedPlatform];
35
+ if (!platformTag || (arch !== 'x64' && arch !== 'arm64')) {
36
+ return null;
37
+ }
38
+ const suffix = `${platformTag}-${arch}`;
39
+ return {
40
+ app: `goodvibes-${suffix}`,
41
+ daemon: `goodvibes-daemon-${suffix}`,
42
+ };
43
+ }
44
+
45
+ export function sha256(buffer: Buffer | Uint8Array): string {
46
+ return createHash('sha256').update(buffer).digest('hex');
47
+ }
48
+
49
+ export function parseChecksumFile(contents: string): Map<string, string> {
50
+ const checksums = new Map<string, string>();
51
+ for (const rawLine of contents.split(/\r?\n/)) {
52
+ const line = rawLine.trim();
53
+ if (!line) continue;
54
+ const match = line.match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
55
+ if (!match) continue;
56
+ checksums.set(match[2], match[1].toLowerCase());
57
+ }
58
+ return checksums;
59
+ }
60
+
61
+ /**
62
+ * Verify a downloaded artifact's checksum against the parsed manifest.
63
+ * An artifact with no entry in the manifest is a hard failure, identical
64
+ * in severity to a mismatching entry — never treated as "unverifiable, so
65
+ * skip the check". Throws naming the artifact and the manifest.
66
+ */
67
+ export function verifyChecksum(
68
+ artifactName: string,
69
+ actual: string,
70
+ expected: string | undefined,
71
+ manifestName: string = CHECKSUM_MANIFEST_NAME,
72
+ ): void {
73
+ if (expected === undefined) {
74
+ throw new Error(`no checksum entry for ${artifactName} in ${manifestName} — refusing to install an unverified binary`);
75
+ }
76
+ if (expected !== actual) {
77
+ throw new Error(`checksum mismatch for ${artifactName}: expected ${expected}, got ${actual}`);
78
+ }
79
+ }
@@ -5,6 +5,8 @@ import { SecretsManager } from '../config/secrets.ts';
5
5
  import { AutomationDeliveryManager, AutomationManager, AutomationRouteStore } from '@pellux/goodvibes-sdk/platform/automation';
6
6
  import { ChannelDeliveryRouter, ChannelPluginRegistry, ChannelPolicyManager, RouteBindingManager, SurfaceRegistry } from '@pellux/goodvibes-sdk/platform/channels';
7
7
  import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
8
+ import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
9
+ import { attachWsOnlyGatewayVerbHandlers, createArchivableFleetRegistry } from '@pellux/goodvibes-terminal-shell';
8
10
  import { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
9
11
  import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
10
12
  import {
@@ -22,16 +24,19 @@ import {
22
24
  import { MediaProviderRegistry, ensureBuiltinMediaProviders } from '@pellux/goodvibes-sdk/platform/media';
23
25
  import { MultimodalService } from '@pellux/goodvibes-sdk/platform/multimodal';
24
26
  import { AgentMessageBus, AgentOrchestrator, ArchetypeLoader, WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
25
- import { AgentManager, OverflowHandler, ProcessManager, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
27
+ import { AgentManager, ContextAccountingHolder, OverflowHandler, ProcessManager, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
26
28
  import { FileStateCache, FileUndoManager, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, resolveCanonicalMemoryDbPath, type CodeIndexStore, type CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
27
29
  import { MemorySpineClient, createLocalMemoryAccess } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
28
30
  import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
29
- import type { RuntimeEventBus } from '@/runtime/index.ts';
31
+ import { createWorkspaceCheckpointing } from './workspace-checkpointing.ts';
32
+ import { createSessionConversationRewindPort } from './conversation-rewind-port.ts';
30
33
  import { createDomainDispatch } from './store/index.ts';
31
34
  import type { DomainDispatch, RuntimeStore } from './store/index.ts';
32
- import { DistributedRuntimeManager } from '@/runtime/index.ts';
33
- import { RemoteRunnerRegistry, RemoteSupervisor } from '@/runtime/index.ts';
34
- import { IntegrationHelperService } from '@/runtime/index.ts';
35
+ import {
36
+ type RuntimeEventBus, DistributedRuntimeManager, RemoteRunnerRegistry, RemoteSupervisor, IntegrationHelperService,
37
+ IdempotencyStore, ComponentHealthMonitor, WorktreeRegistry, SandboxSessionRegistry, createShellPathService,
38
+ type ShellPathService, type FeatureFlagManager, createFeatureFlagManager, PolicyRuntimeState,
39
+ } from '@/runtime/index.ts';
35
40
  import { VoiceProviderRegistry, VoiceService, ensureBuiltinVoiceProviders } from '@pellux/goodvibes-sdk/platform/voice';
36
41
  import { WebSearchProviderRegistry, WebSearchService } from '@pellux/goodvibes-sdk/platform/web-search';
37
42
  import { PanelManager } from '../panels/panel-manager.ts';
@@ -47,18 +52,11 @@ import { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
47
52
  import { BenchmarkStore, CacheHitTracker, FavoritesStore, inferFallbackContextWindow, type ModelDefinition, ModelLimitsService, ProviderCapabilityRegistry, ProviderOptimizer, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
48
53
  import { KeybindingsManager } from '../input/keybindings.ts';
49
54
  import { AdaptivePlanner, DeterministicReplayEngine, ExecutionPlanManager, SessionLineageTracker, SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
50
- import { IdempotencyStore } from '@/runtime/index.ts';
51
- import { ComponentHealthMonitor } from '@/runtime/index.ts';
52
- import { WorktreeRegistry } from '@/runtime/index.ts';
53
- import { SandboxSessionRegistry } from '@/runtime/index.ts';
54
- import { createShellPathService, type ShellPathService } from '@/runtime/index.ts';
55
55
  import { isFeatureFlagEnabled } from './surface-feature-flags.ts';
56
- import type { FeatureFlagManager } from '@/runtime/index.ts';
57
- import { createFeatureFlagManager } from '@/runtime/index.ts';
58
- import { PolicyRuntimeState } from '@/runtime/index.ts';
59
- import { createProcessRegistry, withFleetArchive, type ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
56
+ import { type ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
60
57
  import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
61
58
  import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamCommandService } from './workstream-services.ts';
59
+ import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
62
60
  import { codeIndexDbPath, createCodeIndexServices, isCodeInjectionSettingEnabled } from './code-index-services.ts';
63
61
  import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
64
62
  import {
@@ -74,6 +72,7 @@ import { registerDraftMethods } from '../daemon/handlers/drafts/index.ts';
74
72
  import { registerCalendar } from '../daemon/handlers/calendar/index.ts';
75
73
  import { registerEmailMethods } from '../daemon/handlers/email/index.ts';
76
74
  import { registerRemoteSurface } from '../daemon/handlers/remote/index.ts';
75
+ import { WorkspaceTrustManager } from './trust/workspace-trust.ts';
77
76
 
78
77
  const REGULAR_KNOWLEDGE_DB_FILE = 'knowledge-wiki.sqlite';
79
78
  const HOME_GRAPH_KNOWLEDGE_DB_FILE = 'knowledge-home-graph.sqlite';
@@ -135,6 +134,8 @@ export interface RuntimeServicesOptions {
135
134
  readonly getConversationTitle?: () => string | undefined;
136
135
  readonly workingDir: string;
137
136
  readonly homeDirectory: string;
137
+ /** Prior GoodVibes runtime state existed at startup — sampled before any write; seeds the trust gate's grandfathering. */
138
+ readonly workspaceHadPriorState?: boolean;
138
139
  }
139
140
 
140
141
  export interface RuntimeServices {
@@ -171,6 +172,7 @@ export interface RuntimeServices {
171
172
  readonly memorySpine: MemorySpineClient;
172
173
  readonly serviceRegistry: ServiceRegistry;
173
174
  readonly secretsManager: SecretsManager;
175
+ readonly stepUpService: StepUpService;
174
176
  readonly subscriptionManager: SubscriptionManager;
175
177
  readonly localUserAuthManager: UserAuthManager;
176
178
  readonly profileManager: ProfileManager;
@@ -223,6 +225,7 @@ export interface RuntimeServices {
223
225
  readonly agentManager: AgentManager;
224
226
  readonly agentMessageBus: AgentMessageBus;
225
227
  readonly agentOrchestrator: AgentOrchestrator;
228
+ readonly contextAccountingHolder: ContextAccountingHolder; // bound at bootstrap.ts; see context-accounting-source.ts
226
229
  readonly wrfcController: WrfcController;
227
230
  readonly processManager: ProcessManager;
228
231
  /** The phase/work-item orchestration engine — see runtime/workstream-services.ts. */
@@ -236,6 +239,8 @@ export interface RuntimeServices {
236
239
  readonly modeManager: ModeManager;
237
240
  readonly fileUndoManager: FileUndoManager;
238
241
  readonly workspaceCheckpointManager: WorkspaceCheckpointManager;
242
+ /** Per-workspace trust gate — restricts write/execute/delegate tools until the workspace is trusted. */
243
+ readonly workspaceTrustManager: WorkspaceTrustManager;
239
244
  readonly integrationHelpers: IntegrationHelperService;
240
245
  /** Re-root path-bound stores (MemoryStore, ProjectIndex) to a new working directory, called by WorkspaceSwapManager after verification; stores needing a process restart just warn-log and keep serving the old path until the daemon restarts with the new --working-dir. */
241
246
  rerootStores(newWorkingDir: string): Promise<void>;
@@ -248,6 +253,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
248
253
  workingDirectory,
249
254
  homeDirectory,
250
255
  });
256
+ const workspaceTrustManager = new WorkspaceTrustManager({ shellPaths, hadPriorState: options.workspaceHadPriorState ?? false });
251
257
  const configManager = options.configManager;
252
258
  const featureFlags = options.featureFlags ?? createFeatureFlagManager();
253
259
  const runtimeDispatch = createDomainDispatch(options.runtimeStore);
@@ -272,9 +278,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
272
278
  globalHome: homeDirectory,
273
279
  configManager,
274
280
  });
275
- const subscriptionManager = new SubscriptionManager(
276
- shellPaths.resolveUserPath('tui', 'subscriptions.json'),
277
- );
281
+ // Step-up (WebAuthn) ceremony service, shared between the ceremony gateway verbs and the relay gate's verifier.
282
+ const stepUpService = new StepUpService({ secrets: secretsManager });
283
+ const subscriptionManager = new SubscriptionManager(shellPaths.resolveUserPath('tui', 'subscriptions.json'));
278
284
  const serviceRegistry = new ServiceRegistry(shellPaths.resolveProjectPath('tui', 'services.json'), {
279
285
  secretsManager,
280
286
  subscriptionManager,
@@ -336,6 +342,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
336
342
  executor: agentOrchestrator,
337
343
  configManager,
338
344
  });
345
+ const contextAccountingHolder = new ContextAccountingHolder();
339
346
  agentOrchestrator.setConversationSink({ // Conversation-snapshot bridge (mirrors the SDK's own createRuntimeServices)
340
347
  register: (agentId, source) => agentManager.registerConversationSource(agentId, source),
341
348
  release: (agentId) => agentManager.releaseConversationSource(agentId),
@@ -605,11 +612,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
605
612
  // code-index-services.ts's header doc.
606
613
  const { codeIndexStore, codeIndexReindexScheduler } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry });
607
614
  const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler }; // Code-injection seam (agent here; main via orchestrator-core-services.ts)
608
- // W2.1/W2.2: one shared process registry aggregating the managers above —
609
- // the Fleet panel (panels/fleet-read-model.ts) is its first consumer.
610
- // Constructed once here (not per-consumer) so the coalesced tick and the
611
- // agent-activity side-table are shared, not duplicated.
612
- const processRegistry = withFleetArchive(createProcessRegistry({
615
+ // Shared, archive-aware fleet registry — see gateway-verbs.ts's factory doc.
616
+ const processRegistry = createArchivableFleetRegistry({
613
617
  agentManager,
614
618
  wrfcController,
615
619
  orchestrationEngine, // Folds workstream/phase/work-item nodes into the fleet
@@ -628,23 +632,16 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
628
632
  if (!isModelPriced(modelId)) return null;
629
633
  return calcSessionCost(usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.cacheWriteTokens, modelId);
630
634
  },
631
- }));
635
+ });
632
636
  const modeManager = new ModeManager();
633
637
  const fileUndoManager = new FileUndoManager();
634
- const workspaceCheckpointManager = new WorkspaceCheckpointManager({
635
- workspaceRoot: workingDirectory,
636
- runtimeBus: options.runtimeBus,
637
- });
638
- // Fire-and-forget: subscriptions go live immediately if init() succeeds.
639
- // If it rejects, WorkspaceCheckpointManager caches that rejection on
640
- // `initPromise` and never clears it, so every later call (create/list/
641
- // diff/restore — each awaits init() first) re-throws the same error
642
- // forever: checkpointing becomes entirely unavailable for the rest of the
643
- // session, not "degraded to manual-only". The `.catch(() => {})` here only
644
- // exists to prevent an unhandled rejection at startup; the checkpoint
645
- // commands (checkpoint-runtime.ts) are what actually catch and report the
646
- // failure to the user, on first use.
647
- void workspaceCheckpointManager.init().catch(() => {});
638
+ const workspaceCheckpointManager = createWorkspaceCheckpointing({ workspaceRoot: workingDirectory, runtimeBus: options.runtimeBus, configManager });
639
+
640
+ // Shared terminal-shell wrapper over the SDK's registerGatewayVerbGroups (see gateway-verbs.ts): 501s the ws-only
641
+ // family without it. principals.*/channels.profiles.*/ci.* register unconditionally; checkin.* only with
642
+ // channelDeliveryRouter/providerRegistry/automationManager/sessionLister ALL present; fleet needs-input push gated by
643
+ // runtimeBus/sessionPresence (fleet-needs-input-push.ts). conversationRewindPort serves conversation-scope rewind live.
644
+ attachWsOnlyGatewayVerbHandlers(gatewayMethods, { processRegistry, workspaceCheckpointManager, conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService, approvalBroker, shellPaths, configManager, runtimeStore: options.runtimeStore, channelDeliveryRouter, providerRegistry, automationManager, sessionLister: sessionBroker, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
648
645
  const integrationHelpers = new IntegrationHelperService({
649
646
  workingDirectory,
650
647
  homeDirectory,
@@ -691,12 +688,14 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
691
688
  overflowHandler,
692
689
  sandboxSessionRegistry,
693
690
  workflowServices: workflow,
691
+ contextAccountingHolder,
694
692
  });
695
693
 
696
694
  return {
697
695
  workingDirectory,
698
696
  homeDirectory,
699
697
  shellPaths,
698
+ workspaceTrustManager,
700
699
  configManager,
701
700
  featureFlags,
702
701
  runtimeBus: options.runtimeBus,
@@ -726,6 +725,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
726
725
  memorySpine,
727
726
  serviceRegistry,
728
727
  secretsManager,
728
+ stepUpService,
729
729
  subscriptionManager,
730
730
  localUserAuthManager,
731
731
  profileManager,
@@ -777,6 +777,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
777
777
  agentManager,
778
778
  agentMessageBus,
779
779
  agentOrchestrator,
780
+ contextAccountingHolder,
780
781
  wrfcController,
781
782
  processManager,
782
783
  orchestrationEngine,