@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,349 @@
1
+ /**
2
+ * Result Normalizer — bridges connector text streams to structured step results
3
+ * for the headless build runner.
4
+ *
5
+ * Converts flat Stratum output_fields to JSON Schema, runs a connector,
6
+ * accumulates text, and extracts structured JSON from the response.
7
+ */
8
+
9
+ import { injectSchema } from '../server/connectors/agent-connector.js';
10
+ import { CliProgress } from './cli-progress.js';
11
+ import { calculateCost } from './model-pricing.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Error classes
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export class ResultParseError extends Error {
18
+ /**
19
+ * @param {string} message
20
+ * @param {string} rawText The raw connector output that could not be parsed
21
+ */
22
+ constructor(message, rawText) {
23
+ super(message);
24
+ this.name = 'ResultParseError';
25
+ this.rawText = rawText;
26
+ }
27
+ }
28
+
29
+ export class AgentError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = 'AgentError';
33
+ }
34
+ }
35
+
36
+ export class UserInterruptError extends Error {
37
+ /** @param {string} stepId @param {'skip'|'retry'} action */
38
+ constructor(stepId, action) {
39
+ super(`User requested ${action} for step "${stepId}"`);
40
+ this.name = 'UserInterruptError';
41
+ this.stepId = stepId;
42
+ this.action = action;
43
+ }
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Schema conversion
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /** Map from Stratum flat type names to JSON Schema property descriptors. */
51
+ const TYPE_MAP = {
52
+ string: { type: 'string' },
53
+ boolean: { type: 'boolean' },
54
+ integer: { type: 'integer' },
55
+ number: { type: 'number' },
56
+ array: { type: 'array' },
57
+ object: { type: 'object' },
58
+ };
59
+
60
+ /**
61
+ * Convert Stratum's flat output_fields type map to a JSON Schema object.
62
+ *
63
+ * @param {Record<string, string>} outputFields e.g. { "clean": "boolean", "findings": "array" }
64
+ * @returns {object} A JSON Schema object with type, required, and properties.
65
+ */
66
+ export function outputFieldsToJsonSchema(outputFields) {
67
+ const properties = {};
68
+ const required = Object.keys(outputFields);
69
+
70
+ for (const [key, typeStr] of Object.entries(outputFields)) {
71
+ const lower = typeStr.toLowerCase();
72
+ properties[key] = TYPE_MAP[lower] ?? {}; // any/unknown → unconstrained
73
+ }
74
+
75
+ return {
76
+ type: 'object',
77
+ required,
78
+ properties,
79
+ };
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // JSON extraction helpers
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Try to extract a JSON object from text using multiple strategies.
88
+ *
89
+ * @param {string} text
90
+ * @returns {object|null} Parsed JSON or null if all strategies fail.
91
+ */
92
+ function extractJson(text) {
93
+ // Strategy A: full text is valid JSON
94
+ try {
95
+ return JSON.parse(text);
96
+ } catch { /* continue */ }
97
+
98
+ // Strategy B: fenced ```json ... ``` block
99
+ const fenceMatch = text.match(/```json\s*\n?([\s\S]*?)```/);
100
+ if (fenceMatch) {
101
+ try {
102
+ return JSON.parse(fenceMatch[1].trim());
103
+ } catch { /* continue */ }
104
+ }
105
+
106
+ // Strategy C: first balanced { ... } substring
107
+ const startIdx = text.indexOf('{');
108
+ if (startIdx !== -1) {
109
+ let depth = 0;
110
+ for (let i = startIdx; i < text.length; i++) {
111
+ if (text[i] === '{') depth++;
112
+ else if (text[i] === '}') depth--;
113
+ if (depth === 0) {
114
+ try {
115
+ return JSON.parse(text.slice(startIdx, i + 1));
116
+ } catch { /* continue */ }
117
+ break;
118
+ }
119
+ }
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Main entry point
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Run a connector and normalize its output to a structured result.
131
+ *
132
+ * @param {object} connector Object with a run(prompt, opts) async generator method.
133
+ * @param {string} prompt The prompt to send to the connector.
134
+ * @param {object} stepDispatch Step dispatch descriptor.
135
+ * @param {Record<string, string>} [stepDispatch.output_fields] Expected output fields.
136
+ * @param {object} [opts]
137
+ * @param {CliProgress} [opts.progress] CLI progress renderer.
138
+ * @returns {Promise<{ text: string, result: object|null }>}
139
+ */
140
+ export class AgentTimeoutError extends Error {
141
+ constructor(stepId, durationMs) {
142
+ super(`Agent timed out on step "${stepId}" after ${Math.round(durationMs / 1000)}s`);
143
+ this.name = 'AgentTimeoutError';
144
+ this.stepId = stepId;
145
+ this.durationMs = durationMs;
146
+ }
147
+ }
148
+
149
+ export async function runAndNormalize(connector, prompt, stepDispatch, opts = {}) {
150
+ const progress = opts.progress;
151
+ const streamWriter = opts.streamWriter;
152
+ const onToolUse = opts.onToolUse ?? null; // passive tap — Item 193
153
+ const maxDurationMs = opts.maxDurationMs ?? null; // null = no timeout
154
+ const outputFields = stepDispatch.output_fields;
155
+ const hasSchema = outputFields && typeof outputFields === 'object' && Object.keys(outputFields).length > 0;
156
+
157
+ let actualPrompt = prompt;
158
+ let schema = null;
159
+
160
+ if (hasSchema) {
161
+ schema = outputFieldsToJsonSchema(outputFields);
162
+ actualPrompt = injectSchema(prompt, schema);
163
+ }
164
+
165
+ const textParts = [];
166
+ const startTime = Date.now();
167
+
168
+ // Accumulate usage events across all connector events
169
+ const usageTotals = {
170
+ input_tokens: 0,
171
+ output_tokens: 0,
172
+ cache_creation_input_tokens: 0,
173
+ cache_read_input_tokens: 0,
174
+ cost_usd: 0,
175
+ model: null,
176
+ };
177
+
178
+ // Set up timeout timer if configured
179
+ let timeoutTimer = null;
180
+ let timedOut = false;
181
+ if (maxDurationMs) {
182
+ timeoutTimer = setTimeout(() => {
183
+ timedOut = true;
184
+ try { connector.interrupt(); } catch { /* best effort */ }
185
+ }, maxDurationMs);
186
+ }
187
+
188
+ // Wire up skip/retry interrupt from CLI key press
189
+ let userInterrupted = false;
190
+ const onInterrupt = () => {
191
+ userInterrupted = true;
192
+ try { connector.interrupt(); } catch { /* best effort */ }
193
+ };
194
+ if (progress?.on) progress.on('interrupt', onInterrupt);
195
+
196
+ try {
197
+ for await (const event of connector.run(actualPrompt, {})) {
198
+ // Check for timeout after each event
199
+ if (timedOut) {
200
+ const stepId = stepDispatch.step_id ?? 'unknown';
201
+ throw new AgentTimeoutError(stepId, Date.now() - startTime);
202
+ }
203
+ // Check for user interrupt (skip/retry)
204
+ if (userInterrupted) {
205
+ const stepId = stepDispatch.step_id ?? 'unknown';
206
+ throw new UserInterruptError(stepId, progress?.consumeAction() ?? 'skip');
207
+ }
208
+ if (progress) {
209
+ progress.debug(`event type=${event.type} keys=${Object.keys(event).join(',')}`);
210
+ } else if (process.env.COMPOSE_DEBUG) {
211
+ process.stderr.write(` [event] type=${event.type} keys=${Object.keys(event).join(',')}\n`);
212
+ }
213
+ if (event.type === 'error') {
214
+ // build_error is written by build.js catch blocks, not here — avoids duplicate events
215
+ throw new AgentError(event.message);
216
+ }
217
+ if (event.type === 'assistant' && event.content) {
218
+ textParts.push(event.content);
219
+ if (streamWriter) {
220
+ streamWriter.write({ type: 'assistant', content: event.content });
221
+ }
222
+ }
223
+ if (event.type === 'result' && event.content) {
224
+ // Result contains the final aggregated text — use it if we got nothing from blocks
225
+ if (textParts.length === 0) {
226
+ textParts.push(event.content);
227
+ }
228
+ }
229
+ // Forward tool_use to build stream (before progress logging)
230
+ if (streamWriter && event.type === 'tool_use' && event.tool) {
231
+ streamWriter.write({ type: 'tool_use', tool: event.tool, input: event.input });
232
+ }
233
+ // Passive tap — notify caller of each tool_use without altering event flow (Item 193)
234
+ if (onToolUse && event.type === 'tool_use' && event.tool) {
235
+ onToolUse({ tool: event.tool, input: event.input, timestamp: Date.now() });
236
+ }
237
+ // Progress logging — show tool calls so the user sees activity
238
+ if (event.type === 'tool_use' && event.tool) {
239
+ const detail = event.input?.command
240
+ ?? event.input?.pattern
241
+ ?? event.input?.query
242
+ ?? event.input?.file_path
243
+ ?? '';
244
+ if (progress) {
245
+ progress.toolUse(event.tool, detail);
246
+ } else {
247
+ const short = typeof detail === 'string' && detail.length > 60
248
+ ? detail.slice(0, 57) + '...'
249
+ : detail;
250
+ process.stderr.write(` ↳ ${event.tool}${short ? ': ' + short : ''}\n`);
251
+ }
252
+ }
253
+ // Forward tool_use_summary to build stream (COMP-OBS-STREAM)
254
+ if (streamWriter && event.type === 'tool_use_summary') {
255
+ streamWriter.write({ type: 'tool_use_summary', summary: event.summary, output: event.output });
256
+ }
257
+ if (event.type === 'tool_use_summary' && event.summary) {
258
+ if (progress) {
259
+ progress.toolSummary(event.summary);
260
+ } else {
261
+ const short = event.summary.length > 80
262
+ ? event.summary.slice(0, 77) + '...'
263
+ : event.summary;
264
+ process.stderr.write(` ↳ ${short}\n`);
265
+ }
266
+ }
267
+ // Forward tool_progress to build stream (COMP-OBS-STREAM)
268
+ if (streamWriter && event.type === 'tool_progress') {
269
+ streamWriter.write({ type: 'tool_progress', tool: event.tool, elapsed: event.elapsed });
270
+ }
271
+ if (event.type === 'tool_progress' && event.tool) {
272
+ if (progress) {
273
+ progress.toolProgress(event.tool, event.elapsed);
274
+ } else {
275
+ process.stderr.write(` ↳ ${event.tool} (${Math.round(event.elapsed)}s)\n`);
276
+ }
277
+ }
278
+ // Accumulate usage events (COMP-OBS-COST)
279
+ if (event.type === 'usage') {
280
+ usageTotals.input_tokens += event.input_tokens ?? 0;
281
+ usageTotals.output_tokens += event.output_tokens ?? 0;
282
+ usageTotals.cache_creation_input_tokens += event.cache_creation_input_tokens ?? 0;
283
+ usageTotals.cache_read_input_tokens += event.cache_read_input_tokens ?? 0;
284
+ if (event.model) usageTotals.model = event.model;
285
+ // If the connector already computed cost_usd, use it; otherwise calculate
286
+ const stepCost = event.cost_usd != null
287
+ ? event.cost_usd
288
+ : calculateCost(
289
+ event.model,
290
+ event.input_tokens ?? 0,
291
+ event.output_tokens ?? 0,
292
+ event.cache_creation_input_tokens ?? 0,
293
+ event.cache_read_input_tokens ?? 0,
294
+ );
295
+ usageTotals.cost_usd += stepCost;
296
+ // Forward per-step usage to build stream
297
+ if (streamWriter) {
298
+ streamWriter.write({
299
+ type: 'usage',
300
+ input_tokens: event.input_tokens ?? 0,
301
+ output_tokens: event.output_tokens ?? 0,
302
+ cache_creation_input_tokens: event.cache_creation_input_tokens ?? 0,
303
+ cache_read_input_tokens: event.cache_read_input_tokens ?? 0,
304
+ cost_usd: stepCost,
305
+ model: event.model ?? null,
306
+ });
307
+ }
308
+ }
309
+ }
310
+ } finally {
311
+ if (timeoutTimer) clearTimeout(timeoutTimer);
312
+ if (progress?.removeListener) progress.removeListener('interrupt', onInterrupt);
313
+ }
314
+
315
+ // If we broke out of the loop due to timeout, throw
316
+ if (timedOut) {
317
+ const stepId = stepDispatch.step_id ?? 'unknown';
318
+ throw new AgentTimeoutError(stepId, Date.now() - startTime);
319
+ }
320
+
321
+ const text = textParts.join('');
322
+
323
+ if (progress) {
324
+ progress.debug(`normalizer: textParts=${textParts.length}, text length=${text.length}`);
325
+ if (text.length > 0) progress.debug(`text preview: ${text.slice(0, 300)}`);
326
+ } else if (process.env.COMPOSE_DEBUG) {
327
+ process.stderr.write(` [normalizer] textParts count: ${textParts.length}, text length: ${text.length}\n`);
328
+ if (text.length > 0) process.stderr.write(` [normalizer] text preview: ${text.slice(0, 300)}\n`);
329
+ }
330
+
331
+ if (!hasSchema) {
332
+ return { text, result: null, usage: usageTotals };
333
+ }
334
+
335
+ const result = extractJson(text);
336
+ if (result === null) {
337
+ // Agent did its work but didn't return structured JSON.
338
+ // Log a warning and return a fallback — don't crash the pipeline.
339
+ if (progress) {
340
+ progress.warn('Could not extract JSON from agent output, using fallback');
341
+ } else {
342
+ process.stderr.write(' ⚠ Could not extract JSON from agent output, using fallback\n');
343
+ }
344
+ const summary = text.slice(0, 200).replace(/\n/g, ' ').trim();
345
+ return { text, result: { summary: summary || 'Step complete' }, usage: usageTotals };
346
+ }
347
+
348
+ return { text, result, usage: usageTotals };
349
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * review-lenses.js — Review lens definitions and triage logic for STRAT-REV.
3
+ *
4
+ * Each lens is a specialized reviewer that focuses on one concern.
5
+ * The triage function decides which lenses to activate based on the file list.
6
+ */
7
+
8
+ /** Baseline lenses — always run */
9
+ export const BASELINE_LENSES = ['diff-quality', 'contract-compliance', 'debug-discipline'];
10
+
11
+ /** Full lens definitions */
12
+ export const LENS_DEFINITIONS = {
13
+ 'diff-quality': {
14
+ id: 'diff-quality',
15
+ lens_name: 'diff-quality',
16
+ lens_focus: 'Code style, test coverage gaps, dead code, naming, duplication. Read the git diff only.',
17
+ confidence_gate: 6,
18
+ exclusions: 'Style-only nits without functional impact',
19
+ reasoning_template: {
20
+ require_citations: true,
21
+ sections: [
22
+ { id: 'premises', label: 'Premises', description: 'List each changed function/block from the diff. Cite file:line for each.' },
23
+ { id: 'trace', label: 'Quality Trace', description: 'For each premise, evaluate: naming clarity, duplication, error handling, dead code. Reference premises by ID.' },
24
+ { id: 'findings', label: 'Findings', description: 'List LensFinding items. Each must reference the premise it came from.' },
25
+ ],
26
+ },
27
+ },
28
+ 'contract-compliance': {
29
+ id: 'contract-compliance',
30
+ lens_name: 'contract-compliance',
31
+ lens_focus: 'Does the implementation match the blueprint? Missing acceptance criteria, wrong file paths, unimplemented items. Read blueprint + implementation.',
32
+ confidence_gate: 7,
33
+ exclusions: 'Items explicitly deferred in the plan',
34
+ reasoning_template: {
35
+ require_citations: true,
36
+ sections: [
37
+ { id: 'premises', label: 'Premises', description: 'List each blueprint requirement and the file:line that implements it. List any blueprint item with NO matching implementation.' },
38
+ { id: 'trace', label: 'Compliance Trace', description: 'For each premise pair (requirement <-> implementation), verify: correct path, correct signature, correct behavior. For unmatched items, confirm they are truly missing.' },
39
+ { id: 'findings', label: 'Findings', description: 'List compliance gaps as LensFinding items. Each must cite the blueprint requirement [P<n>] and the implementation (or lack thereof).' },
40
+ ],
41
+ },
42
+ },
43
+ 'security': {
44
+ id: 'security',
45
+ lens_name: 'security',
46
+ lens_focus: 'OWASP top 10, injection, secrets in code, insecure defaults. Focus on concrete, exploitable issues.',
47
+ confidence_gate: 8,
48
+ exclusions: 'DoS/rate-limiting, memory safety in memory-safe languages, theoretical risks without concrete exploit path',
49
+ reasoning_template: {
50
+ require_citations: true,
51
+ sections: [
52
+ { id: 'premises', label: 'Premises', description: 'List each security-sensitive operation in the diff: auth checks, SQL queries, user input handling, crypto, secrets. Cite file:line.' },
53
+ { id: 'trace', label: 'Threat Trace', description: 'For each premise, trace the data flow from source to sink. Identify: is input validated? Is output escaped? Are secrets hardcoded? Reference premises by ID.' },
54
+ { id: 'findings', label: 'Findings', description: 'List vulnerabilities as LensFinding items with OWASP category. Each must trace back to a specific premise and data flow.' },
55
+ ],
56
+ },
57
+ },
58
+ 'debug-discipline': {
59
+ id: 'debug-discipline',
60
+ lens_name: 'debug-discipline',
61
+ lens_focus: 'Fix-chain detection: are multiple iterations patching the same function? ' +
62
+ 'Trace evidence: was actual data inspected before the fix? ' +
63
+ 'Cross-layer completeness: does a migration/rename address ALL references? ' +
64
+ 'Type contracts: are there isinstance(x, dict) gates hiding type ambiguity?',
65
+ confidence_gate: 7,
66
+ exclusions: 'First-attempt fixes with trace evidence, pure refactors',
67
+ reasoning_template: {
68
+ require_citations: true,
69
+ sections: [
70
+ { id: 'premises', label: 'Premises', description: 'List each fix-iteration file change, trace evidence artifact, cross-layer reference, and type boundary check in the diff. Cite file:line for each.' },
71
+ { id: 'trace', label: 'Discipline Trace', description: 'For each premise, evaluate: was this a repeated fix to the same location? Was trace evidence produced before the fix? Were all cross-layer references addressed? Are type boundaries explicit?' },
72
+ { id: 'findings', label: 'Findings', description: 'List discipline violations as LensFinding items. Each must reference the specific premise and the anti-pattern it represents.' },
73
+ ],
74
+ },
75
+ },
76
+ 'framework': {
77
+ id: 'framework',
78
+ lens_name: 'framework',
79
+ lens_focus: 'Framework-specific anti-patterns, deprecated APIs, performance pitfalls for the detected framework.',
80
+ confidence_gate: 6,
81
+ exclusions: 'Opinions without measurable impact',
82
+ reasoning_template: {
83
+ require_citations: true,
84
+ sections: [
85
+ { id: 'premises', label: 'Premises', description: 'List each framework API call or pattern used in the diff. Cite file:line. Note the framework version from package.json/requirements.txt.' },
86
+ { id: 'trace', label: 'Pattern Trace', description: 'For each premise, check: is this API deprecated? Is there a preferred alternative? Does usage match framework conventions for this version?' },
87
+ { id: 'findings', label: 'Findings', description: 'List anti-patterns and deprecations as LensFinding items. Each must reference the specific API call [P<n>] and the framework docs justification.' },
88
+ ],
89
+ },
90
+ },
91
+ };
92
+
93
+ /** File patterns that trigger the security lens */
94
+ const SECURITY_PATTERNS = [
95
+ /auth/i, /login/i, /session/i, /token/i, /crypt/i, /password/i, /secret/i,
96
+ /\.sql$/i, /query/i, /sanitiz/i, /escape/i, /middleware/i,
97
+ /routes?\.(js|ts|jsx|tsx)$/i, /handler/i, /endpoint/i,
98
+ /api\//i, /server\//i,
99
+ ];
100
+
101
+ /** File patterns that trigger the framework lens */
102
+ const FRAMEWORK_PATTERNS = [
103
+ /\.(jsx|tsx)$/i, // React
104
+ /next\.config/i, /pages\//i, /app\//i, // Next.js
105
+ /express/i, /router/i, /middleware/i, // Express
106
+ /\.vue$/i, /nuxt/i, // Vue/Nuxt
107
+ /angular/i, // Angular
108
+ ];
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Diff-size classification (STRAT-REV-7)
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Classify a diff by number of changed files.
116
+ *
117
+ * @param {string[]} filesChanged - list of changed file paths
118
+ * @returns {'small'|'medium'|'large'}
119
+ */
120
+ export function classifyDiffSize(filesChanged) {
121
+ const count = Array.isArray(filesChanged) ? filesChanged.length : 0;
122
+ if (count <= 2) return 'small';
123
+ if (count <= 8) return 'medium';
124
+ return 'large';
125
+ }
126
+
127
+ /**
128
+ * Whether cross-model (Codex) review should run for this diff.
129
+ * Only triggers for large diffs (≥9 files).
130
+ *
131
+ * @param {string[]} filesChanged - list of changed file paths
132
+ * @returns {boolean}
133
+ */
134
+ export function shouldRunCrossModel(filesChanged) {
135
+ return classifyDiffSize(filesChanged) === 'large';
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Triage
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * Triage: decide which lenses to activate.
144
+ *
145
+ * @param {string[]} fileList - list of changed file paths
146
+ * @returns {Array<object>} LensTask[] for parallel_dispatch
147
+ */
148
+ export function triageLenses(fileList) {
149
+ const activeLensIds = [...BASELINE_LENSES];
150
+
151
+ const hasSecurityFiles = fileList.some(f =>
152
+ SECURITY_PATTERNS.some(p => p.test(f))
153
+ );
154
+ if (hasSecurityFiles) activeLensIds.push('security');
155
+
156
+ const hasFrameworkFiles = fileList.some(f =>
157
+ FRAMEWORK_PATTERNS.some(p => p.test(f))
158
+ );
159
+ if (hasFrameworkFiles) activeLensIds.push('framework');
160
+
161
+ return activeLensIds.map(id => {
162
+ const def = LENS_DEFINITIONS[id];
163
+ if (!def) throw new Error(`Unknown lens: ${id}`);
164
+ return { ...def };
165
+ });
166
+ }