@smartmemory/compose 0.1.0 → 0.1.2-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.
- package/.claude/skills/bug-fix/SKILL.md +143 -0
- package/.claude/skills/compose/SKILL.md +604 -0
- package/.compose-deps.json +89 -0
- package/README.md +14 -3
- package/bin/compose.js +473 -0
- package/contracts/comp-obs-contract.schema.json +362 -0
- package/contracts/cross-model-review-result.json +78 -0
- package/contracts/review-result.json +126 -0
- package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
- package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
- package/dist/assets/channel-LRG9kHqJ.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
- package/dist/assets/clone-dRxgFrBv.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
- package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
- package/dist/assets/index-DKBsEUJ-.css +1 -0
- package/dist/assets/index-DkRKLuNr.js +1144 -0
- package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
- package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
- package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
- package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/budget-ledger.js +45 -0
- package/lib/bug-bisect.js +292 -0
- package/lib/bug-checkpoint.js +191 -0
- package/lib/bug-escalation.js +306 -0
- package/lib/bug-index-gen.js +136 -0
- package/lib/bug-ledger.js +126 -0
- package/lib/build-stream-schema.js +176 -0
- package/lib/build-stream-writer.js +3 -1
- package/lib/build.js +854 -284
- package/lib/connector-factory-shim.js +167 -0
- package/lib/constants.js +18 -0
- package/lib/debug-discipline.js +176 -27
- package/lib/deps.js +205 -0
- package/lib/health-score.js +4 -4
- package/lib/import.js +26 -13
- package/lib/inject-schema.js +21 -0
- package/lib/new.js +27 -53
- package/lib/result-normalizer.js +160 -144
- package/lib/review-lenses.js +5 -5
- package/lib/review-normalize.js +413 -0
- package/lib/review-prompt.js +163 -0
- package/lib/sections.js +325 -0
- package/lib/step-prompt.js +21 -1
- package/lib/step-validator.js +5 -3
- package/lib/stratum-mcp-client.js +172 -7
- package/package.json +14 -3
- package/pipelines/bug-fix.stratum.yaml +39 -1
- package/pipelines/build.stratum.yaml +28 -45
- package/pipelines/review-fix.stratum.yaml +1 -1
- package/presets/team-review.stratum.yaml +21 -14
- package/server/build-stream-bridge.js +28 -0
- package/server/cc-session-feature-resolver.js +111 -0
- package/server/cc-session-reader.js +327 -0
- package/server/cc-session-watcher.js +318 -0
- package/server/compose-mcp-tools.js +0 -125
- package/server/compose-mcp.js +2 -4
- package/server/contract-diff.js +192 -0
- package/server/decision-event-emit.js +175 -0
- package/server/decision-event-id.js +64 -0
- package/server/decision-events-snapshot.js +166 -0
- package/server/design-routes.js +92 -49
- package/server/drift-axes.js +365 -0
- package/server/drift-emit.js +121 -0
- package/server/gate-log-store.js +102 -0
- package/server/lifecycle-phase-history.js +44 -0
- package/server/open-loops-store.js +102 -0
- package/server/schema-validator.js +49 -0
- package/server/status-emit.js +27 -0
- package/server/status-snapshot.js +218 -0
- package/server/vision-routes.js +332 -4
- package/server/vision-server.js +104 -12
- package/server/vision-store.js +21 -0
- package/dist/assets/channel-DGElom1e.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
- package/dist/assets/clone-DUJKJXd7.js +0 -1
- package/dist/assets/index-CUd6pFGF.css +0 -1
- package/dist/assets/index-DReRlzZI.js +0 -1144
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
- package/server/connectors/agent-connector.js +0 -78
- package/server/connectors/claude-sdk-connector.js +0 -198
- package/server/connectors/codex-connector.js +0 -240
- package/server/connectors/connector-discovery.js +0 -18
- package/server/connectors/connector-runtime.js +0 -13
- package/server/connectors/opencode-connector.js +0 -200
package/lib/sections.js
ADDED
|
@@ -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
|
+
}
|
package/lib/step-prompt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/lib/step-validator.js
CHANGED
|
@@ -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.
|
|
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,
|
|
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(
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.2-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"
|