@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,502 @@
1
+ /**
2
+ * BuildStreamBridge — tails .compose/build-stream.jsonl and rebroadcasts
3
+ * events as SSE messages via the agent-server's broadcast() function.
4
+ *
5
+ * File-based decoupling: the CLI writes JSONL, this bridge reads it.
6
+ * The CLI and server are separate OS processes with independent lifecycles.
7
+ */
8
+
9
+ import { readFileSync, statSync, watch } from 'node:fs';
10
+ import { join, dirname, basename } from 'node:path';
11
+ import { existsSync } from 'node:fs';
12
+
13
+ const JSONL_FILENAME = 'build-stream.jsonl';
14
+ const DEFAULT_CRASH_TIMEOUT_MS = 300_000; // 5 min
15
+ const STALE_GATE_TIMEOUT_MS = 86_400_000; // 24h
16
+ const DEBOUNCE_MS = 50;
17
+ const POLL_INTERVAL_MS = 2000;
18
+
19
+ export class BuildStreamBridge {
20
+ #filePath;
21
+ #composeDir;
22
+ #broadcast;
23
+ #crashTimeoutMs;
24
+
25
+ // Byte-level cursor tracking
26
+ #cursor = 0;
27
+ #lastSeq = -1;
28
+ #lastIno = null;
29
+ #trailingFragment = '';
30
+
31
+ // Lifecycle state for crash detection
32
+ #buildActive = false;
33
+ #inStep = false;
34
+
35
+ // Timers and watchers
36
+ #watcher = null;
37
+ #pollInterval = null;
38
+ #debounceTimer = null;
39
+ #crashTimer = null;
40
+ #polling = false;
41
+
42
+ /**
43
+ * @param {string} composeDir Path to .compose directory
44
+ * @param {Function} broadcast broadcast(msg) function from agent-server
45
+ * @param {object} [opts]
46
+ * @param {number} [opts.crashTimeoutMs] Crash detection timeout (default 5min)
47
+ */
48
+ constructor(composeDir, broadcast, opts = {}) {
49
+ this.#composeDir = composeDir;
50
+ this.#filePath = join(composeDir, JSONL_FILENAME);
51
+ this.#broadcast = broadcast;
52
+ this.#crashTimeoutMs = opts.crashTimeoutMs ?? DEFAULT_CRASH_TIMEOUT_MS;
53
+ }
54
+
55
+ /**
56
+ * Begin tailing the JSONL file. Catches up from byte 0 if file already
57
+ * exists and is fresh (active build).
58
+ */
59
+ start() {
60
+ if (existsSync(this.#composeDir)) {
61
+ this._startWatching();
62
+ // Catch up if file exists and is fresh
63
+ if (existsSync(this.#filePath)) {
64
+ if (!this._isStaleOnStartup()) {
65
+ this.#cursor = 0;
66
+ this.#lastSeq = -1;
67
+ this._readNewLines();
68
+ } else {
69
+ // Stale file — skip to EOF
70
+ try {
71
+ const stat = statSync(this.#filePath);
72
+ this.#cursor = stat.size;
73
+ this.#lastIno = stat.ino;
74
+ } catch { /* ignore */ }
75
+ }
76
+ }
77
+ } else {
78
+ // Directory doesn't exist yet — poll until it appears
79
+ this._pollForDirectory();
80
+ }
81
+ }
82
+
83
+ /** Stop all timers, watchers, and intervals. */
84
+ stop() {
85
+ if (this.#watcher) {
86
+ this.#watcher.close();
87
+ this.#watcher = null;
88
+ }
89
+ if (this.#pollInterval) {
90
+ clearInterval(this.#pollInterval);
91
+ this.#pollInterval = null;
92
+ }
93
+ if (this.#debounceTimer) {
94
+ clearTimeout(this.#debounceTimer);
95
+ this.#debounceTimer = null;
96
+ }
97
+ if (this.#crashTimer) {
98
+ clearTimeout(this.#crashTimer);
99
+ this.#crashTimer = null;
100
+ }
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // File watching
105
+ // ---------------------------------------------------------------------------
106
+
107
+ _startWatching() {
108
+ try {
109
+ this.#watcher = watch(this.#composeDir, (eventType, filename) => {
110
+ if (filename === JSONL_FILENAME || filename === null) {
111
+ this._debouncedRead();
112
+ }
113
+ });
114
+ this.#watcher.on('error', () => {
115
+ // Watcher died — fall back to polling
116
+ this.#watcher = null;
117
+ this._pollForDirectory();
118
+ });
119
+ } catch {
120
+ // fs.watch can throw on some platforms — fall back to polling
121
+ this._pollForDirectory();
122
+ }
123
+ }
124
+
125
+ _pollForDirectory() {
126
+ if (this.#polling) return;
127
+ this.#polling = true;
128
+
129
+ this.#pollInterval = setInterval(() => {
130
+ if (existsSync(this.#composeDir)) {
131
+ clearInterval(this.#pollInterval);
132
+ this.#pollInterval = null;
133
+ this.#polling = false;
134
+ this._startWatching();
135
+ // Check if file appeared while polling
136
+ if (existsSync(this.#filePath)) {
137
+ this._readNewLines();
138
+ }
139
+ }
140
+ }, POLL_INTERVAL_MS);
141
+ this.#pollInterval.unref();
142
+ }
143
+
144
+ _debouncedRead() {
145
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
146
+ this.#debounceTimer = setTimeout(() => {
147
+ this.#debounceTimer = null;
148
+ this._readNewLines();
149
+ }, DEBOUNCE_MS);
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Core read logic — Buffer-based cursor tracking
154
+ // ---------------------------------------------------------------------------
155
+
156
+ _readNewLines() {
157
+ let stat;
158
+ try {
159
+ stat = statSync(this.#filePath);
160
+ } catch {
161
+ return; // file doesn't exist yet
162
+ }
163
+
164
+ // Inode change detection (primary): file was replaced
165
+ if (this.#lastIno !== null && stat.ino !== this.#lastIno) {
166
+ this.#cursor = 0;
167
+ this.#lastSeq = -1;
168
+ this.#trailingFragment = '';
169
+ }
170
+ // Size-based fallback: truncation without inode change
171
+ else if (stat.size < this.#cursor) {
172
+ this.#cursor = 0;
173
+ this.#lastSeq = -1;
174
+ this.#trailingFragment = '';
175
+ }
176
+
177
+ this.#lastIno = stat.ino;
178
+
179
+ if (stat.size <= this.#cursor) return; // no new data
180
+
181
+ // Read new bytes as Buffer
182
+ let buf;
183
+ try {
184
+ const fd = readFileSync(this.#filePath);
185
+ buf = fd.subarray(this.#cursor, stat.size);
186
+ } catch {
187
+ return; // read error — will retry on next event
188
+ }
189
+
190
+ this.#cursor = stat.size;
191
+
192
+ // Convert to string and split on newlines
193
+ const text = this.#trailingFragment + buf.toString('utf-8');
194
+ const lines = text.split('\n');
195
+
196
+ // Last element is either empty (complete line) or a trailing fragment
197
+ this.#trailingFragment = lines.pop() ?? '';
198
+
199
+ for (const line of lines) {
200
+ if (!line.trim()) continue;
201
+
202
+ let event;
203
+ try {
204
+ event = JSON.parse(line);
205
+ } catch {
206
+ continue; // malformed JSON — skip
207
+ }
208
+
209
+ // Dedup via monotonic _seq guard
210
+ if (typeof event._seq === 'number' && event._seq <= this.#lastSeq) {
211
+ continue;
212
+ }
213
+ if (typeof event._seq === 'number') {
214
+ this.#lastSeq = event._seq;
215
+ }
216
+
217
+ // Map and broadcast
218
+ const mapped = this._mapEvent(event);
219
+ if (mapped) {
220
+ this.#broadcast(mapped);
221
+ }
222
+ }
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Stale file detection on startup
227
+ // ---------------------------------------------------------------------------
228
+
229
+ _isStaleOnStartup() {
230
+ try {
231
+ const content = readFileSync(this.#filePath, 'utf-8').trim();
232
+ if (!content) return true;
233
+
234
+ const lines = content.split('\n');
235
+ const lastLine = lines[lines.length - 1];
236
+ const stat = statSync(this.#filePath);
237
+ const age = Date.now() - stat.mtimeMs;
238
+
239
+ let lastEvent;
240
+ try {
241
+ lastEvent = JSON.parse(lastLine);
242
+ } catch {
243
+ // Malformed last line — stale if old enough
244
+ return age > this.#crashTimeoutMs;
245
+ }
246
+
247
+ // Completed/killed/aborted build — stale
248
+ if (lastEvent.type === 'build_end') return true;
249
+
250
+ // Gate-pending: stale only if older than 24h (gates have unbounded wait)
251
+ if (lastEvent.type === 'build_gate') {
252
+ return age > STALE_GATE_TIMEOUT_MS;
253
+ }
254
+
255
+ // Non-gate, non-terminal: stale if older than crash timeout
256
+ return age > this.#crashTimeoutMs;
257
+ } catch {
258
+ return true; // can't read — treat as stale
259
+ }
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Event mapping (JSONL -> SSE)
264
+ // ---------------------------------------------------------------------------
265
+
266
+ _mapEvent(event) {
267
+ const type = event.type;
268
+
269
+ // Track lifecycle state for crash timer
270
+ if (type === 'build_start' || type === 'build_resume') {
271
+ this.#buildActive = true;
272
+ this.#inStep = false;
273
+ this._clearCrashTimer();
274
+ } else if (type === 'build_step_start') {
275
+ this.#inStep = true;
276
+ this._resetCrashTimer();
277
+ } else if (type === 'build_step_done' || type === 'build_gate') {
278
+ this.#inStep = false;
279
+ this._clearCrashTimer();
280
+ } else if (type === 'build_end') {
281
+ this.#buildActive = false;
282
+ this.#inStep = false;
283
+ this._clearCrashTimer();
284
+ } else if (type === 'tool_use' || type === 'assistant') {
285
+ // Content events during active step — reset crash timer
286
+ if (this.#inStep) this._resetCrashTimer();
287
+ }
288
+
289
+ switch (type) {
290
+ case 'build_start':
291
+ case 'build_resume':
292
+ return {
293
+ type: 'system', subtype: type,
294
+ featureCode: event.featureCode, flowId: event.flowId,
295
+ _source: 'build',
296
+ };
297
+
298
+ case 'build_step_start':
299
+ return {
300
+ type: 'system', subtype: 'build_step',
301
+ stepId: event.stepId, stepNum: event.stepNum,
302
+ totalSteps: event.totalSteps, agent: event.agent,
303
+ intent: event.intent,
304
+ flowId: event.flowId,
305
+ ...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
306
+ ...(event.parallel ? { parallel: true } : {}),
307
+ _source: 'build',
308
+ };
309
+
310
+ case 'tool_use':
311
+ return {
312
+ type: 'assistant',
313
+ message: { content: [{ type: 'tool_use', name: event.tool, input: event.input }] },
314
+ _source: 'build',
315
+ };
316
+
317
+ case 'tool_use_summary':
318
+ return {
319
+ type: 'assistant', subtype: 'tool_use_summary',
320
+ summary: event.summary, output: event.output,
321
+ _source: 'build',
322
+ };
323
+
324
+ case 'tool_progress':
325
+ return {
326
+ type: 'tool_progress',
327
+ tool: event.tool, elapsed: event.elapsed,
328
+ _source: 'build',
329
+ };
330
+
331
+ case 'assistant':
332
+ return {
333
+ type: 'assistant',
334
+ message: { content: [{ type: 'text', text: event.content }] },
335
+ _source: 'build',
336
+ };
337
+
338
+ case 'build_step_done':
339
+ return {
340
+ type: 'system', subtype: 'build_step_done',
341
+ stepId: event.stepId, summary: event.summary,
342
+ retries: event.retries, violations: event.violations,
343
+ flowId: event.flowId,
344
+ // COMP-OBS-COST: per-step and cumulative cost fields
345
+ input_tokens: event.input_tokens ?? 0,
346
+ output_tokens: event.output_tokens ?? 0,
347
+ cost_usd: event.cost_usd ?? 0,
348
+ cumulative_cost_usd: event.cumulative_cost_usd ?? 0,
349
+ ...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
350
+ ...(event.parallel ? { parallel: true } : {}),
351
+ _source: 'build',
352
+ };
353
+
354
+ case 'build_gate':
355
+ return {
356
+ type: 'system', subtype: 'build_gate',
357
+ stepId: event.stepId, gateType: event.gateType,
358
+ flowId: event.flowId,
359
+ ...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
360
+ _source: 'build',
361
+ };
362
+
363
+ case 'build_gate_resolved':
364
+ return {
365
+ type: 'system', subtype: 'build_gate_resolved',
366
+ stepId: event.stepId, outcome: event.outcome,
367
+ rationale: event.rationale,
368
+ flowId: event.flowId,
369
+ ...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
370
+ _source: 'build',
371
+ };
372
+
373
+ case 'build_error':
374
+ return {
375
+ type: 'error',
376
+ message: event.message, source: 'build',
377
+ _source: 'build',
378
+ };
379
+
380
+ case 'build_end':
381
+ return {
382
+ type: 'system', subtype: 'build_end',
383
+ status: event.status, featureCode: event.featureCode,
384
+ // COMP-OBS-COST: cumulative build totals
385
+ total_input_tokens: event.total_input_tokens ?? 0,
386
+ total_output_tokens: event.total_output_tokens ?? 0,
387
+ total_cost_usd: event.total_cost_usd ?? 0,
388
+ _source: 'build',
389
+ };
390
+
391
+ case 'step_usage':
392
+ return {
393
+ type: 'system', subtype: 'step_usage',
394
+ stepId: event.stepId,
395
+ input_tokens: event.input_tokens ?? 0,
396
+ output_tokens: event.output_tokens ?? 0,
397
+ cache_creation_input_tokens: event.cache_creation_input_tokens ?? 0,
398
+ cache_read_input_tokens: event.cache_read_input_tokens ?? 0,
399
+ cost_usd: event.cost_usd ?? 0,
400
+ model: event.model ?? null,
401
+ _source: 'build',
402
+ };
403
+
404
+ case 'idea_suggestion':
405
+ return {
406
+ type: 'system', subtype: 'idea_suggestion',
407
+ text: event.text, stepId: event.stepId,
408
+ _source: 'build',
409
+ };
410
+
411
+ // COMP-OBS-GATES: tier evaluation events
412
+ case 'gate_tier_result':
413
+ return {
414
+ type: 'system', subtype: 'gate_tier_result',
415
+ stepId: event.stepId,
416
+ tierId: event.tierId,
417
+ passed: event.passed,
418
+ details: event.details ?? null,
419
+ _source: 'build',
420
+ };
421
+
422
+ case 'gate_tier_failed':
423
+ return {
424
+ type: 'system', subtype: 'gate_tier_failed',
425
+ stepId: event.stepId,
426
+ tierId: event.tierId,
427
+ summary: event.summary ?? null,
428
+ flowId: event.flowId ?? null,
429
+ _source: 'build',
430
+ };
431
+
432
+ case 'gate_tier_summary':
433
+ return {
434
+ type: 'system', subtype: 'gate_tier_summary',
435
+ featureCode: event.featureCode,
436
+ passed: event.passed,
437
+ tierThatFailed: event.tierThatFailed ?? null,
438
+ tiersRun: event.tiersRun ?? [],
439
+ tiersSkipped: event.tiersSkipped ?? [],
440
+ costSaved: event.costSaved ?? 0,
441
+ _source: 'build',
442
+ };
443
+
444
+ // COMP-QA items 113-116: diff-aware QA scoping — affected routes from filesChanged analysis
445
+ case 'qa_scope':
446
+ return {
447
+ type: 'system', subtype: 'qa_scope',
448
+ affectedRoutes: event.affectedRoutes ?? [],
449
+ adjacentRoutes: event.adjacentRoutes ?? [],
450
+ unmappedFiles: event.unmappedFiles ?? [],
451
+ framework: event.framework ?? 'unknown',
452
+ docsOnly: event.docsOnly ?? false,
453
+ skipCoverage: event.skipCoverage ?? false,
454
+ reason: event.reason ?? null,
455
+ _source: 'build',
456
+ };
457
+
458
+ // COMP-HEALTH item 118: health score after build completion
459
+ case 'health_score':
460
+ return {
461
+ type: 'system', subtype: 'health_score',
462
+ score: event.score,
463
+ breakdown: event.breakdown ?? {},
464
+ missing: event.missing ?? [],
465
+ _source: 'build',
466
+ };
467
+
468
+ default:
469
+ return null; // unknown event type — skip
470
+ }
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // Crash detection
475
+ // ---------------------------------------------------------------------------
476
+
477
+ _resetCrashTimer() {
478
+ this._clearCrashTimer();
479
+ this.#crashTimer = setTimeout(() => {
480
+ this.#crashTimer = null;
481
+ if (this.#buildActive && this.#inStep) {
482
+ // Emit synthetic build_end(crashed)
483
+ this.#broadcast({
484
+ type: 'system', subtype: 'build_end',
485
+ status: 'crashed', _source: 'build',
486
+ });
487
+ // Suppress late events from dead build
488
+ this.#lastSeq = Infinity;
489
+ this.#buildActive = false;
490
+ this.#inStep = false;
491
+ }
492
+ }, this.#crashTimeoutMs);
493
+ if (this.#crashTimer.unref) this.#crashTimer.unref();
494
+ }
495
+
496
+ _clearCrashTimer() {
497
+ if (this.#crashTimer) {
498
+ clearTimeout(this.#crashTimer);
499
+ this.#crashTimer = null;
500
+ }
501
+ }
502
+ }
@@ -0,0 +1,46 @@
1
+ // compose/server/coalescing-buffer.js
2
+
3
+ export class CoalescingBuffer {
4
+ #modes = new Map();
5
+ #pending = new Map();
6
+ #timer = null;
7
+ #flushFn;
8
+
9
+ constructor(flushFn, { intervalMs = 16 } = {}) {
10
+ this.#flushFn = flushFn;
11
+ this.#timer = setInterval(() => this.#flush(), intervalMs);
12
+ }
13
+
14
+ register(key, mode) {
15
+ if (mode !== 'latest-wins' && mode !== 'append') {
16
+ throw new Error(`Unknown mode: ${mode}`);
17
+ }
18
+ this.#modes.set(key, mode);
19
+ }
20
+
21
+ put(key, value) {
22
+ const mode = this.#modes.get(key);
23
+ if (!mode) throw new Error(`Key not registered: ${key}`);
24
+ if (mode === 'latest-wins') {
25
+ this.#pending.set(key, value);
26
+ } else {
27
+ const arr = this.#pending.get(key) ?? [];
28
+ arr.push(value);
29
+ this.#pending.set(key, arr);
30
+ }
31
+ }
32
+
33
+ stop() {
34
+ if (this.#timer) {
35
+ clearInterval(this.#timer);
36
+ this.#timer = null;
37
+ }
38
+ }
39
+
40
+ #flush() {
41
+ if (this.#pending.size === 0) return;
42
+ const data = Object.fromEntries(this.#pending);
43
+ this.#pending.clear();
44
+ this.#flushFn(data);
45
+ }
46
+ }