@smartmemory/compose 0.1.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 (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Vision Server — REST endpoints + WebSocket broadcast for the vision surface.
3
+ * Follows the same attach() pattern as FileWatcherServer.
4
+ */
5
+
6
+ import { WebSocketServer } from 'ws';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { requireSensitiveToken } from './security.js';
11
+ import { spawnJournalAgent, extractSlugFromPath } from './vision-utils.js';
12
+ import { attachFeatureScanRoutes } from './feature-scan.js';
13
+ import { attachGraphExportRoutes } from './graph-export.js';
14
+ import { StratumSync, attachStratumRoutes } from './stratum-sync.js';
15
+ import { createStratumRouter } from './stratum-api.js';
16
+ import { attachAgentSpawnRoutes } from './agent-spawn.js';
17
+ import { AgentRegistry } from './agent-registry.js';
18
+ import { HealthMonitor } from './agent-health.js';
19
+ import { WorktreeGC } from './worktree-gc.js';
20
+ import { attachVisionRoutes } from './vision-routes.js';
21
+ import { attachSessionRoutes } from './session-routes.js';
22
+ import { attachActivityRoutes } from './activity-routes.js';
23
+ import { SettingsStore } from './settings-store.js';
24
+ import { attachSettingsRoutes } from './settings-routes.js';
25
+ import { attachDesignRoutes } from './design-routes.js';
26
+ import { DesignSessionManager } from './design-session.js';
27
+ import { ClaudeSDKConnector } from './connectors/claude-sdk-connector.js';
28
+ import { attachPipelineRoutes } from './pipeline-routes.js';
29
+ import { attachIdeaboxRoutes } from './ideabox-routes.js';
30
+ import { CoalescingBuffer } from './coalescing-buffer.js';
31
+ /** Settings defaults (previously derived from contracts/lifecycle.json). */
32
+ const SETTINGS_DEFAULTS = {
33
+ phases: [
34
+ { id: 'explore_design', defaultPolicy: null },
35
+ { id: 'prd', defaultPolicy: 'skip' },
36
+ { id: 'architecture', defaultPolicy: 'skip' },
37
+ { id: 'blueprint', defaultPolicy: 'gate' },
38
+ { id: 'verification', defaultPolicy: 'gate' },
39
+ { id: 'plan', defaultPolicy: 'gate' },
40
+ { id: 'execute', defaultPolicy: 'flag' },
41
+ { id: 'report', defaultPolicy: 'skip' },
42
+ { id: 'docs', defaultPolicy: 'flag' },
43
+ { id: 'ship', defaultPolicy: 'gate' },
44
+ ],
45
+ iterationDefaults: {
46
+ review: { maxIterations: 4, timeout: 15, maxTotal: 20 },
47
+ coverage: { maxIterations: 15, timeout: 30, maxTotal: 50 },
48
+ },
49
+ policyModes: ['gate', 'flag', 'skip'],
50
+ };
51
+
52
+ import { getTargetRoot, getDataDir } from './project-root.js';
53
+
54
+ export class VisionServer {
55
+ constructor(store, sessionManager = null, { config } = {}) {
56
+ this.store = store;
57
+ this.sessionManager = sessionManager;
58
+ this._config = config || { capabilities: { stratum: true } };
59
+ this.clients = new Set();
60
+ this.wss = null;
61
+ this._coalescingBuffer = new CoalescingBuffer((flushed) => {
62
+ if (flushed.visionState) {
63
+ this.broadcastMessage(Object.assign({ type: 'visionState' }, flushed.visionState));
64
+ }
65
+ }, { intervalMs: 16 });
66
+ this._coalescingBuffer.register('visionState', 'latest-wins');
67
+ this._pendingSnapshots = new Map();
68
+ this._stratumSync = null;
69
+ }
70
+
71
+ attach(httpServer, app) {
72
+ // ── Settings store ────────────────────────────────────────────────────
73
+ this.settingsStore = new SettingsStore(undefined, SETTINGS_DEFAULTS);
74
+
75
+ // ── Settings routes ───────────────────────────────────────────────────
76
+ attachSettingsRoutes(app, {
77
+ settingsStore: this.settingsStore,
78
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
79
+ });
80
+
81
+ // ── Vision CRUD + plan/parse routes ────────────────────────────────────
82
+ attachVisionRoutes(app, {
83
+ store: this.store,
84
+ scheduleBroadcast: () => this.scheduleBroadcast(),
85
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
86
+ projectRoot: getTargetRoot(),
87
+ settingsStore: this.settingsStore,
88
+ });
89
+
90
+ // ── Activity + error routes ─────────────────────────────────────────────
91
+ attachActivityRoutes(app, {
92
+ store: this.store,
93
+ sessionManager: this.sessionManager,
94
+ scheduleBroadcast: () => this.scheduleBroadcast(),
95
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
96
+ resolveItems: (fp) => this.resolveItems(fp),
97
+ });
98
+
99
+ // ── Session routes ──────────────────────────────────────────────────────
100
+ attachSessionRoutes(app, {
101
+ sessionManager: this.sessionManager,
102
+ scheduleBroadcast: () => this.scheduleBroadcast(),
103
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
104
+ spawnJournalAgent,
105
+ projectRoot: getTargetRoot(),
106
+ store: this.store,
107
+ });
108
+
109
+ // ── Pipeline authoring routes ──────────────────────────────────────────
110
+ attachPipelineRoutes(app, {
111
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
112
+ scheduleBroadcast: () => this.scheduleBroadcast(),
113
+ getDataDir: () => getDataDir(),
114
+ getPipelinesDir: () => path.join(getTargetRoot(), 'pipelines'),
115
+ stratumClient: null, // V1: no MCP client in server context; fallback YAML parse is acceptable
116
+ });
117
+
118
+ // ── Ideabox routes ────────────────────────────────────────────────────────
119
+ attachIdeaboxRoutes(app, {
120
+ getProjectRoot: () => getTargetRoot(),
121
+ getDataDir: () => getDataDir(),
122
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
123
+ });
124
+
125
+ // ── Design conversation routes ──────────────────────────────────────────
126
+ // Re-resolve on every call so project switches get fresh instances.
127
+ let _designSessionManager = null;
128
+ let _designDataDir = null;
129
+ let _designConnector = null;
130
+ let _designCwd = null;
131
+
132
+ attachDesignRoutes(app, {
133
+ getSessionManager: () => {
134
+ const dataDir = getDataDir();
135
+ if (dataDir !== _designDataDir) {
136
+ if (_designSessionManager) _designSessionManager.destroy();
137
+ _designSessionManager = new DesignSessionManager(dataDir);
138
+ _designDataDir = dataDir;
139
+ }
140
+ return _designSessionManager;
141
+ },
142
+ getConnector: () => {
143
+ const cwd = getTargetRoot();
144
+ if (cwd !== _designCwd) {
145
+ _designConnector = new ClaudeSDKConnector({ cwd });
146
+ _designCwd = cwd;
147
+ }
148
+ return _designConnector;
149
+ },
150
+ getProjectRoot: () => getTargetRoot(),
151
+ });
152
+
153
+ // ── Build state hydration ─────────────────────────────────────────────
154
+ app.get('/api/build/state', (_req, res) => {
155
+ const buildPath = path.join(getDataDir(), 'active-build.json');
156
+ try {
157
+ if (fs.existsSync(buildPath)) {
158
+ const state = JSON.parse(fs.readFileSync(buildPath, 'utf-8'));
159
+ res.json({ state });
160
+ } else {
161
+ res.json({ state: null });
162
+ }
163
+ } catch {
164
+ res.json({ state: null });
165
+ }
166
+ });
167
+
168
+ // ── Snapshot route (stays inline: uses this._pendingSnapshots + this.clients) ──
169
+ app.get('/api/snapshot', (req, res) => {
170
+ const requestId = `snap-${Date.now()}`;
171
+ const timeout = parseInt(req.query.timeout) || 3000;
172
+
173
+ let target = null;
174
+ for (const client of this.clients) {
175
+ if (client.readyState === 1) { target = client; break; }
176
+ }
177
+ if (!target) {
178
+ return res.status(503).json({ error: 'No connected clients' });
179
+ }
180
+
181
+ const timer = setTimeout(() => {
182
+ this._pendingSnapshots.delete(requestId);
183
+ res.status(504).json({ error: 'Snapshot timeout' });
184
+ }, timeout);
185
+
186
+ this._pendingSnapshots.set(requestId, { res, timer });
187
+
188
+ try {
189
+ target.send(JSON.stringify({ type: 'snapshotRequest', requestId }));
190
+ } catch (err) {
191
+ clearTimeout(timer);
192
+ this._pendingSnapshots.delete(requestId);
193
+ res.status(500).json({ error: err.message });
194
+ }
195
+ });
196
+
197
+ // ── Agent spawn routes + lifecycle services ────────────────────────────
198
+ // TODO: Load lifecycle config (silenceKillMs, defaultTimeoutMs, memoryLimitMB,
199
+ // gcIntervalMs, gcMaxAgeMs) from .compose/compose.json and pass to HealthMonitor
200
+ // and WorktreeGC constructors. Defaults work fine for V1.
201
+ const agentRegistry = new AgentRegistry(getDataDir());
202
+ this._healthMonitor = new HealthMonitor({
203
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
204
+ });
205
+ this._worktreeGC = new WorktreeGC({
206
+ projectRoot: getTargetRoot(),
207
+ parDir: path.join(getTargetRoot(), '.compose', 'par'),
208
+ });
209
+ this._worktreeGC.start();
210
+ attachAgentSpawnRoutes(app, {
211
+ projectRoot: getTargetRoot(),
212
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
213
+ requireSensitiveToken,
214
+ registry: agentRegistry,
215
+ sessionManager: this.sessionManager,
216
+ healthMonitor: this._healthMonitor,
217
+ worktreeGC: this._worktreeGC,
218
+ });
219
+
220
+ // ── Feature scan routes ────────────────────────────────────────────────
221
+ attachFeatureScanRoutes(app, {
222
+ store: this.store,
223
+ scheduleBroadcast: () => this.scheduleBroadcast(),
224
+ });
225
+
226
+ // ── Graph export routes ──────────────────────────────────────────────
227
+ attachGraphExportRoutes(app, { store: this.store });
228
+
229
+ // ── Stratum (conditional) ────────────────────────────────────────────
230
+ if (this._config.capabilities?.stratum) {
231
+ app.use('/api/stratum', createStratumRouter());
232
+ this._stratumSync = new StratumSync(this.store, () => this.scheduleBroadcast());
233
+ attachStratumRoutes(app, {
234
+ store: this.store,
235
+ scheduleBroadcast: () => this.scheduleBroadcast(),
236
+ broadcastMessage: (msg) => this.broadcastMessage(msg),
237
+ sync: this._stratumSync,
238
+ });
239
+ this._stratumSync.start();
240
+ console.log('[vision] Stratum sync enabled');
241
+ } else {
242
+ app.use('/api/stratum', (_req, res) => {
243
+ res.status(503).json({ error: 'Stratum not enabled', hint: 'pip install stratum && compose init' });
244
+ });
245
+ }
246
+
247
+ // ── Haiku summary broadcast ─────────────────────────────────────────────
248
+ if (this.sessionManager) {
249
+ this.sessionManager.onSummary((summary) => {
250
+ this.broadcastMessage({ type: 'sessionSummary', ...summary, timestamp: new Date().toISOString() });
251
+ });
252
+ }
253
+
254
+ // ── WebSocket ───────────────────────────────────────────────────────────
255
+ this.wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
256
+
257
+ this.wss.on('connection', (ws) => {
258
+ this.clients.add(ws);
259
+ console.log(`[vision] Client connected (${this.clients.size} total)`);
260
+
261
+ try {
262
+ this.getVisionSnapshot(ws);
263
+ ws.send(JSON.stringify({ type: 'settingsState', settings: this.settingsStore.get() }));
264
+ } catch (err) {
265
+ console.error('[vision] Error sending initial state:', err.message);
266
+ }
267
+
268
+ ws.on('message', (data) => {
269
+ try {
270
+ const msg = JSON.parse(data);
271
+ if (msg.type === 'snapshotResponse' && msg.requestId) {
272
+ const pending = this._pendingSnapshots.get(msg.requestId);
273
+ if (pending) {
274
+ clearTimeout(pending.timer);
275
+ this._pendingSnapshots.delete(msg.requestId);
276
+ pending.res.json(msg.snapshot);
277
+ }
278
+ }
279
+ } catch {
280
+ // ignore malformed messages
281
+ }
282
+ });
283
+
284
+ ws.on('close', () => {
285
+ this.clients.delete(ws);
286
+ console.log(`[vision] Client disconnected (${this.clients.size} total)`);
287
+ });
288
+
289
+ ws.on('error', (err) => {
290
+ console.error('[vision] WebSocket error:', err.message);
291
+ this.clients.delete(ws);
292
+ });
293
+ });
294
+
295
+ console.log('Vision server attached (REST + WebSocket at /ws/vision)');
296
+ }
297
+
298
+ /** Schedule a coalesced broadcast via CoalescingBuffer (16ms interval, latest-wins) */
299
+ scheduleBroadcast() {
300
+ this._coalescingBuffer.put('visionState', this.store.getState());
301
+ }
302
+
303
+ /** Send a hydrate snapshot to a single newly-connected WebSocket client */
304
+ getVisionSnapshot(ws) {
305
+ try {
306
+ const state = this.store.getState();
307
+ if ('type' in state) {
308
+ throw new Error('store.getState() must not include a `type` field — would collide with hydrate envelope');
309
+ }
310
+ const snapshot = Object.assign(
311
+ { type: 'hydrate' },
312
+ state,
313
+ { sessions: this.sessionManager?.getRecentSessions?.() || [] }
314
+ );
315
+ ws.send(JSON.stringify(snapshot));
316
+ } catch (err) {
317
+ console.error('[vision] Hydrate error:', err.message);
318
+ }
319
+ }
320
+
321
+ /** Broadcast full state to all connected clients */
322
+ broadcastState() {
323
+ this.broadcastMessage({ type: 'visionState', ...this.store.getState(), sessions: this.sessionManager?.getRecentSessions?.() || [] });
324
+ }
325
+
326
+ /** Broadcast any message to all connected clients */
327
+ broadcastMessage(msg) {
328
+ const data = JSON.stringify(msg);
329
+ for (const client of this.clients) {
330
+ if (client.readyState === 1) {
331
+ try {
332
+ client.send(data);
333
+ } catch (err) {
334
+ console.error('[vision] Broadcast error:', err.message);
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+ close() {
341
+ this._coalescingBuffer?.stop();
342
+ if (this._stratumSync) this._stratumSync.stop();
343
+ if (this._healthMonitor) this._healthMonitor.destroy();
344
+ if (this._worktreeGC) this._worktreeGC.stop();
345
+ for (const client of this.clients) {
346
+ client.close();
347
+ }
348
+ this.clients.clear();
349
+ if (this.wss) this.wss.close();
350
+ }
351
+
352
+ /** Resolve a file path to matching tracker items */
353
+ resolveItems(filePath) {
354
+ const rel = filePath.startsWith(getTargetRoot())
355
+ ? filePath.slice(getTargetRoot().length + 1)
356
+ : filePath.replace(/^\.\//, '');
357
+ const matches = [];
358
+ const matchType = new Map();
359
+
360
+ for (const item of this.store.items.values()) {
361
+ if (item.files && item.files.length > 0) {
362
+ for (const pattern of item.files) {
363
+ if (pattern.endsWith('/')) {
364
+ if (rel.startsWith(pattern)) { matches.push(item); matchType.set(item.id, 'prefix'); break; }
365
+ } else {
366
+ if (rel === pattern) { matches.push(item); matchType.set(item.id, 'exact'); break; }
367
+ }
368
+ }
369
+ }
370
+ if (rel.startsWith('docs/') && item.slug) {
371
+ const slug = extractSlugFromPath(rel);
372
+ if (slug && slug === item.slug) {
373
+ if (!matches.find(m => m.id === item.id)) {
374
+ matches.push(item);
375
+ matchType.set(item.id, 'slug');
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ const specificity = { exact: 0, prefix: 1, slug: 2 };
382
+ matches.sort((a, b) => {
383
+ if (a.status === 'in_progress' && b.status !== 'in_progress') return -1;
384
+ if (b.status === 'in_progress' && a.status !== 'in_progress') return 1;
385
+ const sa = specificity[matchType.get(a.id)] ?? 3;
386
+ const sb = specificity[matchType.get(b.id)] ?? 3;
387
+ if (sa !== sb) return sa - sb;
388
+ return new Date(b.updatedAt) - new Date(a.updatedAt);
389
+ });
390
+
391
+ return matches;
392
+ }
393
+ }