@pellux/goodvibes-tui 1.7.0 → 1.9.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 (137) hide show
  1. package/README.md +10 -9
  2. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  3. package/package.json +2 -2
  4. package/src/audio/spoken-turn-controller.ts +12 -2
  5. package/src/audio/spoken-turn-wiring.ts +2 -1
  6. package/src/cli/help.ts +8 -1
  7. package/src/cli/service-posture.ts +2 -1
  8. package/src/cli/status.ts +2 -1
  9. package/src/cli/surface-command.ts +2 -2
  10. package/src/core/composer-state.ts +11 -3
  11. package/src/core/conversation-line-cache.ts +27 -3
  12. package/src/core/conversation-rendering.ts +71 -14
  13. package/src/core/stream-event-wiring.ts +20 -1
  14. package/src/core/system-message-noise.ts +87 -0
  15. package/src/core/system-message-router.ts +68 -1
  16. package/src/core/turn-cancellation.ts +7 -2
  17. package/src/core/turn-event-wiring.ts +10 -2
  18. package/src/daemon/cli.ts +29 -2
  19. package/src/daemon/handlers/register.ts +8 -1
  20. package/src/daemon/service-commands.ts +329 -0
  21. package/src/input/autocomplete.ts +27 -1
  22. package/src/input/command-registry.ts +46 -4
  23. package/src/input/commands/codebase-runtime.ts +46 -6
  24. package/src/input/commands/config.ts +43 -3
  25. package/src/input/commands/health-runtime.ts +9 -1
  26. package/src/input/commands/memory.ts +68 -35
  27. package/src/input/commands/operator-panel-runtime.ts +31 -7
  28. package/src/input/commands/planning-runtime.ts +95 -6
  29. package/src/input/commands/qrcode-runtime.ts +25 -5
  30. package/src/input/commands/remote-runtime-setup.ts +5 -3
  31. package/src/input/commands/session-content.ts +20 -9
  32. package/src/input/commands/settings-sync-runtime.ts +15 -3
  33. package/src/input/commands/shell-core.ts +10 -1
  34. package/src/input/commands/workstream-runtime.ts +168 -18
  35. package/src/input/config-modal-types.ts +15 -1
  36. package/src/input/config-modal.ts +227 -12
  37. package/src/input/feed-context-factory.ts +3 -0
  38. package/src/input/handler-command-route.ts +10 -3
  39. package/src/input/handler-content-actions.ts +17 -2
  40. package/src/input/handler-feed-routes.ts +43 -121
  41. package/src/input/handler-feed.ts +32 -4
  42. package/src/input/handler-modal-routes.ts +78 -13
  43. package/src/input/handler-modal-stack.ts +11 -2
  44. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  45. package/src/input/handler-onboarding.ts +71 -59
  46. package/src/input/handler-picker-routes.ts +15 -8
  47. package/src/input/handler-shortcuts.ts +59 -9
  48. package/src/input/handler.ts +6 -1
  49. package/src/input/keybindings.ts +6 -5
  50. package/src/input/model-picker.ts +19 -2
  51. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  52. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  53. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  54. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  55. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  56. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  57. package/src/input/panel-mouse-geometry.ts +97 -0
  58. package/src/input/panel-paste-flood-guard.ts +86 -0
  59. package/src/input/selection-modal.ts +11 -0
  60. package/src/input/session-picker-modal.ts +44 -4
  61. package/src/input/settings-modal-data.ts +51 -2
  62. package/src/input/settings-modal-types.ts +4 -2
  63. package/src/main.ts +28 -27
  64. package/src/panels/builtin/shared.ts +9 -3
  65. package/src/panels/fleet-deep-link.ts +31 -0
  66. package/src/panels/fleet-panel-format.ts +62 -0
  67. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  68. package/src/panels/fleet-panel.ts +89 -131
  69. package/src/panels/fleet-read-model.ts +43 -0
  70. package/src/panels/fleet-steer.ts +35 -2
  71. package/src/panels/fleet-stop.ts +29 -1
  72. package/src/panels/modals/keybindings-modal.ts +16 -1
  73. package/src/panels/modals/modal-theme.ts +35 -29
  74. package/src/panels/modals/pairing-modal.ts +25 -6
  75. package/src/panels/modals/planning-modal.ts +43 -21
  76. package/src/panels/modals/work-plan-modal.ts +28 -0
  77. package/src/panels/panel-manager.ts +15 -4
  78. package/src/panels/polish-core.ts +38 -25
  79. package/src/panels/project-planning-answer-actions.ts +8 -13
  80. package/src/panels/types.ts +24 -0
  81. package/src/permissions/prompt.ts +160 -13
  82. package/src/renderer/autocomplete-overlay.ts +28 -2
  83. package/src/renderer/compositor.ts +2 -3
  84. package/src/renderer/config-modal.ts +19 -5
  85. package/src/renderer/footer-tips.ts +5 -1
  86. package/src/renderer/fullscreen-primitives.ts +32 -22
  87. package/src/renderer/git-status.ts +3 -1
  88. package/src/renderer/layout.ts +0 -4
  89. package/src/renderer/markdown.ts +7 -3
  90. package/src/renderer/modal-factory.ts +25 -20
  91. package/src/renderer/model-workspace.ts +18 -3
  92. package/src/renderer/overlay-box.ts +21 -17
  93. package/src/renderer/process-indicator.ts +14 -3
  94. package/src/renderer/selection-modal-overlay.ts +6 -1
  95. package/src/renderer/session-picker-modal.ts +196 -3
  96. package/src/renderer/settings-modal-helpers.ts +2 -0
  97. package/src/renderer/settings-modal.ts +7 -0
  98. package/src/renderer/shell-surface.ts +8 -1
  99. package/src/renderer/status-glyphs.ts +14 -15
  100. package/src/renderer/system-message.ts +15 -3
  101. package/src/renderer/terminal-bg-probe.ts +339 -0
  102. package/src/renderer/terminal-escapes.ts +20 -0
  103. package/src/renderer/theme-mode-config.ts +67 -0
  104. package/src/renderer/theme.ts +91 -1
  105. package/src/renderer/thinking.ts +11 -3
  106. package/src/renderer/tool-call.ts +15 -9
  107. package/src/renderer/tool-result-summary.ts +148 -0
  108. package/src/renderer/turn-injection.ts +22 -3
  109. package/src/renderer/ui-factory.ts +154 -85
  110. package/src/renderer/ui-primitives.ts +30 -129
  111. package/src/runtime/bootstrap-command-context.ts +6 -0
  112. package/src/runtime/bootstrap-command-parts.ts +8 -4
  113. package/src/runtime/bootstrap-core.ts +33 -11
  114. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  115. package/src/runtime/bootstrap-shell.ts +19 -1
  116. package/src/runtime/bootstrap.ts +118 -5
  117. package/src/runtime/code-index-services.ts +25 -2
  118. package/src/runtime/legacy-daemon-migration.ts +516 -0
  119. package/src/runtime/memory-fold.ts +26 -0
  120. package/src/runtime/onboarding/derivation.ts +7 -2
  121. package/src/runtime/onboarding/snapshot.ts +27 -1
  122. package/src/runtime/onboarding/types.ts +31 -1
  123. package/src/runtime/operator-token-cleanup.ts +82 -1
  124. package/src/runtime/orchestrator-core-services.ts +10 -0
  125. package/src/runtime/resume-notice.ts +209 -0
  126. package/src/runtime/services.ts +12 -8
  127. package/src/runtime/session-inbound-inputs.ts +252 -0
  128. package/src/runtime/session-spine-transport.ts +64 -0
  129. package/src/runtime/terminal-output-guard.ts +15 -8
  130. package/src/runtime/ui-services.ts +19 -3
  131. package/src/runtime/workstream-services.ts +160 -28
  132. package/src/runtime/wrfc-persistence.ts +124 -17
  133. package/src/shell/blocking-input.ts +46 -3
  134. package/src/shell/recovery-input-helpers.ts +170 -1
  135. package/src/shell/ui-openers.ts +42 -9
  136. package/src/utils/terminal-width.ts +52 -0
  137. package/src/version.ts +1 -1
@@ -5,6 +5,7 @@ import { readOnboardingRuntimeState } from './state.ts';
5
5
  import type {
6
6
  OnboardingAcknowledgementSnapshot,
7
7
  OnboardingConfigSnapshot,
8
+ OnboardingLegacyDaemonSnapshot,
8
9
  OnboardingProviderRoutingSnapshot,
9
10
  OnboardingRuntimeDefaultsSnapshot,
10
11
  OnboardingServiceState,
@@ -198,6 +199,10 @@ function buildFallbackAuthSnapshot(): LocalAuthSnapshot {
198
199
  };
199
200
  }
200
201
 
202
+ function buildFallbackLegacyDaemonSnapshot(): OnboardingLegacyDaemonSnapshot {
203
+ return { present: false, active: false, path: '' };
204
+ }
205
+
201
206
  function buildConfiguredSurfaceKinds(
202
207
  surfaces: OnboardingConfigSnapshot['surfaces'],
203
208
  ): string[] {
@@ -320,6 +325,7 @@ export async function collectOnboardingSnapshot(
320
325
  secretRecordsResult,
321
326
  surfaceResult,
322
327
  providerAccountsResult,
328
+ legacyDaemonResult,
323
329
  ] = await Promise.all([
324
330
  loadOptionalSnapshot(
325
331
  'subscriptions-active',
@@ -357,6 +363,11 @@ export async function collectOnboardingSnapshot(
357
363
  deps.providerAccounts ? () => deps.providerAccounts!.loadSnapshot() : undefined,
358
364
  null,
359
365
  ),
366
+ loadOptionalSnapshot(
367
+ 'legacy-daemon',
368
+ deps.legacyDaemon ? () => Promise.resolve(deps.legacyDaemon!.detect()) : undefined,
369
+ buildFallbackLegacyDaemonSnapshot(),
370
+ ),
360
371
  ]);
361
372
 
362
373
  const collectionIssues: OnboardingSnapshotCollectionIssue[] = [];
@@ -369,6 +380,7 @@ export async function collectOnboardingSnapshot(
369
380
  if (secretRecordsResult.issue) collectionIssues.push(secretRecordsResult.issue);
370
381
  if (surfaceResult.issue) collectionIssues.push(surfaceResult.issue);
371
382
  if (providerAccountsResult.issue) collectionIssues.push(providerAccountsResult.issue);
383
+ if (legacyDaemonResult.issue) collectionIssues.push(legacyDaemonResult.issue);
372
384
 
373
385
  return {
374
386
  capturedAt,
@@ -391,7 +403,20 @@ export async function collectOnboardingSnapshot(
391
403
  snapshot: authSnapshotResult.value,
392
404
  },
393
405
  bindSettings: {
394
- daemonEnabled: Boolean(config.danger.daemon),
406
+ // danger.daemon (removed Wave 6 — see docs/decisions/2026-07-05-daemon-by-default.md)
407
+ // used to gate the pre-Wave-2 opt-in daemon posture; this site read it RAW
408
+ // (not through resolveDaemonEnabled) so the wizard's network-mode
409
+ // classification tracked "did the user explicitly request the legacy
410
+ // dangerous posture" rather than "does the daemon run" (which has defaulted
411
+ // true unconditionally since Wave 2, and would misclassify every default,
412
+ // local-only install as server-mode if read here). Every realistic config
413
+ // already evaluated this to `false` (unset, or an explicit `danger.daemon:
414
+ // false` both did); the alias's removal migration preserves the one case
415
+ // that changes real daemon behavior (explicit `false` -> `daemon.enabled:
416
+ // false`) but does not resurrect a signal for "explicitly true" — that path
417
+ // was always a no-op for actual daemon operation. With the alias gone there
418
+ // is no signal left to read, so this is pinned at its steady-state value.
419
+ daemonEnabled: false,
395
420
  httpListenerEnabled: Boolean(config.danger.httpListener),
396
421
  controlPlane: config.controlPlane,
397
422
  httpListener: config.httpListener,
@@ -402,6 +427,7 @@ export async function collectOnboardingSnapshot(
402
427
  records: sortSurfaceRecords(surfaceResult.value),
403
428
  },
404
429
  providerAccounts: providerAccountsResult.value,
430
+ legacyDaemon: legacyDaemonResult.value,
405
431
  collectionIssues,
406
432
  };
407
433
  }
@@ -125,6 +125,29 @@ export interface OnboardingSurfacesSnapshot {
125
125
  readonly records: readonly OnboardingSurfaceRecord[];
126
126
  }
127
127
 
128
+ /**
129
+ * W4-D1 wizard wiring: read-only detection of a legacy `goodvibes-daemon.service`
130
+ * systemd unit (see `../legacy-daemon-migration.ts`'s `LegacyUnitInfo`), carried
131
+ * on the snapshot so the Network step can show the guided migration action only
132
+ * when there is actually something to migrate. Never implies anything was
133
+ * touched — detection only.
134
+ */
135
+ export interface OnboardingLegacyDaemonSnapshot {
136
+ readonly present: boolean;
137
+ readonly active: boolean;
138
+ readonly path: string;
139
+ /**
140
+ * F2 follow-up: the unit name this tool actually manages on this host,
141
+ * resolved from the `service.serviceName` config key at snapshot-collection
142
+ * time (`resolveConfiguredServiceName`, `../legacy-daemon-migration.ts`) —
143
+ * so the wizard's detection banner names the real unit instead of the
144
+ * hardcoded default. Optional so snapshots built without config access
145
+ * (fallbacks, older fixtures) stay valid; readers fall back to
146
+ * `MANAGED_SERVICE_NAME`.
147
+ */
148
+ readonly trackedServiceName?: string;
149
+ }
150
+
128
151
  export interface OnboardingProviderAccountRecord {
129
152
  readonly providerId: string;
130
153
  readonly configured: boolean;
@@ -170,7 +193,8 @@ export type OnboardingSnapshotCollectionIssueArea =
170
193
  | 'secrets-records'
171
194
  | 'surfaces'
172
195
  | 'provider-accounts'
173
- | 'acknowledgements';
196
+ | 'acknowledgements'
197
+ | 'legacy-daemon';
174
198
 
175
199
  export interface OnboardingSnapshotCollectionIssue {
176
200
  readonly area: OnboardingSnapshotCollectionIssueArea;
@@ -190,6 +214,7 @@ export interface OnboardingSnapshotState {
190
214
  readonly bindSettings: OnboardingBindSettingsSnapshot;
191
215
  readonly surfaces: OnboardingSurfacesSnapshot;
192
216
  readonly providerAccounts: OnboardingProviderAccountsSnapshot | null;
217
+ readonly legacyDaemon: OnboardingLegacyDaemonSnapshot;
193
218
  readonly collectionIssues: readonly OnboardingSnapshotCollectionIssue[];
194
219
  }
195
220
 
@@ -377,6 +402,10 @@ export interface OnboardingProviderAccountReadHelper {
377
402
  loadSnapshot(): Promise<OnboardingProviderAccountsSnapshot>;
378
403
  }
379
404
 
405
+ export interface OnboardingLegacyDaemonReadHelper {
406
+ detect(): OnboardingLegacyDaemonSnapshot | Promise<OnboardingLegacyDaemonSnapshot>;
407
+ }
408
+
380
409
  export type OnboardingShellPaths = Pick<
381
410
  ShellPathService,
382
411
  'workingDirectory' | 'resolveProjectPath' | 'resolveUserPath'
@@ -391,6 +420,7 @@ export interface OnboardingSnapshotDependencies {
391
420
  readonly services: Pick<ServiceInspectionQuery, 'getAll' | 'inspect'>;
392
421
  readonly surfaces?: OnboardingSurfaceReadHelper;
393
422
  readonly providerAccounts?: OnboardingProviderAccountReadHelper;
423
+ readonly legacyDaemon?: OnboardingLegacyDaemonReadHelper;
394
424
  readonly shellPaths?: OnboardingShellPaths;
395
425
  readonly acknowledgementScope?: OnboardingStateScope;
396
426
  }
@@ -11,7 +11,11 @@
11
11
  * the new path will be inspected on the next daemon boot.
12
12
  */
13
13
 
14
- import { join } from 'node:path';
14
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { randomBytes } from 'node:crypto';
16
+ import { dirname, join } from 'node:path';
17
+ import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
18
+ import type { CompanionPairingResult } from '@pellux/goodvibes-sdk/platform/pairing';
15
19
 
16
20
  /**
17
21
  * Return the list of absolute operator-tokens.json paths the TUI may have written
@@ -26,3 +30,80 @@ export function workspaceOperatorTokenCandidates(workingDirectory: string): read
26
30
  join(workingDirectory, '.goodvibes', 'tui', 'operator-tokens.json'),
27
31
  ];
28
32
  }
33
+
34
+ /**
35
+ * F1 (adopt-an-already-running-external-daemon): resolve the operator/companion
36
+ * token this TUI process uses to authenticate with its daemon, honoring
37
+ * `GOODVIBES_DAEMON_TOKEN` as a non-interactive override.
38
+ *
39
+ * Without this override, the only way to make a TUI instance adopt an
40
+ * already-running external daemon that used an out-of-band or explicit token
41
+ * (rather than one derived from `getOrCreateCompanionToken` under a *shared*
42
+ * home directory) was to hand-write `<daemonHomeDir>/operator-tokens.json`
43
+ * before startup — there was no env var or CLI flag equivalent.
44
+ *
45
+ * `GOODVIBES_DAEMON_TOKEN` is already the documented env var `bin/goodvibes-daemon`
46
+ * honors for its own bearer token (see `src/daemon/cli.ts`'s `readDaemonCliTokens`)
47
+ * and the one `src/verification/live-verifier.ts`'s `readDaemonToken` already falls
48
+ * back to when probing a daemon's HTTP surface from the outside. Reusing the same
49
+ * name here means one env var configures both sides of an adopted-daemon setup —
50
+ * start the daemon with `GOODVIBES_DAEMON_TOKEN=<token>` and point the TUI at it
51
+ * with the same `GOODVIBES_DAEMON_TOKEN=<token>` plus
52
+ * `--config controlPlane.host=<host> --config controlPlane.port=<port>` — instead
53
+ * of introducing a second, TUI-only flag.
54
+ *
55
+ * When the override is set and does not already match the on-disk record, the
56
+ * file is rewritten so the override becomes durable for this home directory
57
+ * (an existing peerId is kept when present, so companion-pairing identity does
58
+ * not needlessly churn). Falls back to the existing `getOrCreateCompanionToken`
59
+ * behavior — read the existing file, or mint a fresh random token — when the
60
+ * override is unset.
61
+ *
62
+ * @param explicitToken - Takes precedence over `GOODVIBES_DAEMON_TOKEN` when
63
+ * provided. Used by the onboarding wizard's "connect to an existing daemon"
64
+ * action (a pasted token) so it shares this exact persistence logic with the
65
+ * env-var path instead of duplicating it.
66
+ */
67
+ export function resolveDaemonCompanionToken(daemonHomeDir: string, explicitToken?: string): CompanionPairingResult {
68
+ const override = explicitToken?.trim() || process.env.GOODVIBES_DAEMON_TOKEN?.trim();
69
+ if (!override) return getOrCreateCompanionToken('tui', { daemonHomeDir });
70
+
71
+ const tokenPath = join(daemonHomeDir, 'operator-tokens.json');
72
+ let existingPeerId: string | undefined;
73
+ let existingCreatedAt: number | undefined;
74
+ if (existsSync(tokenPath)) {
75
+ try {
76
+ const record = JSON.parse(readFileSync(tokenPath, 'utf-8')) as {
77
+ token?: unknown;
78
+ peerId?: unknown;
79
+ createdAt?: unknown;
80
+ };
81
+ if (typeof record.token === 'string' && record.token === override) {
82
+ return {
83
+ token: override,
84
+ peerId: typeof record.peerId === 'string' ? record.peerId : randomBytes(12).toString('hex'),
85
+ createdAt: typeof record.createdAt === 'number' ? record.createdAt : Date.now(),
86
+ };
87
+ }
88
+ if (typeof record.peerId === 'string') existingPeerId = record.peerId;
89
+ if (typeof record.createdAt === 'number') existingCreatedAt = record.createdAt;
90
+ } catch {
91
+ // Malformed on-disk record — fall through and rewrite it with the override.
92
+ }
93
+ }
94
+
95
+ const record: CompanionPairingResult = {
96
+ token: override,
97
+ peerId: existingPeerId ?? randomBytes(12).toString('hex'),
98
+ createdAt: existingCreatedAt ?? Date.now(),
99
+ };
100
+ try {
101
+ mkdirSync(dirname(tokenPath), { recursive: true });
102
+ writeFileSync(tokenPath, JSON.stringify(record, null, 2), { encoding: 'utf-8', mode: 0o600 });
103
+ chmodSync(tokenPath, 0o600);
104
+ } catch {
105
+ // Best-effort persistence — the override still applies for this process
106
+ // even if the file could not be written (e.g. a read-only home directory).
107
+ }
108
+ return record;
109
+ }
@@ -2,6 +2,7 @@ import type { OrchestratorCoreServices } from '@pellux/goodvibes-sdk/platform/co
2
2
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
3
3
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
4
4
  import type { RuntimeServices } from './services.ts';
5
+ import { isCodeInjectionSettingEnabled } from './code-index-services.ts';
5
6
 
6
7
  /** The slice of the runtime services bag the shared orchestrator payload draws from. */
7
8
  export type OrchestratorCoreServicesSource = Pick<
@@ -12,6 +13,8 @@ export type OrchestratorCoreServicesSource = Pick<
12
13
  | 'sessionLineageTracker'
13
14
  | 'idempotencyStore'
14
15
  | 'memoryRegistry'
16
+ | 'codeIndexStore'
17
+ | 'codeIndexReindexScheduler'
15
18
  >;
16
19
 
17
20
  /**
@@ -45,5 +48,12 @@ export function buildSharedOrchestratorCoreServices(input: {
45
48
  sessionLineageTracker: services.sessionLineageTracker,
46
49
  idempotencyStore: services.idempotencyStore,
47
50
  memoryRegistry: services.memoryRegistry,
51
+ // Wave-5 Stage B — main-session code auto-injection + tool-site reindex. Injection is
52
+ // additionally gated by the default-off `agent-passive-code-injection` flag inside the
53
+ // SDK; here we supply the source, the live storage.codeIndexEnabled predicate, and the
54
+ // reindex scheduler.
55
+ codeIndex: services.codeIndexStore,
56
+ isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager),
57
+ codeIndexReindexScheduler: services.codeIndexReindexScheduler,
48
58
  };
49
59
  }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * resume-notice.ts — the boot-time "previous session found" transcript notice.
3
+ *
4
+ * UX-D item 1: a supervision-journey audit of 1.7.0 found that the TUI
5
+ * accumulates rich resumable state on disk (a saved conversation, workspace
6
+ * checkpoints, WRFC chain history) but never surfaces any of it at startup —
7
+ * an operator has no way to know it exists short of already knowing the
8
+ * right command. This module builds ONE compact, honest system-message block
9
+ * printed after the splash and before the first prompt, summarizing exactly
10
+ * what real state exists and how to reach it.
11
+ *
12
+ * Honesty constraints (verified against the actual runtime, not assumed):
13
+ * - There is no bare `/resume` command and no `/sessions` command (a
14
+ * pre-existing typo already lives in the splash's own hint at
15
+ * utils/splash-lines.ts — out of scope here, splash stays byte-identical).
16
+ * The real, working command is `/session resume <id|name>` — it always
17
+ * requires an explicit target. So this notice advertises
18
+ * `/session resume <id>` with the real last-session id substituted in,
19
+ * never a bare `/resume`.
20
+ * - `/checkpoints` works with zero arguments and behaves correctly at any
21
+ * checkpoint count (including zero) — advertised whenever the checkpoint
22
+ * manager is available in this session.
23
+ * - `/recall` (memory) is only advertised when the memory API is actually
24
+ * wired up in this session (context.clients?.knowledgeApi?.memory) —
25
+ * some runtimes don't have it, and claiming it works there would not be
26
+ * honest.
27
+ * - Every clause is independently gated on real data: a claim about
28
+ * checkpoints/chain history is only made when that data is known; "no
29
+ * chain history" means no chain clause is printed, not a fabricated one.
30
+ */
31
+
32
+ import { readLastSessionPointer } from '@/runtime/index.ts';
33
+ import type { WrfcChain } from '@pellux/goodvibes-sdk/platform/agents';
34
+ import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
35
+ import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
36
+ import type { SystemMessageRouter } from '../core/system-message-router.ts';
37
+
38
+ // ─── Chain outcome ───────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * Honest, human-facing outcome of a WRFC chain, derived from real chain
42
+ * state rather than guessed. `chain.state` alone cannot distinguish a
43
+ * user-cancelled chain from an ordinary review/gate failure. The SDK now
44
+ * records that distinction first-class as `chain.failureKind` ('cancelled'
45
+ * vs 'transport'/'other'), set by cancelChain()/failChain(), so that field
46
+ * is the primary source of truth. Snapshots persisted before the field
47
+ * existed lack it; for those we fall back to the owner-decision log, where
48
+ * cancelChain() also records a `chain_cancelled` decision. A chain still
49
+ * non-terminal after rehydrate's zombie-reap check (see wrfc-persistence.ts)
50
+ * is reported as 'interrupted' — re-imported and live again, not history.
51
+ */
52
+ export type ChainOutcome = 'passed' | 'failed' | 'cancelled' | 'interrupted';
53
+
54
+ /** Terminal WRFC states (mirrors wrfc-persistence.ts's own TERMINAL_STATES). */
55
+ function isTerminalState(state: WrfcChain['state']): boolean {
56
+ return state === 'passed' || state === 'failed';
57
+ }
58
+
59
+ export function describeChainOutcome(chain: WrfcChain): ChainOutcome {
60
+ if (!isTerminalState(chain.state)) return 'interrupted';
61
+ if (chain.state === 'passed') return 'passed';
62
+ // Primary: the SDK's first-class failureKind, authoritative for chains
63
+ // failed/cancelled under the current SDK.
64
+ if (chain.failureKind === 'cancelled') return 'cancelled';
65
+ // Fallback for pre-failureKind snapshots: consult the owner-decision log,
66
+ // where cancelChain() records a chain_cancelled decision.
67
+ if (chain.failureKind === undefined) {
68
+ const lastAction = chain.ownerDecisions.length > 0 ? chain.ownerDecisions[chain.ownerDecisions.length - 1]?.action : undefined;
69
+ if (lastAction === 'chain_cancelled') return 'cancelled';
70
+ }
71
+ return 'failed';
72
+ }
73
+
74
+ /** Pick the most recently completed (or, if still interrupted, most recently created) chain from a set. Null if the set is empty. */
75
+ export function mostRecentChain(chains: readonly WrfcChain[]): WrfcChain | null {
76
+ if (chains.length === 0) return null;
77
+ return [...chains].sort((a, b) => (b.completedAt ?? b.createdAt ?? 0) - (a.completedAt ?? a.createdAt ?? 0))[0]!;
78
+ }
79
+
80
+ // ─── Notice text ─────────────────────────────────────────────────────────────
81
+
82
+ export interface ResumeNoticeFacts {
83
+ /** Number of user turns in the last saved session. Null when there is no prior session (or it could not be read). */
84
+ readonly turnCount: number | null;
85
+ /** Session id of the last saved session — needed to build a truthful, directly-runnable resume hint. Null when there is no prior session. */
86
+ readonly lastSessionId: string | null;
87
+ /** Number of workspace checkpoints. Null when the checkpoint manager is unavailable in this session (not the same as zero). */
88
+ readonly checkpointCount: number | null;
89
+ /** Outcome of the most recently known WRFC chain. Null when there is no chain history at all. */
90
+ readonly lastChainOutcome: ChainOutcome | null;
91
+ /** Whether /recall (memory) is wired up in this session. */
92
+ readonly memoryAvailable: boolean;
93
+ }
94
+
95
+ function plural(n: number, word: string): string {
96
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
97
+ }
98
+
99
+ /**
100
+ * Build the boot resume notice text, or null when there is nothing to
101
+ * report (no prior session, no checkpoints ever taken, no chain history —
102
+ * a clean/new working directory prints nothing, respecting quiet startup).
103
+ */
104
+ export function buildResumeNotice(facts: ResumeNoticeFacts): string | null {
105
+ const hasSession = facts.lastSessionId !== null && facts.turnCount !== null;
106
+ const checkpointsKnown = facts.checkpointCount !== null;
107
+ const checkpointCount = facts.checkpointCount ?? 0;
108
+ const hasCheckpoints = checkpointsKnown && checkpointCount > 0;
109
+ const hasChainHistory = facts.lastChainOutcome !== null;
110
+
111
+ if (!hasSession && !hasCheckpoints && !hasChainHistory) return null;
112
+
113
+ const summary: string[] = [];
114
+ if (hasSession) summary.push(plural(facts.turnCount!, 'turn'));
115
+ // Show the checkpoint count whenever it's knowable and there's a reason to
116
+ // (anchored to an existing session, or checkpoints genuinely exist even
117
+ // without one) — never guessed when the manager is unavailable.
118
+ if (checkpointsKnown && (hasSession || hasCheckpoints)) summary.push(plural(checkpointCount, 'checkpoint'));
119
+ if (hasChainHistory) summary.push(`last chain: ${facts.lastChainOutcome}`);
120
+
121
+ const lead = hasSession ? 'Previous session found' : 'Workspace history found';
122
+ let notice = `${lead}: ${summary.join(', ')}`;
123
+
124
+ const hints: string[] = [];
125
+ // No bare `/resume` exists — /session resume always requires a target.
126
+ if (hasSession) hints.push(`/session resume ${facts.lastSessionId} to continue`);
127
+ if (hasCheckpoints) hints.push('/checkpoints to browse');
128
+ if (facts.memoryAvailable) hints.push('/recall for memory');
129
+ if (hints.length > 0) notice += ` — ${hints.join(' · ')}`;
130
+
131
+ return notice;
132
+ }
133
+
134
+ // ─── Fact gathering (I/O) ────────────────────────────────────────────────────
135
+
136
+ export interface ResumeNoticeDeps {
137
+ readonly workingDirectory: string;
138
+ readonly homeDirectory: string;
139
+ /** Surface root used by session persistence — the TUI always uses 'tui'. */
140
+ readonly surfaceRoot: string;
141
+ /** Only `load()` is needed — kept narrow for testability. */
142
+ readonly sessionManager: Pick<SessionManager, 'load'>;
143
+ /** Undefined when checkpoints are not wired up in this session at all. Only `list()` is needed. */
144
+ readonly checkpointManager: Pick<WorkspaceCheckpointManager, 'list'> | undefined;
145
+ /** The full known-chain set from WrfcPersistence.knownChains, gathered post-rehydrate. */
146
+ readonly chainHistory: readonly WrfcChain[];
147
+ readonly memoryAvailable: boolean;
148
+ readonly router: Pick<SystemMessageRouter, 'high'>;
149
+ }
150
+
151
+ /**
152
+ * Read the last session's real turn count from its saved JSONL file. A user
153
+ * turn is one stored message with role 'user' — the number of times the
154
+ * operator spoke, which is what "N turns" means to a human reading the
155
+ * notice (as opposed to a raw message count, which double-counts replies).
156
+ * Returns null when there is no last-session pointer, or the pointed-to
157
+ * session file is missing/corrupt — never a claim about a session that
158
+ * cannot actually be resumed.
159
+ */
160
+ function readLastSessionTurns(deps: Pick<ResumeNoticeDeps, 'workingDirectory' | 'homeDirectory' | 'surfaceRoot' | 'sessionManager'>): { turnCount: number; lastSessionId: string } | null {
161
+ const lastSessionId = readLastSessionPointer({
162
+ workingDirectory: deps.workingDirectory,
163
+ homeDirectory: deps.homeDirectory,
164
+ surfaceRoot: deps.surfaceRoot,
165
+ });
166
+ if (!lastSessionId) return null;
167
+ try {
168
+ const { messages } = deps.sessionManager.load(lastSessionId);
169
+ const turnCount = messages.filter((m) => (m as { role?: unknown }).role === 'user').length;
170
+ return { turnCount, lastSessionId };
171
+ } catch {
172
+ // Pointer file present but the session it points to is gone/corrupt —
173
+ // there is nothing truthful to resume.
174
+ return null;
175
+ }
176
+ }
177
+
178
+ async function readCheckpointCount(mgr: ResumeNoticeDeps['checkpointManager']): Promise<number | null> {
179
+ if (!mgr) return null;
180
+ try {
181
+ return (await mgr.list()).length;
182
+ } catch {
183
+ // Checkpoint manager present but its cached init() rejection makes every
184
+ // call fail forever (see services.ts) — treat as "unknown", not zero.
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Gather real facts from disk/services and, if there is anything to report,
191
+ * print ONE compact system message via `deps.router.high`. No-op (and no
192
+ * message) when there is no prior session, no checkpoints, and no chain
193
+ * history — a fresh working directory stays quiet.
194
+ */
195
+ export async function announceResumeState(deps: ResumeNoticeDeps): Promise<void> {
196
+ const session = readLastSessionTurns(deps);
197
+ const checkpointCount = await readCheckpointCount(deps.checkpointManager);
198
+ const lastChain = mostRecentChain(deps.chainHistory);
199
+
200
+ const notice = buildResumeNotice({
201
+ turnCount: session?.turnCount ?? null,
202
+ lastSessionId: session?.lastSessionId ?? null,
203
+ checkpointCount,
204
+ lastChainOutcome: lastChain ? describeChainOutcome(lastChain) : null,
205
+ memoryAvailable: deps.memoryAvailable,
206
+ });
207
+
208
+ if (notice) deps.router.high(notice);
209
+ }
@@ -23,7 +23,7 @@ import { MediaProviderRegistry, ensureBuiltinMediaProviders } from '@pellux/good
23
23
  import { MultimodalService } from '@pellux/goodvibes-sdk/platform/multimodal';
24
24
  import { AgentMessageBus, AgentOrchestrator, ArchetypeLoader, WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
25
25
  import { AgentManager, OverflowHandler, ProcessManager, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
26
- import { FileStateCache, FileUndoManager, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, type CodeIndexStore } from '@pellux/goodvibes-sdk/platform/state';
26
+ import { FileStateCache, FileUndoManager, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, resolveCanonicalMemoryDbPath, type CodeIndexStore, type CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
27
27
  import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
28
28
  import type { RuntimeEventBus } from '@/runtime/index.ts';
29
29
  import { createDomainDispatch } from './store/index.ts';
@@ -58,7 +58,7 @@ import { PolicyRuntimeState } from '@/runtime/index.ts';
58
58
  import { createProcessRegistry, type ProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
59
59
  import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
60
60
  import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamCommandService } from './workstream-services.ts';
61
- import { codeIndexDbPath, createCodeIndexServices } from './code-index-services.ts';
61
+ import { codeIndexDbPath, createCodeIndexServices, isCodeInjectionSettingEnabled } from './code-index-services.ts';
62
62
  import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
63
63
  import {
64
64
  registerDaemonHandlers,
@@ -227,6 +227,7 @@ export interface RuntimeServices {
227
227
  readonly workstreamCommands: WorkstreamCommandService;
228
228
  /** Wave 5 (wo804): the repo source-tree code index — see runtime/code-index-services.ts. */
229
229
  readonly codeIndexStore: CodeIndexStore;
230
+ readonly codeIndexReindexScheduler: CodeIndexReindexScheduler; // Wave-5 Stage B tool-site reindex
230
231
  /** W2.1/W2.2: unified live process registry (agents, WRFC chains, workflows, watchers, background processes) backing the Fleet panel. */
231
232
  readonly processRegistry: ProcessRegistry;
232
233
  readonly modeManager: ModeManager;
@@ -386,7 +387,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
386
387
  });
387
388
  const artifactStore = new ArtifactStore({ configManager });
388
389
  const memoryEmbeddingRegistry = new MemoryEmbeddingProviderRegistry({ configManager });
389
- const memoryDbPath = join(workingDirectory, '.goodvibes', 'tui', 'memory.sqlite');
390
+ // W6-C2 (E6): open the ONE home-scoped canonical store; legacy per-project TUI memory folds in at boot (foldTuiLegacyMemory).
391
+ const memoryDbPath = resolveCanonicalMemoryDbPath(homeDirectory);
390
392
  const memoryStore = new MemoryStore(memoryDbPath, {
391
393
  embeddingRegistry: memoryEmbeddingRegistry,
392
394
  });
@@ -603,7 +605,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
603
605
  // Wave 5 (wo804): repo source-tree code index, sharing memoryEmbeddingRegistry
604
606
  // with MemoryStore above. Auto-build is config-gated (default off) — see
605
607
  // code-index-services.ts's header doc.
606
- const { codeIndexStore } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry });
608
+ const { codeIndexStore, codeIndexReindexScheduler } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry });
609
+ const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler }; // Wave-5 Stage B seam (agent here; main via orchestrator-core-services.ts)
607
610
  // W2.1/W2.2: one shared process registry aggregating the managers above —
608
611
  // the Fleet panel (panels/fleet-read-model.ts) is its first consumer.
609
612
  // Constructed once here (not per-consumer) so the coalesced tick and the
@@ -678,6 +681,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
678
681
  remoteRunnerRegistry,
679
682
  knowledgeService,
680
683
  memoryRegistry,
684
+ ...codeInjectionOrchestratorDeps, // Wave-5 Stage B: agent-run code injection + tool-site reindex
681
685
  archetypeLoader,
682
686
  configManager,
683
687
  providerRegistry,
@@ -779,16 +783,16 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
779
783
  orchestrationEngine,
780
784
  workstreamCommands,
781
785
  codeIndexStore,
786
+ codeIndexReindexScheduler,
782
787
  processRegistry,
783
788
  modeManager,
784
789
  fileUndoManager,
785
790
  workspaceCheckpointManager,
786
791
  integrationHelpers,
787
792
  async rerootStores(newWorkingDir: string): Promise<void> {
788
- const newMemoryDbPath = join(newWorkingDir, '.goodvibes', 'tui', 'memory.sqlite');
789
- await memoryStore.reroot(newMemoryDbPath);
790
- // Wave 5 (wo804) risk #7: the code index must follow memory to the new
791
- // tree, or it keeps pointing at the old working directory.
793
+ // W6-C2 (E6): the memory store is the home-scoped canonical cross-surface store and
794
+ // deliberately does NOT reroot per-project (that would re-silo memory, the E6
795
+ // regression). Only working-tree-bound stores (code index, project index) reroot.
792
796
  await codeIndexStore.reroot(newWorkingDir, codeIndexDbPath(newWorkingDir));
793
797
  await projectIndex.reroot(newWorkingDir);
794
798
  },