@smartmemory/compose 0.1.1-beta → 0.1.3-beta

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 (124) hide show
  1. package/.claude/skills/bug-fix/SKILL.md +143 -0
  2. package/.claude/skills/compose/SKILL.md +604 -0
  3. package/.compose-deps.json +89 -0
  4. package/README.md +47 -983
  5. package/bin/compose.js +473 -0
  6. package/contracts/comp-obs-contract.schema.json +362 -0
  7. package/contracts/cross-model-review-result.json +78 -0
  8. package/contracts/review-result.json +126 -0
  9. package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
  10. package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
  11. package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
  12. package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
  13. package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
  14. package/dist/assets/channel-LRG9kHqJ.js +1 -0
  15. package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
  16. package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
  17. package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
  20. package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
  21. package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
  22. package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
  25. package/dist/assets/clone-dRxgFrBv.js +1 -0
  26. package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
  27. package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
  28. package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
  29. package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
  30. package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
  31. package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
  32. package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
  33. package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
  34. package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
  36. package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
  37. package/dist/assets/index-DKBsEUJ-.css +1 -0
  38. package/dist/assets/index-DkRKLuNr.js +1144 -0
  39. package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
  41. package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
  42. package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
  43. package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
  44. package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
  45. package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
  46. package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
  47. package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
  49. package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
  52. package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
  54. package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
  55. package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
  56. package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
  57. package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
  58. package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/lib/budget-ledger.js +45 -0
  61. package/lib/bug-bisect.js +292 -0
  62. package/lib/bug-checkpoint.js +191 -0
  63. package/lib/bug-escalation.js +306 -0
  64. package/lib/bug-index-gen.js +136 -0
  65. package/lib/bug-ledger.js +126 -0
  66. package/lib/build-stream-schema.js +176 -0
  67. package/lib/build-stream-writer.js +3 -1
  68. package/lib/build.js +854 -284
  69. package/lib/connector-factory-shim.js +167 -0
  70. package/lib/constants.js +18 -0
  71. package/lib/debug-discipline.js +176 -27
  72. package/lib/deps.js +205 -0
  73. package/lib/health-score.js +4 -4
  74. package/lib/import.js +26 -13
  75. package/lib/inject-schema.js +21 -0
  76. package/lib/new.js +27 -53
  77. package/lib/result-normalizer.js +160 -144
  78. package/lib/review-lenses.js +5 -5
  79. package/lib/review-normalize.js +413 -0
  80. package/lib/review-prompt.js +163 -0
  81. package/lib/sections.js +325 -0
  82. package/lib/step-prompt.js +21 -1
  83. package/lib/step-validator.js +5 -3
  84. package/lib/stratum-mcp-client.js +172 -7
  85. package/package.json +14 -3
  86. package/pipelines/bug-fix.stratum.yaml +39 -1
  87. package/pipelines/build.stratum.yaml +28 -45
  88. package/pipelines/review-fix.stratum.yaml +1 -1
  89. package/presets/team-review.stratum.yaml +21 -14
  90. package/server/build-stream-bridge.js +28 -0
  91. package/server/cc-session-feature-resolver.js +111 -0
  92. package/server/cc-session-reader.js +327 -0
  93. package/server/cc-session-watcher.js +318 -0
  94. package/server/compose-mcp-tools.js +0 -125
  95. package/server/compose-mcp.js +2 -4
  96. package/server/contract-diff.js +192 -0
  97. package/server/decision-event-emit.js +175 -0
  98. package/server/decision-event-id.js +64 -0
  99. package/server/decision-events-snapshot.js +166 -0
  100. package/server/design-routes.js +92 -49
  101. package/server/drift-axes.js +365 -0
  102. package/server/drift-emit.js +121 -0
  103. package/server/gate-log-store.js +102 -0
  104. package/server/lifecycle-phase-history.js +44 -0
  105. package/server/open-loops-store.js +102 -0
  106. package/server/schema-validator.js +49 -0
  107. package/server/status-emit.js +27 -0
  108. package/server/status-snapshot.js +218 -0
  109. package/server/vision-routes.js +332 -4
  110. package/server/vision-server.js +104 -12
  111. package/server/vision-store.js +21 -0
  112. package/dist/assets/channel-DGElom1e.js +0 -1
  113. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
  114. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
  115. package/dist/assets/clone-DUJKJXd7.js +0 -1
  116. package/dist/assets/index-CUd6pFGF.css +0 -1
  117. package/dist/assets/index-DReRlzZI.js +0 -1144
  118. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
  119. package/server/connectors/agent-connector.js +0 -78
  120. package/server/connectors/claude-sdk-connector.js +0 -198
  121. package/server/connectors/codex-connector.js +0 -240
  122. package/server/connectors/connector-discovery.js +0 -18
  123. package/server/connectors/connector-runtime.js +0 -13
  124. package/server/connectors/opencode-connector.js +0 -200
@@ -0,0 +1,325 @@
1
+ /**
2
+ * sections.js — per-task plan section files for COMP-PLAN-SECTIONS.
3
+ *
4
+ * Owned by compose. Invoked from build.js after the Phase 6 plan_gate is
5
+ * approved (emitSections) and after the feature-final ship step records a
6
+ * commit (appendTrailers). External skills (buddy:*, superpowers:*) are
7
+ * untouched.
8
+ *
9
+ * See docs/features/COMP-PLAN-SECTIONS/{design,blueprint,plan}.md.
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { execFileSync } from 'node:child_process';
15
+
16
+ import { SECTIONS_DIR, getSectionsThreshold } from './constants.js';
17
+ import { parsePlanItems } from './plan-parser.js';
18
+
19
+ // ---------- Pure helpers ----------
20
+
21
+ /**
22
+ * slugify(text) — stable URL-safe slug. Lowercase, runs of non-alphanumerics
23
+ * collapse to single dashes, leading/trailing dashes trimmed, capped at 40.
24
+ */
25
+ export function slugify(text) {
26
+ if (!text || typeof text !== 'string') return '';
27
+ return text
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, '-')
30
+ .replace(/^-+|-+$/g, '')
31
+ .slice(0, 40)
32
+ .replace(/-+$/, ''); // trim trailing dash if cap landed on one
33
+ }
34
+
35
+ /**
36
+ * shouldEmitSections(taskCount) — true iff taskCount > threshold.
37
+ */
38
+ export function shouldEmitSections(taskCount) {
39
+ if (!Number.isFinite(taskCount)) return false;
40
+ return taskCount > getSectionsThreshold();
41
+ }
42
+
43
+ /**
44
+ * parseTaskBlocks(planMarkdown) — split a plan into task blocks.
45
+ *
46
+ * Recognised heading shapes:
47
+ * ## Task N <separator> <title>
48
+ * ### Task N <separator> <title>
49
+ * where <separator> is optional and may be one of: '—', '-', ':', '.'.
50
+ *
51
+ * Returns [{ id: 'TN', title, headingLevel, body }] in order. Empty array
52
+ * if no task headings match.
53
+ */
54
+ export function parseTaskBlocks(planMarkdown) {
55
+ if (!planMarkdown || typeof planMarkdown !== 'string') return [];
56
+
57
+ const lines = planMarkdown.split('\n');
58
+ const headingRe = /^(#{2,3})\s+Task\s+(\d+)\b\s*(?:[—\-:.]\s*)?(.*)$/i;
59
+
60
+ // Collect heading positions
61
+ const heads = [];
62
+ for (let i = 0; i < lines.length; i++) {
63
+ const m = lines[i].match(headingRe);
64
+ if (m) {
65
+ heads.push({
66
+ line: i,
67
+ headingLevel: m[1].length,
68
+ num: parseInt(m[2], 10),
69
+ title: (m[3] || '').trim(),
70
+ });
71
+ }
72
+ }
73
+ if (heads.length === 0) return [];
74
+
75
+ const blocks = [];
76
+ for (let i = 0; i < heads.length; i++) {
77
+ const start = heads[i].line + 1;
78
+ const end = i + 1 < heads.length ? heads[i + 1].line : lines.length;
79
+ const body = lines.slice(start, end).join('\n').replace(/^\n+|\n+$/g, '');
80
+ blocks.push({
81
+ id: `T${heads[i].num}`,
82
+ title: heads[i].title,
83
+ headingLevel: heads[i].headingLevel,
84
+ body,
85
+ });
86
+ }
87
+ return blocks;
88
+ }
89
+
90
+ /**
91
+ * extractSectionFiles(taskBody) — distinct file refs declared in checkboxes.
92
+ *
93
+ * Reuses plan-parser.parsePlanItems for `Files:` extraction. Returns deduped
94
+ * file paths in encounter order; empty array if none.
95
+ */
96
+ export function extractSectionFiles(taskBody) {
97
+ const items = parsePlanItems(taskBody || '');
98
+ const seen = new Set();
99
+ const out = [];
100
+ for (const it of items) {
101
+ if (it.file && !seen.has(it.file)) {
102
+ seen.add(it.file);
103
+ out.push(it.file);
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+
109
+ // ---------- Filesystem: emitSections ----------
110
+
111
+ function readPlan(featureDir) {
112
+ const planPath = path.join(featureDir, 'plan.md');
113
+ if (!fs.existsSync(planPath)) return null;
114
+ return fs.readFileSync(planPath, 'utf8');
115
+ }
116
+
117
+ function pad2(n) {
118
+ return String(n).padStart(2, '0');
119
+ }
120
+
121
+ /**
122
+ * parseDependsOn(taskBody) — string-match a "Depends on:" or "Depends:" line
123
+ * from the task body. Returns the trimmed value, or null if none found.
124
+ */
125
+ export function parseDependsOn(taskBody) {
126
+ if (!taskBody || typeof taskBody !== 'string') return null;
127
+ const m = taskBody.match(/^[ \t]*Depends(?:\s+on)?:\s*(.+?)\s*$/im);
128
+ if (!m) return null;
129
+ const v = m[1].trim();
130
+ return v || null;
131
+ }
132
+
133
+ function renderSectionFile({ block, idx, files }) {
134
+ const filesLine = files.length ? files.join(', ') : '—';
135
+ const title = block.title || `Task ${block.id.slice(1)}`;
136
+ const dependsRaw = parseDependsOn(block.body);
137
+ const dependsLine = dependsRaw || '—';
138
+ return [
139
+ `# Section ${pad2(idx)} — ${title}`,
140
+ ``,
141
+ `**Task ID:** ${block.id}`,
142
+ `**Depends on:** ${dependsLine}`,
143
+ `**Files:** ${filesLine}`,
144
+ ``,
145
+ `## Plan`,
146
+ ``,
147
+ block.body,
148
+ ``,
149
+ ].join('\n');
150
+ }
151
+
152
+ /**
153
+ * emitSections(featureDir) — idempotent emission of `<featureDir>/sections/`.
154
+ *
155
+ * Returns { created: string[], skipped: string[] } (relative paths within
156
+ * sections/). No-op if `plan.md` is missing or task count is sub-threshold.
157
+ * Existing section files are NEVER overwritten.
158
+ */
159
+ export function emitSections(featureDir) {
160
+ const result = { created: [], skipped: [] };
161
+ if (!featureDir) return result;
162
+
163
+ const plan = readPlan(featureDir);
164
+ if (plan == null) return result;
165
+
166
+ const blocks = parseTaskBlocks(plan);
167
+ if (!shouldEmitSections(blocks.length)) return result;
168
+
169
+ const sectionsDir = path.join(featureDir, SECTIONS_DIR);
170
+ fs.mkdirSync(sectionsDir, { recursive: true });
171
+
172
+ for (let i = 0; i < blocks.length; i++) {
173
+ const block = blocks[i];
174
+ const idx = i + 1;
175
+ const slug = slugify(block.title) || `task-${block.id.slice(1)}`;
176
+ const filename = `section-${pad2(idx)}-${slug}.md`;
177
+ const fullPath = path.join(sectionsDir, filename);
178
+ if (fs.existsSync(fullPath)) {
179
+ result.skipped.push(filename);
180
+ continue;
181
+ }
182
+ const files = extractSectionFiles(block.body);
183
+ const content = renderSectionFile({ block, idx, files });
184
+ fs.writeFileSync(fullPath, content);
185
+ result.created.push(filename);
186
+ }
187
+
188
+ return result;
189
+ }
190
+
191
+ // ---------- Filesystem: appendTrailers ----------
192
+
193
+ const TRAILER_HEADING_RE = /^## What Was Built(?:\s*\(iteration\s+(\d+)\))?\s*$/gm;
194
+
195
+ function readDeclaredFiles(sectionContent) {
196
+ const m = sectionContent.match(/^\*\*Files:\*\*\s+(.+)$/m);
197
+ if (!m) return [];
198
+ const raw = m[1].trim();
199
+ if (!raw || raw === '—' || raw === '-') return [];
200
+ return raw
201
+ .split(',')
202
+ .map(s => s.trim())
203
+ .filter(Boolean);
204
+ }
205
+
206
+ /**
207
+ * maxIteration(sectionContent) — scan all "What Was Built" headers and return
208
+ * the maximum iteration N (treating the unnumbered first one as N=1). Returns
209
+ * 0 if no trailer exists yet.
210
+ */
211
+ function maxIteration(sectionContent) {
212
+ const re = /^## What Was Built(?:\s*\(iteration\s+(\d+)\))?\s*$/gm;
213
+ let max = 0;
214
+ let m;
215
+ while ((m = re.exec(sectionContent)) !== null) {
216
+ const n = m[1] ? parseInt(m[1], 10) : 1;
217
+ if (Number.isFinite(n) && n > max) max = n;
218
+ }
219
+ return max;
220
+ }
221
+
222
+ function nextTrailerHeading(maxIter) {
223
+ if (maxIter <= 0) return '## What Was Built';
224
+ return `## What Was Built (iteration ${maxIter + 1})`;
225
+ }
226
+
227
+ /**
228
+ * computeFilteredDiffStat(cwd, commit, declaredFiles) — best-effort per-section
229
+ * `git diff --stat <commit>~1..<commit> -- <files>`. Returns the trimmed string,
230
+ * or a sentinel:
231
+ * - "(no declared files)" when declaredFiles is empty
232
+ * - "(diff stat unavailable)" on any failure
233
+ */
234
+ function computeFilteredDiffStat(cwd, commit, declaredFiles) {
235
+ if (!declaredFiles || declaredFiles.length === 0) return '(no declared files)';
236
+ if (!cwd || !commit) return '(diff stat unavailable)';
237
+ try {
238
+ // Use execFileSync with an argv array — no shell, no expansion, so file paths
239
+ // containing $(...), backticks, backslashes, spaces, etc. are passed verbatim
240
+ // to git as literal pathspecs.
241
+ const argv = ['diff', '--stat', `${commit}~1..${commit}`, '--', ...declaredFiles];
242
+ const out = execFileSync('git', argv, {
243
+ cwd,
244
+ encoding: 'utf-8',
245
+ timeout: 5000,
246
+ stdio: ['ignore', 'pipe', 'pipe'],
247
+ }).trim();
248
+ return out || '(diff stat unavailable)';
249
+ } catch {
250
+ return '(diff stat unavailable)';
251
+ }
252
+ }
253
+
254
+ function renderTrailer({ heading, commit, diffStat, owned, deviated }) {
255
+ const ownedStr = owned.length ? owned.join(', ') : 'None';
256
+ const deviatedStr = deviated.length ? deviated.join(', ') : 'None';
257
+ const commitStr = commit ? `\`${commit}\`` : '`unknown`';
258
+ const diffStr = diffStat && String(diffStat).trim() ? String(diffStat).trim() : '(diff stat unavailable)';
259
+ return [
260
+ ``,
261
+ heading,
262
+ ``,
263
+ `- **Commit:** ${commitStr}`,
264
+ `- **Diff:** ${diffStr}`,
265
+ `- **Files this section owns that changed:** ${ownedStr}`,
266
+ `- **Files this section declared but did not change:** ${deviatedStr}`,
267
+ ``,
268
+ ].join('\n');
269
+ }
270
+
271
+ /**
272
+ * appendTrailers({ featureDir, commit, filesChanged, cwd, diffStat? }) — append
273
+ * a "What Was Built" block to every section file under `<featureDir>/sections/`.
274
+ *
275
+ * - No-op if sections/ is absent.
276
+ * - Auto-numbers re-runs as `iteration max(N)+1` (append-only; never overwrites).
277
+ * - Per-section partition: declared ∩ changed → owned; declared \ changed →
278
+ * deviation. Changed-but-undeclared is deferred to COMP-PLAN-SECTIONS-REPORT.
279
+ * - Per-section diff stat: when `cwd` is provided, runs
280
+ * `git diff --stat <commit>~1..<commit> -- <declared-files>`
281
+ * filtered to that section's declared files. When `cwd` is omitted but a
282
+ * `diffStat` string is provided (legacy callers), uses that string verbatim.
283
+ *
284
+ * Returns { trailed: string[] } — section filenames updated.
285
+ */
286
+ export function appendTrailers({ featureDir, commit, filesChanged, cwd, diffStat } = {}) {
287
+ const result = { trailed: [] };
288
+ if (!featureDir) return result;
289
+ const sectionsDir = path.join(featureDir, SECTIONS_DIR);
290
+ if (!fs.existsSync(sectionsDir)) return result;
291
+
292
+ const changedSet = new Set(Array.isArray(filesChanged) ? filesChanged : []);
293
+ const files = fs
294
+ .readdirSync(sectionsDir)
295
+ .filter(f => /^section-\d+-.+\.md$/.test(f))
296
+ .sort();
297
+
298
+ for (const filename of files) {
299
+ const fullPath = path.join(sectionsDir, filename);
300
+ const existing = fs.readFileSync(fullPath, 'utf8');
301
+ const declared = readDeclaredFiles(existing);
302
+ const owned = declared.filter(f => changedSet.has(f));
303
+ const deviated = declared.filter(f => !changedSet.has(f));
304
+ const heading = nextTrailerHeading(maxIteration(existing));
305
+ // Prefer cwd-based per-section filtered diff. Fall back to legacy diffStat
306
+ // string only when cwd is not supplied. Wrapped in try/catch — failure
307
+ // substitutes "(diff stat unavailable)".
308
+ let perSectionDiff;
309
+ if (cwd) {
310
+ try {
311
+ perSectionDiff = computeFilteredDiffStat(cwd, commit, declared);
312
+ } catch {
313
+ perSectionDiff = '(diff stat unavailable)';
314
+ }
315
+ } else {
316
+ perSectionDiff = diffStat;
317
+ }
318
+ const trailer = renderTrailer({ heading, commit, diffStat: perSectionDiff, owned, deviated });
319
+ const sep = existing.endsWith('\n') ? '' : '\n';
320
+ fs.writeFileSync(fullPath, existing + sep + trailer);
321
+ result.trailed.push(filename);
322
+ }
323
+
324
+ return result;
325
+ }
@@ -5,6 +5,7 @@
5
5
  import { readdirSync, readFileSync, existsSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { checkStaleness } from './staleness.js';
8
+ import { readHypotheses, formatRejectedHypotheses } from './bug-ledger.js';
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // Ambient context cache — loaded once per build, keyed by contextDir path.
@@ -165,7 +166,26 @@ export function buildRetryPrompt(stepDispatch, violations, context, conflicts) {
165
166
  sections.push(buildConflictSection(conflicts));
166
167
  }
167
168
 
168
- return sections.join('\n\n');
169
+ let prompt = sections.join('\n\n');
170
+
171
+ // COMP-FIX-HARD T6: in bug-mode diagnose retries, prepend a digest of
172
+ // previously rejected hypotheses so the next attempt avoids dead ends.
173
+ // Guard: silent no-op if any precondition fails (regression-safe).
174
+ if (
175
+ context && context.mode === 'bug'
176
+ && stepDispatch && stepDispatch.step_id === 'diagnose'
177
+ && context.bug_code && context.cwd
178
+ ) {
179
+ try {
180
+ const entries = readHypotheses(context.cwd, context.bug_code);
181
+ const block = formatRejectedHypotheses(entries);
182
+ if (block) prompt = block + '\n' + prompt;
183
+ } catch {
184
+ // best-effort: never let ledger I/O break a retry
185
+ }
186
+ }
187
+
188
+ return prompt;
169
189
  }
170
190
 
171
191
  /**
@@ -15,10 +15,11 @@ import { runAndNormalize } from './result-normalizer.js';
15
15
  * @param {string} opts.artifact - File path to validate (relative to cwd)
16
16
  * @param {string[]} opts.criteria - List of things to check
17
17
  * @param {string} opts.stepId - Step ID (for logging)
18
- * @param {object} opts.connector - Agent connector to use
18
+ * @param {object} opts.stratum - StratumMcpClient for dispatching the validator agent
19
+ * @param {string} [opts.cwd] - Working directory
19
20
  * @returns {Promise<{ valid: boolean, issues: string[] }>}
20
21
  */
21
- export async function validateStep({ artifact, criteria, stepId, connector }) {
22
+ export async function validateStep({ artifact, criteria, stepId, stratum, cwd }) {
22
23
  const prompt =
23
24
  `You are a validator. Read the file "${artifact}" and check the following criteria:\n\n` +
24
25
  criteria.map((c, i) => `${i + 1}. ${c}`).join('\n') + '\n\n' +
@@ -31,13 +32,14 @@ export async function validateStep({ artifact, criteria, stepId, connector }) {
31
32
  // Minimal dispatch descriptor — only output_fields needed for JSON extraction
32
33
  const dispatch = {
33
34
  step_id: `validate_${stepId}`,
35
+ agent: 'claude',
34
36
  output_fields: {
35
37
  valid: 'boolean',
36
38
  issues: 'array',
37
39
  },
38
40
  };
39
41
 
40
- const { result } = await runAndNormalize(connector, prompt, dispatch);
42
+ const { result } = await runAndNormalize(null, prompt, dispatch, { stratum, cwd });
41
43
 
42
44
  if (!result || typeof result.valid !== 'boolean') {
43
45
  // Extraction failed — assume valid (optimistic fallback)
@@ -14,8 +14,10 @@
14
14
  */
15
15
 
16
16
  import { execFileSync } from 'node:child_process';
17
+ import { randomUUID } from 'node:crypto';
17
18
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18
19
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
20
+ import { validateBuildStreamEvent } from './build-stream-schema.js';
19
21
 
20
22
  export class StratumError extends Error {
21
23
  constructor(code, message, detail) {
@@ -30,6 +32,86 @@ export class StratumMcpClient {
30
32
  #client = null;
31
33
  #transport = null;
32
34
  #connected = false;
35
+ // STRAT-PAR-STREAM: subscribers keyed by `${flowId}::${stepId}` → Set<handler>
36
+ #eventSubs = new Map();
37
+
38
+ /**
39
+ * Subscribe to BuildStreamEvent push notifications scoped to a (flowId, stepId).
40
+ * Handler receives a parsed BuildStreamEvent envelope. Returns an unsubscribe fn.
41
+ *
42
+ * Events arrive only while a tool call (parallelStart/parallelPoll/...) for that
43
+ * scope is in flight — the underlying transport is MCP progress notifications,
44
+ * which are tied to an active request. Subscribe BEFORE the poll loop, unsubscribe
45
+ * after `outcome` is observed.
46
+ *
47
+ * @param {string} flowId
48
+ * @param {string} stepId
49
+ * @param {(event: object) => void} handler
50
+ * @returns {() => void} unsubscribe
51
+ */
52
+ onEvent(flowId, stepId, handler) {
53
+ const key = `${flowId}::${stepId}`;
54
+ let set = this.#eventSubs.get(key);
55
+ if (!set) {
56
+ set = new Set();
57
+ this.#eventSubs.set(key, set);
58
+ }
59
+ set.add(handler);
60
+ return () => {
61
+ const s = this.#eventSubs.get(key);
62
+ if (!s) return;
63
+ s.delete(handler);
64
+ if (s.size === 0) this.#eventSubs.delete(key);
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Internal: dispatch a parsed BuildStreamEvent to subscribers for its scope.
70
+ * Errors in handlers are logged and swallowed — push delivery is best-effort.
71
+ */
72
+ #dispatchEvent(event) {
73
+ if (!event || typeof event !== 'object') return;
74
+ const { flow_id, step_id } = event;
75
+ if (!flow_id || !step_id) return;
76
+ const set = this.#eventSubs.get(`${flow_id}::${step_id}`);
77
+ if (!set || set.size === 0) return;
78
+ for (const h of set) {
79
+ try { h(event); } catch (err) {
80
+ console.error('[stratum-mcp-client] onEvent handler threw:', err);
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Build the onprogress callback for a tool call. Parses the
87
+ * notification.message field as JSON and dispatches if it looks like a
88
+ * BuildStreamEvent (schema_version + kind). Other progress payloads are
89
+ * ignored.
90
+ */
91
+ #makeProgressHandler() {
92
+ return (progress) => {
93
+ const msg = progress?.message;
94
+ if (typeof msg !== 'string' || msg.length === 0) return;
95
+ let parsed;
96
+ try { parsed = JSON.parse(msg); } catch { return; }
97
+ // Discriminator: BuildStreamEvent has schema_version + kind + flow_id + step_id
98
+ if (!parsed || typeof parsed !== 'object') return;
99
+ if (typeof parsed.kind !== 'string') return;
100
+ // STRAT-PAR-STREAM-CONSUMER-VALIDATE: validate envelope before forwarding.
101
+ // On failure: warn and drop — never throw. Consumer must remain robust to
102
+ // producer drift. Non-BuildStreamEvent payloads are silently ignored above.
103
+ const validation = validateBuildStreamEvent(parsed);
104
+ if (!validation.valid) {
105
+ console.warn(
106
+ `[stratum-mcp-client] dropping invalid BuildStreamEvent` +
107
+ ` kind=${parsed.kind} schema_version=${parsed.schema_version}:`,
108
+ validation.error
109
+ );
110
+ return;
111
+ }
112
+ this.#dispatchEvent(parsed);
113
+ };
114
+ }
33
115
 
34
116
  /**
35
117
  * Spawn stratum-mcp and establish MCP connection.
@@ -85,9 +167,11 @@ export class StratumMcpClient {
85
167
  * Call an MCP tool and return the parsed JSON result.
86
168
  * @param {string} toolName
87
169
  * @param {object} args
170
+ * @param {object} [opts]
171
+ * @param {boolean} [opts.subscribeProgress] - if true, attach onprogress to demux BuildStreamEvents
88
172
  * @returns {Promise<any>}
89
173
  */
90
- async #callTool(toolName, args) {
174
+ async #callTool(toolName, args, opts = {}) {
91
175
  // Allow test-injected client to bypass real connection requirement.
92
176
  // Gated on NODE_ENV=test so production code cannot accidentally redirect calls.
93
177
  const client = (process.env.NODE_ENV === 'test' && this._testClient) || null;
@@ -95,10 +179,18 @@ export class StratumMcpClient {
95
179
  throw new Error('StratumMcpClient not connected. Call connect() first.');
96
180
  }
97
181
 
98
- const result = await (client ?? this.#client).callTool({
99
- name: toolName,
100
- arguments: args,
101
- });
182
+ const callArgs = { name: toolName, arguments: args };
183
+ const requestOpts = {};
184
+ if (opts.subscribeProgress) {
185
+ // Long-running tool calls may stream events for many minutes.
186
+ // Use generous timeouts and reset on each progress notification.
187
+ requestOpts.onprogress = this.#makeProgressHandler();
188
+ requestOpts.resetTimeoutOnProgress = true;
189
+ requestOpts.timeout = 600_000; // 10 min per heartbeat
190
+ requestOpts.maxTotalTimeout = 24 * 60 * 60 * 1000; // 24h hard cap
191
+ }
192
+
193
+ const result = await (client ?? this.#client).callTool(callArgs, undefined, requestOpts);
102
194
 
103
195
  // MCP tool results come back as content array; extract text content
104
196
  const textContent = result.content?.find(c => c.type === 'text');
@@ -325,7 +417,7 @@ export class StratumMcpClient {
325
417
  return this.#callTool('stratum_parallel_start', {
326
418
  flow_id: flowId,
327
419
  step_id: stepId,
328
- });
420
+ }, { subscribeProgress: true });
329
421
  }
330
422
 
331
423
  /**
@@ -340,7 +432,7 @@ export class StratumMcpClient {
340
432
  return this.#callTool('stratum_parallel_poll', {
341
433
  flow_id: flowId,
342
434
  step_id: stepId,
343
- });
435
+ }, { subscribeProgress: true });
344
436
  }
345
437
 
346
438
  /**
@@ -362,4 +454,77 @@ export class StratumMcpClient {
362
454
  merge_status: mergeStatus,
363
455
  });
364
456
  }
457
+
458
+ /**
459
+ * Run an agent (claude/codex) via the Python connector tier and stream
460
+ * BuildStreamEvent envelopes back via MCP progress notifications.
461
+ * Subscribe via `onEvent(correlationId, '_agent_run', handler)` BEFORE calling.
462
+ *
463
+ * @param {string} agentType 'claude' | 'codex'
464
+ * @param {string} prompt
465
+ * @param {object} [opts]
466
+ * @param {string} [opts.correlationId] Generated if absent.
467
+ * @param {string} [opts.modelID] Override model.
468
+ * @param {string[]} [opts.allowedTools]
469
+ * @param {string[]} [opts.disallowedTools]
470
+ * @param {object} [opts.thinking]
471
+ * @param {string} [opts.effort]
472
+ * @param {string} [opts.cwd]
473
+ * @returns {Promise<{text: string, correlation_id: string}>}
474
+ *
475
+ * NOTE: Schema injection is the caller's responsibility — `runAndNormalize`
476
+ * runs `injectSchema(prompt, schema)` client-side. Forwarding `schema` to
477
+ * the server would cause double-injection (producer also calls
478
+ * inject_schema on its end). Schema is intentionally not forwarded here.
479
+ */
480
+ async agentRun(agentType, prompt, opts = {}) {
481
+ const correlationId = opts.correlationId ?? randomUUID();
482
+ const result = await this.#callTool('stratum_agent_run', {
483
+ type: agentType,
484
+ prompt,
485
+ modelID: opts.modelID ?? undefined,
486
+ allowed_tools: opts.allowedTools ?? undefined,
487
+ disallowed_tools: opts.disallowedTools ?? undefined,
488
+ thinking: opts.thinking ?? undefined,
489
+ effort: opts.effort ?? undefined,
490
+ cwd: opts.cwd ?? undefined,
491
+ correlation_id: correlationId,
492
+ }, { subscribeProgress: true });
493
+ if (result && typeof result === 'object' && !result.correlation_id) {
494
+ result.correlation_id = correlationId;
495
+ }
496
+ return result;
497
+ }
498
+
499
+ /**
500
+ * One-shot agent text call without progress streaming. Used for short Q&A
501
+ * (gate askAgent path) where cockpit visibility is not needed.
502
+ *
503
+ * @param {string} agentType
504
+ * @param {string} prompt
505
+ * @param {object} [opts]
506
+ * @param {string} [opts.cwd]
507
+ * @returns {Promise<string>}
508
+ */
509
+ async runAgentText(agentType, prompt, opts = {}) {
510
+ const result = await this.#callTool('stratum_agent_run', {
511
+ type: agentType,
512
+ prompt,
513
+ cwd: opts.cwd ?? undefined,
514
+ }, { subscribeProgress: false });
515
+ return result?.text ?? '';
516
+ }
517
+
518
+ /**
519
+ * Cancel an in-flight stratum_agent_run by correlation id. Returns
520
+ * `{status: 'cancelled' | 'not_found', correlation_id}` from the producer.
521
+ *
522
+ * @param {string} correlationId
523
+ * @returns {Promise<object>}
524
+ */
525
+ async cancelAgentRun(correlationId) {
526
+ return this.#callTool('stratum_cancel_agent_run', {
527
+ correlation_id: correlationId,
528
+ });
529
+ }
365
530
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.1.1-beta",
3
+ "version": "0.1.3-beta",
4
4
  "description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
5
5
  "author": "SmartMemory",
6
6
  "license": "MIT",
@@ -15,8 +15,10 @@
15
15
  "dev:client": "vite",
16
16
  "build": "vite build",
17
17
  "preview": "vite preview",
18
- "test": "node --test test/*.test.js",
18
+ "test": "node --test test/*.test.js test/comp-obs-branch/*.test.js && npm run test:ui",
19
+ "test:ui": "vitest run",
19
20
  "test:integration": "node --test test/integration/*.test.js",
21
+ "test:wave-6": "node --test test/wave-6-integration.test.js test/wave-6-contract-compliance.test.js",
20
22
  "prepublishOnly": "npm run build"
21
23
  },
22
24
  "keywords": [
@@ -46,6 +48,8 @@
46
48
  "access": "public"
47
49
  },
48
50
  "files": [
51
+ ".compose-deps.json",
52
+ ".claude/skills/**",
49
53
  "bin/**",
50
54
  "server/**",
51
55
  "lib/**",
@@ -55,6 +59,7 @@
55
59
  "pipelines/**",
56
60
  "contracts/**",
57
61
  "scripts/**",
62
+ "skills/**",
58
63
  "README.md",
59
64
  "LICENSE"
60
65
  ],
@@ -75,6 +80,8 @@
75
80
  "@radix-ui/react-toggle-group": "^1.1.11",
76
81
  "@radix-ui/react-tooltip": "^1.2.8",
77
82
  "@tanstack/react-virtual": "^3.13.23",
83
+ "ajv": "^8.18.0",
84
+ "ajv-formats": "^3.0.1",
78
85
  "class-variance-authority": "^0.7.1",
79
86
  "clsx": "^2.1.1",
80
87
  "cors": "^2.8.5",
@@ -99,11 +106,15 @@
99
106
  "zustand": "^5.0.11"
100
107
  },
101
108
  "devDependencies": {
109
+ "@testing-library/dom": "^10.4.1",
110
+ "@testing-library/react": "^16.3.2",
102
111
  "@vitejs/plugin-react": "^4.3.4",
103
112
  "autoprefixer": "^10.4.20",
113
+ "jsdom": "^29.0.2",
104
114
  "postcss": "^8.5.3",
105
115
  "tailwindcss": "^3.4.17",
106
- "vite": "^6.1.0"
116
+ "vite": "^6.1.0",
117
+ "vitest": "^4.1.4"
107
118
  },
108
119
  "overrides": {
109
120
  "zod": "^4.0.0"