@pellux/goodvibes-agent 1.5.5 → 1.5.6

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -0,0 +1,253 @@
1
+ /**
2
+ * LAN-scan consent gate (Wave-4 A7).
3
+ *
4
+ * The SDK's background provider-discovery pass (`startBackgroundProviderDiscovery`
5
+ * in `@pellux/goodvibes-sdk/platform/runtime`) probes every host on this
6
+ * computer's local subnet (254 addresses per non-internal network interface)
7
+ * looking for local model servers such as Ollama or LM Studio, then dumps one
8
+ * raw "[Scan] Found <name> at <host>:<port>" line per server straight into the
9
+ * activity feed. It ran unconditionally, with no consent step, and no way to
10
+ * turn it off.
11
+ *
12
+ * This module is the only place in the Agent that is allowed to call the SDK
13
+ * discovery entry point. It:
14
+ * 1. Gates the call behind a persisted, explicit consent decision. The very
15
+ * first time the Agent starts, nothing is scanned; the user is told what
16
+ * the feature does, what it touches (the local subnet — nothing beyond
17
+ * it), and what it stores, then the decision defaults to "off" until the
18
+ * user turns it on.
19
+ * 2. Reframes whatever the SDK emits when consent has been granted: instead
20
+ * of one raw host:port line per server, the Agent surfaces a single
21
+ * framed summary ("Discovered N local model servers ..."), with full
22
+ * detail available on demand via /provider.
23
+ *
24
+ * The SDK scanner itself is not modified — see
25
+ * node_modules/@pellux/goodvibes-sdk/dist/platform/discovery/scanner.js and
26
+ * node_modules/@pellux/goodvibes-sdk/dist/platform/runtime/bootstrap-background.js.
27
+ */
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
29
+ import { dirname } from 'node:path';
30
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
31
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
32
+ import {
33
+ startBackgroundProviderRegistration,
34
+ type BackgroundProviderDiscoveryOptions,
35
+ type BackgroundRuntimeTaskHandle,
36
+ type HostSystemMessageSink,
37
+ type ShellPathService,
38
+ } from '@/runtime/index.ts';
39
+
40
+ export type LanScanDecision = 'granted' | 'declined';
41
+
42
+ export interface LanScanConsentState {
43
+ readonly decision: LanScanDecision;
44
+ readonly decidedAt: string;
45
+ }
46
+
47
+ type ConsentRoots = Pick<ShellPathService, 'resolveUserPath'>;
48
+
49
+ const CONSENT_FILE_NAME = 'network-discovery-consent.json';
50
+ const DISCOVERED_PROVIDERS_FILE = 'discovered-providers.json';
51
+
52
+ function consentFilePath(roots: ConsentRoots, surfaceRoot: string): string {
53
+ return roots.resolveUserPath(surfaceRoot, CONSENT_FILE_NAME);
54
+ }
55
+
56
+ /**
57
+ * The path the SDK persists discovered servers to, for display in user-facing
58
+ * copy (this module does not read or write that file — the SDK owns it).
59
+ */
60
+ export function discoveredProvidersFilePath(roots: ConsentRoots, surfaceRoot: string): string {
61
+ return roots.resolveUserPath(surfaceRoot, DISCOVERED_PROVIDERS_FILE);
62
+ }
63
+
64
+ /**
65
+ * Load the persisted LAN-scan consent decision.
66
+ * Returns undefined when no decision has ever been recorded (true first run).
67
+ */
68
+ export function loadLanScanConsent(roots: ConsentRoots, surfaceRoot: string): LanScanConsentState | undefined {
69
+ const path = consentFilePath(roots, surfaceRoot);
70
+ try {
71
+ if (!existsSync(path)) return undefined;
72
+ const raw = readFileSync(path, 'utf-8');
73
+ const parsed = JSON.parse(raw) as Partial<LanScanConsentState> | null;
74
+ if (!parsed || (parsed.decision !== 'granted' && parsed.decision !== 'declined')) {
75
+ logger.warn('[LanScanConsent] ignoring invalid consent file', { path });
76
+ return undefined;
77
+ }
78
+ return {
79
+ decision: parsed.decision,
80
+ decidedAt: typeof parsed.decidedAt === 'string' ? parsed.decidedAt : new Date(0).toISOString(),
81
+ };
82
+ } catch (err) {
83
+ logger.warn('[LanScanConsent] load failed; treating as undecided', { path, error: summarizeError(err) });
84
+ return undefined;
85
+ }
86
+ }
87
+
88
+ /** Persist the user's LAN-scan consent decision (explicit grant, decline, or the first-run default). */
89
+ export function saveLanScanConsent(roots: ConsentRoots, surfaceRoot: string, decision: LanScanDecision): void {
90
+ const path = consentFilePath(roots, surfaceRoot);
91
+ try {
92
+ mkdirSync(dirname(path), { recursive: true });
93
+ const state: LanScanConsentState = { decision, decidedAt: new Date().toISOString() };
94
+ writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
95
+ } catch (err) {
96
+ logger.warn('[LanScanConsent] save failed; decision will not persist', { path, error: summarizeError(err) });
97
+ }
98
+ }
99
+
100
+ // ── Wording ──────────────────────────────────────────────────────────────
101
+
102
+ export const NETWORK_SCAN_ENABLE_COMMAND = '/network-scan on';
103
+ export const NETWORK_SCAN_DISABLE_COMMAND = '/network-scan off';
104
+ export const NETWORK_SCAN_STATUS_COMMAND = '/network-scan';
105
+
106
+ /** Shown exactly once — the very first time the Agent starts and no decision has been recorded yet. */
107
+ export function firstRunConsentMessage(roots: ConsentRoots, surfaceRoot: string): string {
108
+ const storePath = discoveredProvidersFilePath(roots, surfaceRoot);
109
+ return (
110
+ '[Scan] Local network scanning for model servers is off by default. ' +
111
+ 'If turned on, it checks other devices on this computer\'s local network (your subnet only — nothing beyond it) ' +
112
+ 'for model servers such as Ollama or LM Studio, so they can be added as chat providers. ' +
113
+ `Servers it finds are saved to ${storePath}. ` +
114
+ `Turn it on with ${NETWORK_SCAN_ENABLE_COMMAND}; nothing is scanned until you do.`
115
+ );
116
+ }
117
+
118
+ /**
119
+ * F3 fix: the first-run explanation (firstRunConsentMessage) already told the
120
+ * user this is off and how to turn it on. Printing that reminder again on
121
+ * EVERY subsequent boot forever — with no way to quiet it short of turning
122
+ * the feature on — was the friction: an indefinitely repeating notice for a
123
+ * state the user already chose. The off state is still fully discoverable on
124
+ * demand via `/network-scan status`; boot itself stays silent about it from
125
+ * the second run onward. The granted-path behavior (framed scan summary) is
126
+ * unchanged.
127
+ */
128
+
129
+ // ── Output framing ───────────────────────────────────────────────────────
130
+
131
+ const FOUND_OR_RESTORED_RE = /\bat\s+\S+:\d+\s+\(\d+\s+models?\)/;
132
+ const REMOVED_RE = /\bat\s+\S+:\d+\s+is no longer reachable\b/;
133
+ const RAW_IP_PAREN_RE = /\s*\(\d{1,3}(?:\.\d{1,3}){3}\)/g;
134
+
135
+ /** Strips a bare "(1.2.3.4)" parenthetical from an otherwise-fine message, defense in depth. */
136
+ function stripRawIpMention(message: string): string {
137
+ return message.replace(RAW_IP_PAREN_RE, '');
138
+ }
139
+
140
+ /**
141
+ * Wraps the real system-message sink + requestRender callback so that the raw
142
+ * per-server "[Scan] Found X at host:port" / "[Local] X at host:port ... from
143
+ * last session" / "[Scan] X at host:port is no longer reachable" lines the SDK
144
+ * emits are never forwarded verbatim. Instead they are counted, and a single
145
+ * framed summary line is emitted the next time requestRender fires — which the
146
+ * SDK reliably calls exactly once per batch of emitted messages (see
147
+ * bootstrap-background.js), so this stays in lockstep with the underlying
148
+ * scan/restore passes without needing to touch the SDK.
149
+ */
150
+ function createFramedDiscoverySink(
151
+ realRouter: HostSystemMessageSink,
152
+ realRequestRender: () => void,
153
+ ): { readonly sink: HostSystemMessageSink; readonly requestRender: () => void } {
154
+ let foundCount = 0;
155
+ let removedCount = 0;
156
+
157
+ const sink: HostSystemMessageSink = {
158
+ low(message: string) {
159
+ if (FOUND_OR_RESTORED_RE.test(message)) {
160
+ foundCount += 1;
161
+ return;
162
+ }
163
+ if (REMOVED_RE.test(message)) {
164
+ removedCount += 1;
165
+ return;
166
+ }
167
+ realRouter.low(stripRawIpMention(message));
168
+ },
169
+ high(message: string) {
170
+ realRouter.high(stripRawIpMention(message));
171
+ },
172
+ };
173
+
174
+ const requestRender = () => {
175
+ if (foundCount > 0) {
176
+ realRouter.low(
177
+ `[Scan] Discovered ${foundCount} local model server${foundCount === 1 ? '' : 's'} on this network — run /provider to see them.`,
178
+ );
179
+ }
180
+ if (removedCount > 0) {
181
+ realRouter.low(
182
+ `[Scan] ${removedCount} previously found server${removedCount === 1 ? '' : 's'} no longer reachable — removed.`,
183
+ );
184
+ }
185
+ foundCount = 0;
186
+ removedCount = 0;
187
+ realRequestRender();
188
+ };
189
+
190
+ return { sink, requestRender };
191
+ }
192
+
193
+ function noopHandle(): BackgroundRuntimeTaskHandle {
194
+ let stopped = false;
195
+ return {
196
+ get stopped() {
197
+ return stopped;
198
+ },
199
+ stop() {
200
+ stopped = true;
201
+ },
202
+ };
203
+ }
204
+
205
+ export interface LanScanGateOptions extends BackgroundProviderDiscoveryOptions {
206
+ readonly shellPaths: BackgroundProviderDiscoveryOptions['shellPaths'] & ConsentRoots;
207
+ /**
208
+ * Injectable seam for tests. Defaults to the real SDK discovery pass
209
+ * (`startBackgroundProviderRegistration`, which performs a real subnet
210
+ * scan). Tests must supply a fake here instead of exercising the real
211
+ * network — never let a test fall through to the default.
212
+ */
213
+ readonly startDiscovery?: (opts: BackgroundProviderDiscoveryOptions) => BackgroundRuntimeTaskHandle;
214
+ }
215
+
216
+ /**
217
+ * The single gated entry point to LAN provider discovery. Bootstrap's Phase 8
218
+ * and the /network-scan command are the only two callers; neither calls the
219
+ * SDK's startBackgroundProviderRegistration/startBackgroundProviderDiscovery
220
+ * directly.
221
+ *
222
+ * - No decision recorded (first run): shows the full consent explanation,
223
+ * persists "declined" as the honest first-run default, and returns without
224
+ * scanning.
225
+ * - Decision is "declined" (F3): stays silent and returns without scanning —
226
+ * the first-run boot already explained the off state and how to turn it
227
+ * on; the ongoing state is discoverable on demand via `/network-scan
228
+ * status`, not repeated on every boot.
229
+ * - Decision is "granted": runs the SDK discovery pass through a framing
230
+ * wrapper that replaces the raw per-server feed dump with a single summary
231
+ * line.
232
+ */
233
+ export function runGatedLanScan(options: LanScanGateOptions): BackgroundRuntimeTaskHandle {
234
+ const { shellPaths, surfaceRoot, systemMessageRouter, requestRender, startDiscovery = startBackgroundProviderRegistration } = options;
235
+ const existing = loadLanScanConsent(shellPaths, surfaceRoot);
236
+
237
+ if (!existing) {
238
+ saveLanScanConsent(shellPaths, surfaceRoot, 'declined');
239
+ systemMessageRouter.low(firstRunConsentMessage(shellPaths, surfaceRoot));
240
+ return noopHandle();
241
+ }
242
+
243
+ if (existing.decision === 'declined') {
244
+ return noopHandle();
245
+ }
246
+
247
+ const framed = createFramedDiscoverySink(systemMessageRouter, requestRender);
248
+ return startDiscovery({
249
+ ...options,
250
+ systemMessageRouter: framed.sink,
251
+ requestRender: framed.requestRender,
252
+ });
253
+ }
@@ -3,6 +3,7 @@ import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
3
3
  import { shell as runtimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
4
4
  import type { shell as RuntimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
5
5
  import { SecretsManager } from '../config/secrets.ts';
6
+ import { FocusTracker } from '../core/focus-tracker.ts';
6
7
  import { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config';
7
8
  import { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
8
9
  import { AutomationDeliveryManager, AutomationManager, AutomationRouteStore } from '@pellux/goodvibes-sdk/platform/automation';
@@ -10,6 +11,13 @@ import { ChannelPluginRegistry, ChannelPolicyManager, RouteBindingManager, Surfa
10
11
  import { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
11
12
  import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
12
13
  import type { SharedSessionRoutingIntent } from '@pellux/goodvibes-sdk/platform/control-plane';
14
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
15
+ import { AGENT_SPINE_PARTICIPANT, SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
16
+ import {
17
+ createSpineConnectionResolver,
18
+ createSpineRestProbe,
19
+ createSpineRestTransport,
20
+ } from './session-spine-rest-transport.ts';
13
21
  import { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
14
22
  import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
15
23
  import {
@@ -32,11 +40,19 @@ import { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents';
32
40
  import { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
33
41
  import { AgentOrchestrator } from '@pellux/goodvibes-sdk/platform/agents';
34
42
  import { ArchetypeLoader } from '@pellux/goodvibes-sdk/platform/agents';
43
+ import { CodeIndexStore } from '@pellux/goodvibes-sdk/platform/state';
44
+ import { CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
45
+ import { createProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
46
+ import { createOrchestrationEngine } from '@pellux/goodvibes-sdk/platform/orchestration';
47
+ import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
35
48
  import { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
36
49
  import { ModeManager } from '@pellux/goodvibes-sdk/platform/state';
37
50
  import { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
38
51
  import { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
39
52
  import { MemoryStore } from '@pellux/goodvibes-sdk/platform/state';
53
+ // W6-C2 (E6): one canonical cross-surface memory store + no-loss legacy fold.
54
+ import { resolveCanonicalMemoryDbPath, foldMemoryStores } from '@pellux/goodvibes-sdk/platform/state';
55
+ import type { LegacyMemorySource, MemoryFoldReport } from '@pellux/goodvibes-sdk/platform/state';
40
56
  import type { RuntimeEventBus } from '@/runtime/index.ts';
41
57
  import { createDomainDispatch } from './store/index.ts';
42
58
  import type { DomainDispatch, RuntimeStore } from './store/index.ts';
@@ -228,7 +244,11 @@ function createDisabledAgentWrfcWorktreeOps(): AgentWrfcWorktreeOps {
228
244
  async cleanup(_agentId: string): Promise<void> {
229
245
  throw agentWrfcWorktreeError('cleanup');
230
246
  },
231
- async commitWorkingTree(_message: string): Promise<string | null> {
247
+ // No explicit return-type annotation: this always throws before returning,
248
+ // so it structurally satisfies whatever CommitWorkingTreeResult shape the
249
+ // SDK's WrfcController#createWorktree option currently declares (derived
250
+ // via AgentWrfcWorktreeOps above) without importing that SDK-internal type.
251
+ async commitWorkingTree(_message: string) {
232
252
  throw agentWrfcWorktreeError('commit');
233
253
  },
234
254
  async currentHead(): Promise<string | null> {
@@ -395,6 +415,7 @@ export interface RuntimeServices extends SdkRuntimeServices {
395
415
  readonly watcherRegistry: WatcherRegistry;
396
416
  readonly approvalBroker: ApprovalBroker;
397
417
  readonly sessionBroker: SharedSessionBroker;
418
+ readonly sessionSpineClient: SessionSpineClient;
398
419
  readonly deliveryManager: AutomationDeliveryManager;
399
420
  readonly automationManager: AutomationManager;
400
421
  readonly gatewayMethods: GatewayMethodCatalog;
@@ -435,6 +456,8 @@ export interface RuntimeServices extends SdkRuntimeServices {
435
456
  readonly worktreeRegistry: WorktreeRegistry;
436
457
  readonly sandboxSessionRegistry: SandboxSessionRegistry;
437
458
  readonly webhookNotifier: WebhookNotifier;
459
+ /** W4-R3 — OS-level terminal focus tracker, ported from goodvibes-tui's W2.3 (core/focus-tracker.ts). */
460
+ readonly focusTracker: FocusTracker;
438
461
  readonly replayEngine: DeterministicReplayEngine;
439
462
  readonly providerOptimizer: ProviderOptimizer;
440
463
  readonly providerCapabilityRegistry: ProviderCapabilityRegistry;
@@ -590,6 +613,21 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
590
613
  agentStatusProvider: agentManager,
591
614
  messageSender: agentMessageBus,
592
615
  });
616
+ // W3-A1: the SDK's extracted session-spine core, consumed via this surface's
617
+ // own REST transport adapter (session-spine-rest-transport.ts) — version-
618
+ // tolerant, since the agent may compile against a pinned SDK predating the
619
+ // typed sessions.register client. Live-immediately mode: passing `transport`
620
+ // at construction starts the keepalive now, for the whole process lifetime
621
+ // (no separate activate() step — the agent has none). Client-only; never
622
+ // starts the daemon. All calls are fire-and-forget; a down daemon degrades to
623
+ // an honest offline queue while the local broker keeps rendering.
624
+ const spineResolveConnection = createSpineConnectionResolver(configManager, homeDirectory);
625
+ const sessionSpineClient = new SessionSpineClient({
626
+ participant: AGENT_SPINE_PARTICIPANT,
627
+ transport: createSpineRestTransport({ resolveConnection: spineResolveConnection }),
628
+ probe: createSpineRestProbe({ resolveConnection: spineResolveConnection }),
629
+ log: logger,
630
+ });
593
631
  sessionBroker.setContinuationRunner(async ({ task, input }) => {
594
632
  const record = agentManager.spawn({
595
633
  mode: 'spawn',
@@ -601,7 +639,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
601
639
  });
602
640
  const artifactStore = new ArtifactStore({ configManager });
603
641
  const memoryEmbeddingRegistry = new MemoryEmbeddingProviderRegistry({ configManager });
604
- const memoryDbPath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
642
+ // W6-C2 (E6): the agent no longer owns a private per-surface memory.sqlite. It opens
643
+ // the ONE canonical cross-surface store so a fact learned here recalls in the TUI (and
644
+ // vice-versa). The old agent-global path is folded in at boot with no loss (see
645
+ // foldAgentLegacyMemory), then left in place — migration never deletes.
646
+ const memoryDbPath = resolveCanonicalMemoryDbPath(shellPaths.homeDirectory);
605
647
  const memoryStore = new MemoryStore(memoryDbPath, {
606
648
  embeddingRegistry: memoryEmbeddingRegistry,
607
649
  });
@@ -737,6 +779,10 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
737
779
  const worktreeRegistry = createDisabledAgentWorktreeRegistry(workingDirectory);
738
780
  // Configured and attached to the runtime bus during bootstrap when webhook URLs are present.
739
781
  const webhookNotifier = new WebhookNotifier();
782
+ // W4-R3 — one shared instance for the process lifetime (mirrors goodvibes-tui's
783
+ // runtime/services.ts); fed from 'focus' tokens in handler-feed.ts and read by
784
+ // the approval-alert wiring in main.ts.
785
+ const focusTracker = new FocusTracker();
740
786
  const replayEngine = new DeterministicReplayEngine(workingDirectory);
741
787
  const providerOptimizer = new ProviderOptimizer(providerRegistry, providerCapabilityRegistry, false);
742
788
  const sessionMemoryStore = new SessionMemoryStore();
@@ -820,12 +866,70 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
820
866
  workflowServices: workflow,
821
867
  });
822
868
 
869
+ // ── One-Platform Wave 0: RuntimeServices members required by SDK 0.38 ──────
870
+ // The Agent is delegation-only: it does not own local build/worktree work.
871
+ // These are constructed as real-but-inert SDK services purely to satisfy the
872
+ // RuntimeServices contract — none auto-start:
873
+ // • orchestrationEngine — never .start()ed here (delegation goes through
874
+ // /delegate to the TUI); worktree isolation is
875
+ // omitted so any accidental run degrades to shared.
876
+ // • codeIndexStore — constructed but neither schema-initialized nor
877
+ // auto-built; the Agent runs no repo source-tree
878
+ // code index (inert unless explicitly invoked).
879
+ // • codeIndexReindexScheduler — real-but-inert twin of the above: isEnabled
880
+ // is a permanent `() => false`, so tool-driven
881
+ // reindex scheduling (Stage B, TUI-only feature)
882
+ // never fires for the Agent.
883
+ // • processRegistry — fleet observability over the managers the Agent
884
+ // already owns (agents, wrfc, processes, watchers).
885
+ // • workspaceCheckpointManager — constructed without a runtimeBus so it
886
+ // never auto-subscribes to turn events / snapshots
887
+ // the workspace; inert unless explicitly invoked.
888
+ const orchestrationEngine = createOrchestrationEngine({
889
+ agentManager,
890
+ configManager,
891
+ runtimeBus: options.runtimeBus,
892
+ projectRoot: workingDirectory,
893
+ });
894
+ const codeIndexStore = new CodeIndexStore(
895
+ workingDirectory,
896
+ join(workingDirectory, '.goodvibes', 'agent', 'code-index.sqlite'),
897
+ memoryEmbeddingRegistry,
898
+ );
899
+ const codeIndexReindexScheduler = new CodeIndexReindexScheduler({
900
+ target: codeIndexStore,
901
+ workingDirectory,
902
+ isEnabled: () => false,
903
+ });
904
+ const processRegistry = createProcessRegistry({
905
+ agentManager,
906
+ wrfcController,
907
+ orchestrationEngine,
908
+ codeIndexService: codeIndexStore,
909
+ processManager,
910
+ watcherRegistry,
911
+ workflow,
912
+ approvalBroker,
913
+ sessionBroker,
914
+ messageBus: agentMessageBus,
915
+ automationManager,
916
+ runtimeBus: options.runtimeBus,
917
+ });
918
+ const workspaceCheckpointManager = new WorkspaceCheckpointManager({
919
+ workspaceRoot: workingDirectory,
920
+ });
921
+
823
922
  return {
824
923
  workingDirectory,
825
924
  homeDirectory,
826
925
  shellPaths,
827
926
  configManager,
828
927
  featureFlags,
928
+ orchestrationEngine,
929
+ codeIndexStore,
930
+ codeIndexReindexScheduler,
931
+ processRegistry,
932
+ workspaceCheckpointManager,
829
933
  runtimeBus: options.runtimeBus,
830
934
  runtimeStore: options.runtimeStore,
831
935
  runtimeDispatch,
@@ -838,6 +942,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
838
942
  watcherRegistry,
839
943
  approvalBroker,
840
944
  sessionBroker,
945
+ sessionSpineClient,
841
946
  deliveryManager,
842
947
  automationManager,
843
948
  gatewayMethods,
@@ -877,6 +982,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
877
982
  worktreeRegistry,
878
983
  sandboxSessionRegistry,
879
984
  webhookNotifier,
985
+ focusTracker,
880
986
  replayEngine,
881
987
  providerOptimizer,
882
988
  providerCapabilityRegistry,
@@ -912,3 +1018,22 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
912
1018
  },
913
1019
  };
914
1020
  }
1021
+
1022
+ /**
1023
+ * W6-C2 (E6): fold the agent's legacy per-surface memory store into the canonical
1024
+ * cross-surface store. Called once at boot AFTER `memoryStore.init()` so any records
1025
+ * written before unification survive. Id-keyed and idempotent — a re-run imports
1026
+ * nothing new and never deletes the legacy file. Returns the report so boot can log
1027
+ * exactly what moved (nothing is silently dropped).
1028
+ */
1029
+ export async function foldAgentLegacyMemory(
1030
+ memoryStore: MemoryStore,
1031
+ memoryEmbeddingRegistry: MemoryEmbeddingProviderRegistry,
1032
+ shellPaths: ShellPathService,
1033
+ ): Promise<MemoryFoldReport> {
1034
+ const legacyAgentGlobal = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
1035
+ const sources: LegacyMemorySource[] = [
1036
+ { label: 'agent-global (pre-E6)', dbPath: legacyAgentGlobal },
1037
+ ];
1038
+ return foldMemoryStores(memoryStore, sources, { embeddingRegistry: memoryEmbeddingRegistry });
1039
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * session-spine-rest-transport.ts
3
+ *
4
+ * The agent's REST transport adapter for the SDK's extracted session-spine core
5
+ * (`@pellux/goodvibes-sdk/platform/runtime/session-spine`). The core is
6
+ * transport-agnostic and never assumes a typed sessions client exists — that is
7
+ * exactly why the agent, which compiles against a pinned/version-tolerant npm
8
+ * SDK that may predate `sessions.register`, supplies this hand-rolled REST
9
+ * mirror instead of a typed client (see
10
+ * docs/decisions/2026-07-05-session-spine-sdk-extraction.md, divergence ruling
11
+ * #1). Do not switch this adapter to a typed client.
12
+ *
13
+ * Also agent-local (per the same decision record, ruling #6 and the SDK-side
14
+ * rejection of "pulling token-reading into the SDK"):
15
+ * - `createSpineConnectionResolver`: builds the daemon base URL from
16
+ * ConfigManager and reads the connected-host operator token via
17
+ * ../runtime/connected-host-auth.ts (home-dir-specific, trust-boundary-bound
18
+ * — the SDK core never reads token files; it only ever receives a
19
+ * pre-built resolver through this adapter).
20
+ * - register/close/probe TIMEOUTS: the SDK core no longer makes the wire
21
+ * call (transport does), so it has nothing to time out. Timeouts are a REST
22
+ * implementation concern and live here, in the adapter that owns fetch.
23
+ */
24
+
25
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
26
+ import type { RegisterSharedSessionInput } from '@pellux/goodvibes-sdk/platform/control-plane';
27
+ import type { SpineResult, SpineTransport } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
28
+ import {
29
+ postSessionClose,
30
+ postSessionRegister,
31
+ type SessionCloseResult,
32
+ type SessionRegisterResult,
33
+ type SessionRegistrationConnection,
34
+ type SessionRegistrationInput,
35
+ } from '../agent/session-registration.ts';
36
+ import { readConnectedHostOperatorToken } from './connected-host-auth.ts';
37
+
38
+ const DEFAULT_REGISTER_TIMEOUT_MS = 1_500;
39
+ const DEFAULT_CLOSE_TIMEOUT_MS = 500;
40
+ const DEFAULT_PROBE_TIMEOUT_MS = 1_500;
41
+
42
+ /**
43
+ * Builds a per-op connection resolver from the config manager (host/port,
44
+ * default 127.0.0.1:3421) and the connected-host operator token (env
45
+ * overrides + the canonical operator-tokens.json). Re-read every op so a
46
+ * token that appears after boot is picked up.
47
+ */
48
+ export function createSpineConnectionResolver(
49
+ configManager: Pick<ConfigManager, 'get'>,
50
+ homeDirectory: string,
51
+ ): () => SessionRegistrationConnection {
52
+ return () => {
53
+ const hostValue = configManager.get('controlPlane.host');
54
+ const portValue = configManager.get('controlPlane.port');
55
+ const host = typeof hostValue === 'string' && hostValue.trim().length > 0 ? hostValue.trim() : '127.0.0.1';
56
+ const port = typeof portValue === 'number' && Number.isFinite(portValue) ? portValue : 3421;
57
+ const token = readConnectedHostOperatorToken(homeDirectory);
58
+ return { baseUrl: `http://${host}:${port}`, token: token.token, tokenPath: token.path };
59
+ };
60
+ }
61
+
62
+ /** Maps the SDK's canonical register input onto the REST mirror's wire shape. No `kind` field: the REST mirror always stamps SESSION_REGISTER_KIND server-side (see session-registration.ts), matching the SDK's `recordKind` left unset for this participant. */
63
+ function toRegistrationInput(input: RegisterSharedSessionInput): SessionRegistrationInput {
64
+ return {
65
+ sessionId: input.sessionId,
66
+ ...(input.project ? { project: input.project } : {}),
67
+ ...(input.title ? { title: input.title } : {}),
68
+ participant: {
69
+ surfaceKind: input.participant.surfaceKind,
70
+ surfaceId: input.participant.surfaceId,
71
+ ...(input.participant.externalId ? { externalId: input.participant.externalId } : {}),
72
+ ...(input.participant.userId ? { userId: input.participant.userId } : {}),
73
+ ...(input.participant.displayName ? { displayName: input.participant.displayName } : {}),
74
+ ...(input.participant.routeId ? { routeId: input.participant.routeId } : {}),
75
+ lastSeenAt: input.participant.lastSeenAt,
76
+ },
77
+ ...(input.reopen ? { reopen: true } : {}),
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Folds the REST mirror's richer failure vocabulary onto the SDK core's
83
+ * ok/offline/rejected outcome (decision-record ruling #5): `ok` stays `ok`;
84
+ * `connected_host_unavailable` (a transient connectivity fault) becomes
85
+ * `offline` so the core enqueues for reconnect replay; every DURABLE refusal
86
+ * (`auth_required`, `connected_host_route_unavailable`, `connected_host_error`)
87
+ * becomes `rejected` so the core logs it and never enqueues it (no
88
+ * retry-forever).
89
+ */
90
+ function foldResult(result: SessionRegisterResult | SessionCloseResult): SpineResult {
91
+ if (result.ok) return { outcome: 'ok' };
92
+ if (result.kind === 'connected_host_unavailable') return { outcome: 'offline', error: result.error };
93
+ return { outcome: 'rejected', error: result.error };
94
+ }
95
+
96
+ export interface SpineRestTransportOptions {
97
+ readonly resolveConnection: () => SessionRegistrationConnection;
98
+ readonly registerTimeoutMs?: number;
99
+ readonly closeTimeoutMs?: number;
100
+ }
101
+
102
+ /**
103
+ * The agent's version-tolerant REST transport injected into the SDK
104
+ * `SessionSpineClient` at construction (live-immediately mode). Wraps
105
+ * postSessionRegister/postSessionClose and folds their result kinds onto
106
+ * {@link SpineResult}.
107
+ */
108
+ export function createSpineRestTransport(options: SpineRestTransportOptions): SpineTransport {
109
+ const registerTimeoutMs = options.registerTimeoutMs ?? DEFAULT_REGISTER_TIMEOUT_MS;
110
+ const closeTimeoutMs = options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
111
+ return {
112
+ async register(input: RegisterSharedSessionInput): Promise<SpineResult> {
113
+ const connection = options.resolveConnection();
114
+ const result = await postSessionRegister(connection, toRegistrationInput(input), { timeoutMs: registerTimeoutMs });
115
+ return foldResult(result);
116
+ },
117
+ async close(sessionId: string): Promise<SpineResult> {
118
+ const connection = options.resolveConnection();
119
+ const result = await postSessionClose(connection, sessionId, { timeoutMs: closeTimeoutMs });
120
+ return foldResult(result);
121
+ },
122
+ };
123
+ }
124
+
125
+ async function defaultProbe(connection: SessionRegistrationConnection, timeoutMs: number): Promise<boolean> {
126
+ const controller = new AbortController();
127
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
128
+ try {
129
+ // Any HTTP response (even 401) means the host answered -> reachable. Auth is
130
+ // a separate concern surfaced by register/close results, not the probe.
131
+ await fetch(`${connection.baseUrl}/status`, {
132
+ headers: connection.token ? { authorization: `Bearer ${connection.token}` } : undefined,
133
+ signal: controller.signal,
134
+ });
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ } finally {
139
+ clearTimeout(timer);
140
+ }
141
+ }
142
+
143
+ export interface SpineRestProbeOptions {
144
+ readonly resolveConnection: () => SessionRegistrationConnection;
145
+ readonly probeTimeoutMs?: number;
146
+ /** Override for tests; default does a short GET {baseUrl}/status. */
147
+ readonly probeImpl?: (connection: SessionRegistrationConnection, timeoutMs: number) => Promise<boolean>;
148
+ }
149
+
150
+ /**
151
+ * Builds the zero-argument `probe` the SDK core calls directly from
152
+ * `probeReachability()` (its injected-probe shape takes no parameters — the
153
+ * core has no connection to hand it). This closure captures the resolver and
154
+ * timeout so the core stays connection-agnostic.
155
+ */
156
+ export function createSpineRestProbe(options: SpineRestProbeOptions): () => Promise<boolean> {
157
+ const probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
158
+ const probeImpl = options.probeImpl ?? defaultProbe;
159
+ return () => probeImpl(options.resolveConnection(), probeTimeoutMs);
160
+ }
@@ -229,7 +229,12 @@ export function installTuiTerminalOutputGuard(options: TuiTerminalOutputGuardOpt
229
229
  capturedWriteCount = 0;
230
230
  lastNoticeAt = now;
231
231
  const plural = count === 1 ? '' : 's';
232
- options.notify(`[Terminal] Captured ${count} direct ${event.source} write${plural} that would have corrupted the TUI: ${event.preview}`);
232
+ const notice = `[Terminal] Captured ${count} direct ${event.source} write${plural} that would have corrupted the TUI: ${event.preview}`;
233
+ // Keep the aggregate count reachable in the activity log even though the
234
+ // noise gate (system-message-noise.ts) drops the notice from the Recent
235
+ // feed — drop-from-the-feed, not delete. (W4-R2 honest degraded state.)
236
+ logger.info(notice, { source: event.source, count });
237
+ options.notify(notice);
233
238
  },
234
239
  });
235
240
  }
@@ -54,6 +54,8 @@ export interface UiPlatformServices {
54
54
  readonly tokenAuditor: RuntimeServices['tokenAuditor'];
55
55
  readonly replayEngine: RuntimeServices['replayEngine'];
56
56
  readonly webhookNotifier: RuntimeServices['webhookNotifier'];
57
+ /** W4-R3 — OS-level terminal focus tracker, ported from goodvibes-tui's W2.3. */
58
+ readonly focusTracker: RuntimeServices['focusTracker'];
57
59
  readonly policyRuntimeState: RuntimeServices['policyRuntimeState'];
58
60
  readonly externalServices?: {
59
61
  inspect(): {
@@ -165,6 +167,7 @@ export function createUiRuntimeServices(
165
167
  tokenAuditor: runtimeServices.tokenAuditor,
166
168
  replayEngine: runtimeServices.replayEngine,
167
169
  webhookNotifier: runtimeServices.webhookNotifier,
170
+ focusTracker: runtimeServices.focusTracker,
168
171
  policyRuntimeState: runtimeServices.policyRuntimeState,
169
172
  },
170
173
  planning: {