@smartmemory/compose 0.1.7-beta → 0.1.9-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/README.md +32 -5
- package/bin/compose.js +294 -34
- package/bin/git-hooks/post-commit.template +2 -1
- package/bin/git-hooks/pre-push.template +2 -1
- package/dist/assets/{_baseUniq-D-avYfn5.js → _baseUniq-3jW4HAOf.js} +1 -1
- package/dist/assets/{arc-BC4dfQ-X.js → arc-DzzDimyd.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BZmFXnGI.js → architectureDiagram-Q4EWVU46-CtAgwORz.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DlfWSuux.js → blockDiagram-DXYQGD6D-Bryby0c_.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-Y__uJrRx.js → c4Diagram-AHTNJAMY-C7N9RTJ8.js} +1 -1
- package/dist/assets/channel-DDkv7DUd.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BfMePfTp.js → chunk-4BX2VUAB-wijkFgZY.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-BdlMSdEA.js → chunk-4TB4RGXK-zdSZGRS2.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-vrQHZTdv.js → chunk-55IACEB6-6zqzTZQQ.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-B8wioVlW.js → chunk-EDXVE4YY-frd1Vwf-.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Cd6Hrux2.js → chunk-FMBD7UC4-CdkRK5Hx.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-CfrhdQXY.js → chunk-OYMX7WX6-C6bMB0cf.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B9JQerOU.js → chunk-QZHKN3VN-4vsxN3jq.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-DFN9X99H.js → chunk-YZCP3GAM-DbNARKip.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-J6ZTeCbW.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-J6ZTeCbW.js +1 -0
- package/dist/assets/clone-5MVZ89iV.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BAn0ap_E.js → cose-bilkent-S5V4N54A-BpXeV7Vj.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DyxnVq1g.js → dagre-KV5264BT-DQLu_W8r.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-XCrzqski.js → diagram-5BDNPKRD-skaOoe5A.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-MBCAXft_.js → diagram-G4DWMVQ6-DezlfFH4.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-DbtB2yS6.js → diagram-MMDJMWI5-BUu-v-wT.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Bb5NzX61.js → diagram-TYMM5635-CziQ6LPs.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CpIeCOh2.js → erDiagram-SMLLAGMA-BsAyOVTI.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CHyoKnhW.js → flowDiagram-DWJPFMVM-CbYWJOLq.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-DErKteO_.js → ganttDiagram-T4ZO3ILL-CAwgDkLl.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-KFVAtj2F.js → gitGraphDiagram-UUTBAWPF-DK4RlkjO.js} +1 -1
- package/dist/assets/{graph-CRnO_ifT.js → graph-orv1XHGx.js} +1 -1
- package/dist/assets/{index-DkRKLuNr.js → index-Ceywghsu.js} +143 -143
- package/dist/assets/{infoDiagram-42DDH7IO-BZFnuSp5.js → infoDiagram-42DDH7IO-DQyA75sK.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-4Xe2Szde.js → ishikawaDiagram-UXIWVN3A-C-F_5q4k.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CZRByfS-.js → journeyDiagram-VCZTEJTY-Bj8UIvK-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-B95sk6Fk.js → kanban-definition-6JOO6SKY-DZYr8Dp1.js} +1 -1
- package/dist/assets/{layout-BqNQzxWT.js → layout-CBaTKjpX.js} +1 -1
- package/dist/assets/{linear-CUh7qb64.js → linear-j1sI_SiN.js} +1 -1
- package/dist/assets/{min-wXgOS3ig.js → min-DtJISjld.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-DB6iaAbO.js → mindmap-definition-QFDTVHPH-Bulb64RS.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CHkZHrTW.js → pieDiagram-DEJITSTG-D11keQxr.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DoTEO8e3.js → quadrantDiagram-34T5L4WZ-BEcWQiEG.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-Dn8peXYp.js → requirementDiagram-MS252O5E-Cbp23uDf.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DRXs6Ipb.js → sankeyDiagram-XADWPNL6-Dae1hMc5.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-wBBYZ0aq.js → sequenceDiagram-FGHM5R23-C16abORi.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DPlBNGmf.js → stateDiagram-FHFEXIEX-CbEtfhbx.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CyY84hEA.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-CbbyTlHk.js → timeline-definition-GMOUNBTQ-BV7JTNMI.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-Bj4GaFfj.js → vennDiagram-DHZGUBPP-DBZiT48j.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-RtNzq8KU.js → wardley-RL74JXVD-Cc8uoiL3.js} +37 -37
- package/dist/assets/{wardleyDiagram-NUSXRM2D-CDfE3zSj.js → wardleyDiagram-NUSXRM2D-DEYcWGo5.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CZXHHYD5.js → xychartDiagram-5P7HB3ND-bFhLXv2b.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build.js +193 -19
- package/lib/completion-writer.js +7 -4
- package/lib/deps.js +17 -6
- package/lib/discover-workspaces.js +109 -0
- package/lib/feature-events.js +3 -0
- package/lib/feature-writer.js +34 -22
- package/lib/followup-writer.js +556 -0
- package/lib/mcp-enforcement.js +173 -0
- package/lib/migrate-roadmap.js +4 -1
- package/lib/project-paths.js +36 -0
- package/lib/resolve-workspace.js +166 -0
- package/lib/review-lenses.js +23 -8
- package/lib/review-normalize.js +42 -3
- package/lib/roadmap-drift.js +54 -0
- package/lib/roadmap-gen.js +297 -27
- package/lib/roadmap-preservers.js +353 -0
- package/lib/step-prompt.js +15 -0
- package/lib/triage.js +2 -1
- package/lib/version-check.js +110 -0
- package/package.json +1 -2
- package/server/compose-mcp-tools.js +44 -8
- package/server/compose-mcp.js +66 -1
- package/server/project-root.js +4 -0
- package/server/vision-routes.js +51 -2
- package/templates/ROADMAP.md +6 -0
- package/dist/assets/channel-LRG9kHqJ.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +0 -1
- package/dist/assets/clone-dRxgFrBv.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +0 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preservation helpers for typed-writer regen of ROADMAP.md.
|
|
3
|
+
*
|
|
4
|
+
* COMP-MCP-MIGRATION-2-1-1 (Option A — hand-rolled augmentation).
|
|
5
|
+
*
|
|
6
|
+
* Three pure functions with no I/O. Each scans an existing ROADMAP.md
|
|
7
|
+
* source string and returns a Map of curated content for the writer to
|
|
8
|
+
* splice back during regen, so typed-writer flips don't destroy:
|
|
9
|
+
*
|
|
10
|
+
* - phase-status overrides like `PARKED (Claude Code dependency)`
|
|
11
|
+
* - anonymous historical rows with `—` in the Feature column
|
|
12
|
+
* - non-feature sections wrapped in `<!-- preserved-section: <id> -->`
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PHASE_HEADING_RE = /^##\s+(.+?)\s+—\s+(.+?)\s*$/;
|
|
16
|
+
const FENCE_RE = /^```/;
|
|
17
|
+
const TABLE_HEADER_RE = /^\|.*\|$/;
|
|
18
|
+
const TABLE_DIVIDER_RE = /^\|[\s|:-]+\|$/;
|
|
19
|
+
const TABLE_ROW_RE = /^\|.+\|$/;
|
|
20
|
+
const FEATURE_CODE_RE = /^[A-Z][A-Z0-9-]*[A-Z0-9]$/;
|
|
21
|
+
|
|
22
|
+
const PRESERVED_OPEN_RE = /^<!--\s*preserved-section:\s*([a-z][a-z0-9-]*)\s*-->\s*$/;
|
|
23
|
+
const PRESERVED_CLOSE_RE = /^<!--\s*\/preserved-section\s*-->\s*$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Scan ROADMAP.md text and return a Map of phaseId → override text.
|
|
27
|
+
*
|
|
28
|
+
* Override is the substring after `— ` in any `## ...` heading line.
|
|
29
|
+
* Headings without an em-dash override are not included.
|
|
30
|
+
* Headings inside fenced code blocks are ignored.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} text
|
|
33
|
+
* @returns {Map<string, string>}
|
|
34
|
+
*/
|
|
35
|
+
export function readPhaseOverrides(text) {
|
|
36
|
+
const out = new Map();
|
|
37
|
+
let inFence = false;
|
|
38
|
+
for (const line of text.split('\n')) {
|
|
39
|
+
if (FENCE_RE.test(line)) {
|
|
40
|
+
inFence = !inFence;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (inFence) continue;
|
|
44
|
+
const m = line.match(PHASE_HEADING_RE);
|
|
45
|
+
if (m) out.set(m[1].trim(), m[2].trim());
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Scan ROADMAP.md text and return a Map of phaseId → AnonRow[].
|
|
52
|
+
*
|
|
53
|
+
* AnonRow shape: { rawLine, predecessorCode }
|
|
54
|
+
* - rawLine: the full table-row line as it appears in source.
|
|
55
|
+
* - predecessorCode: feature code of the prior typed row in the same phase
|
|
56
|
+
* table, or null if this anon row was at the table head (no typed predecessor).
|
|
57
|
+
*
|
|
58
|
+
* A row is "anonymous" if its Feature column (detected by header) is `—` or
|
|
59
|
+
* doesn't match FEATURE_CODE_RE. The current parser regex (looser, requires
|
|
60
|
+
* trailing -<digits>) is NOT used here; we use the strict regex for accurate
|
|
61
|
+
* classification (anon means truly no feature code, not the parser's regex bug).
|
|
62
|
+
* For the 3-col anonymous form (`# | Item | Status`), all rows are anon.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} text
|
|
65
|
+
* @returns {Map<string, Array<{rawLine: string, predecessorCode: string|null}>>}
|
|
66
|
+
*/
|
|
67
|
+
export function readAnonymousRows(text) {
|
|
68
|
+
const out = new Map();
|
|
69
|
+
let inFence = false;
|
|
70
|
+
let currentPhaseId = null;
|
|
71
|
+
let inTable = false;
|
|
72
|
+
let codeColIdx = -1; // -1 means anonymous-form (3-col) table
|
|
73
|
+
let lastTypedCode = null;
|
|
74
|
+
|
|
75
|
+
const lines = text.split('\n');
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
if (FENCE_RE.test(line)) {
|
|
79
|
+
inFence = !inFence;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (inFence) continue;
|
|
83
|
+
|
|
84
|
+
// Phase heading resets table state.
|
|
85
|
+
const phaseMatch = line.match(/^##\s+(.+?)(?:\s+—\s+.+)?\s*$/);
|
|
86
|
+
if (phaseMatch && line.startsWith('## ')) {
|
|
87
|
+
currentPhaseId = phaseMatch[1].trim();
|
|
88
|
+
inTable = false;
|
|
89
|
+
codeColIdx = -1;
|
|
90
|
+
lastTypedCode = null;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!currentPhaseId) continue;
|
|
95
|
+
if (!TABLE_ROW_RE.test(line.trim())) {
|
|
96
|
+
inTable = false;
|
|
97
|
+
codeColIdx = -1;
|
|
98
|
+
lastTypedCode = null;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
|
103
|
+
|
|
104
|
+
// Header row — detect column layout.
|
|
105
|
+
if (!inTable) {
|
|
106
|
+
const lower = cells.map(c => c.toLowerCase());
|
|
107
|
+
const featureIdx = lower.findIndex(c => c === 'feature');
|
|
108
|
+
if (featureIdx !== -1) {
|
|
109
|
+
codeColIdx = featureIdx;
|
|
110
|
+
} else if (lower[0] === 'id') {
|
|
111
|
+
codeColIdx = 0;
|
|
112
|
+
} else {
|
|
113
|
+
codeColIdx = -1; // anonymous form
|
|
114
|
+
}
|
|
115
|
+
inTable = true;
|
|
116
|
+
lastTypedCode = null;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Skip divider rows.
|
|
121
|
+
if (TABLE_DIVIDER_RE.test(line.trim())) continue;
|
|
122
|
+
|
|
123
|
+
// Data row.
|
|
124
|
+
let isAnon = false;
|
|
125
|
+
if (codeColIdx === -1) {
|
|
126
|
+
isAnon = true;
|
|
127
|
+
} else {
|
|
128
|
+
const codeCell = cells[codeColIdx] ?? '';
|
|
129
|
+
if (codeCell === '—' || codeCell === '' || !FEATURE_CODE_RE.test(codeCell)) {
|
|
130
|
+
isAnon = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isAnon) {
|
|
135
|
+
const arr = out.get(currentPhaseId) ?? [];
|
|
136
|
+
arr.push({ rawLine: line, predecessorCode: lastTypedCode });
|
|
137
|
+
out.set(currentPhaseId, arr);
|
|
138
|
+
} else {
|
|
139
|
+
lastTypedCode = cells[codeColIdx];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Scan ROADMAP.md text and return a Map of preserved-section id → rawSource.
|
|
147
|
+
*
|
|
148
|
+
* rawSource includes both open and close markers and everything between.
|
|
149
|
+
* Markers inside fenced code blocks are ignored. Unbalanced markers (open
|
|
150
|
+
* without matching close) are excluded.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} text
|
|
153
|
+
* @returns {Map<string, string>}
|
|
154
|
+
*/
|
|
155
|
+
export function readPreservedSections(text) {
|
|
156
|
+
const out = new Map();
|
|
157
|
+
let inFence = false;
|
|
158
|
+
let openId = null;
|
|
159
|
+
let openLineIdx = -1;
|
|
160
|
+
const lines = text.split('\n');
|
|
161
|
+
|
|
162
|
+
// Track byte offsets to slice rawSource.
|
|
163
|
+
// Compute cumulative offsets per line (start-of-line offsets).
|
|
164
|
+
const lineOffsets = new Array(lines.length + 1);
|
|
165
|
+
lineOffsets[0] = 0;
|
|
166
|
+
for (let i = 0; i < lines.length; i++) {
|
|
167
|
+
lineOffsets[i + 1] = lineOffsets[i] + lines[i].length + 1; // +1 for newline
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < lines.length; i++) {
|
|
171
|
+
const line = lines[i];
|
|
172
|
+
if (FENCE_RE.test(line)) {
|
|
173
|
+
inFence = !inFence;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (inFence) continue;
|
|
177
|
+
|
|
178
|
+
const openMatch = line.match(PRESERVED_OPEN_RE);
|
|
179
|
+
if (openMatch) {
|
|
180
|
+
openId = openMatch[1];
|
|
181
|
+
openLineIdx = i;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (openId !== null && PRESERVED_CLOSE_RE.test(line)) {
|
|
185
|
+
const startOffset = lineOffsets[openLineIdx];
|
|
186
|
+
const endOffset = lineOffsets[i + 1] - 1; // exclude trailing newline of close marker line
|
|
187
|
+
out.set(openId, text.slice(startOffset, endOffset));
|
|
188
|
+
openId = null;
|
|
189
|
+
openLineIdx = -1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Unbalanced open is silently dropped (could log, but tests expect empty/missing).
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* For each `## ` phase heading in source, capture the entire phase block —
|
|
198
|
+
* heading line + everything up to (but not including) the next `## ` heading,
|
|
199
|
+
* `<!-- preserved-section: ... -->` open marker, or EOF. Trailing `---`
|
|
200
|
+
* separator and surrounding blank lines before the next boundary are excluded.
|
|
201
|
+
*
|
|
202
|
+
* Used by the writer as a fallback for phases that have no feature.json
|
|
203
|
+
* features — emit the raw block verbatim so curated prose, exit text, and
|
|
204
|
+
* legacy table formatting all survive regen.
|
|
205
|
+
*
|
|
206
|
+
* Headings inside fenced code blocks are ignored.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} text
|
|
209
|
+
* @returns {Map<string, string>}
|
|
210
|
+
*/
|
|
211
|
+
export function readPhaseBlocks(text) {
|
|
212
|
+
const out = new Map();
|
|
213
|
+
let inFence = false;
|
|
214
|
+
let inPreserved = false;
|
|
215
|
+
let currentPhaseId = null;
|
|
216
|
+
let currentStartLineIdx = -1;
|
|
217
|
+
const lines = text.split('\n');
|
|
218
|
+
|
|
219
|
+
const finalize = (endLineIdx) => {
|
|
220
|
+
if (currentPhaseId === null || currentStartLineIdx < 0) return;
|
|
221
|
+
// Walk back over trailing blank lines and a single `---` separator.
|
|
222
|
+
let endIdx = endLineIdx;
|
|
223
|
+
while (endIdx > currentStartLineIdx && lines[endIdx - 1].trim() === '') endIdx--;
|
|
224
|
+
if (endIdx > currentStartLineIdx && lines[endIdx - 1].trim() === '---') endIdx--;
|
|
225
|
+
while (endIdx > currentStartLineIdx && lines[endIdx - 1].trim() === '') endIdx--;
|
|
226
|
+
const block = lines.slice(currentStartLineIdx, endIdx).join('\n');
|
|
227
|
+
out.set(currentPhaseId, block);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < lines.length; i++) {
|
|
231
|
+
const line = lines[i];
|
|
232
|
+
if (FENCE_RE.test(line)) {
|
|
233
|
+
inFence = !inFence;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (inFence) continue;
|
|
237
|
+
|
|
238
|
+
if (PRESERVED_OPEN_RE.test(line)) {
|
|
239
|
+
finalize(i);
|
|
240
|
+
currentPhaseId = null;
|
|
241
|
+
currentStartLineIdx = -1;
|
|
242
|
+
inPreserved = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (PRESERVED_CLOSE_RE.test(line)) {
|
|
246
|
+
inPreserved = false;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inPreserved) continue;
|
|
250
|
+
|
|
251
|
+
const phaseMatch = line.match(/^##\s+(.+?)(?:\s+—\s+.+)?\s*$/);
|
|
252
|
+
if (phaseMatch && line.startsWith('## ')) {
|
|
253
|
+
finalize(i);
|
|
254
|
+
currentPhaseId = phaseMatch[1].trim();
|
|
255
|
+
currentStartLineIdx = i;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
finalize(lines.length);
|
|
259
|
+
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Return the array of phaseIds in their order of appearance in source.
|
|
265
|
+
*
|
|
266
|
+
* Used by the writer to preserve original phase order when emitting phases
|
|
267
|
+
* that exist only in the source (no feature.json features). Headings inside
|
|
268
|
+
* fenced code blocks or inside open preserved-section markers are ignored.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} text
|
|
271
|
+
* @returns {string[]}
|
|
272
|
+
*/
|
|
273
|
+
export function readPhaseOrder(text) {
|
|
274
|
+
const out = [];
|
|
275
|
+
let inFence = false;
|
|
276
|
+
let inPreserved = false;
|
|
277
|
+
for (const line of text.split('\n')) {
|
|
278
|
+
if (FENCE_RE.test(line)) {
|
|
279
|
+
inFence = !inFence;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (inFence) continue;
|
|
283
|
+
|
|
284
|
+
if (PRESERVED_OPEN_RE.test(line)) {
|
|
285
|
+
inPreserved = true;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (PRESERVED_CLOSE_RE.test(line)) {
|
|
289
|
+
inPreserved = false;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (inPreserved) continue;
|
|
293
|
+
|
|
294
|
+
const phaseMatch = line.match(/^##\s+(.+?)(?:\s+—\s+.+)?\s*$/);
|
|
295
|
+
if (phaseMatch && line.startsWith('## ')) {
|
|
296
|
+
out.push(phaseMatch[1].trim());
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* For each preserved section, find the phaseId of the most recent `## ` heading
|
|
304
|
+
* before its open marker. Returns Map<id, phaseId|null>.
|
|
305
|
+
*
|
|
306
|
+
* Used by the writer to splice preserved sections back into the regenerated
|
|
307
|
+
* output at the correct sequential position relative to phases.
|
|
308
|
+
*
|
|
309
|
+
* - `null` anchor means the preserved section appeared at the top of the
|
|
310
|
+
* file before any phase heading (e.g. Roadmap Conventions).
|
|
311
|
+
* - Markers inside fenced code blocks are ignored (false-positive guard).
|
|
312
|
+
* - Open markers without a matching close are excluded.
|
|
313
|
+
*
|
|
314
|
+
* @param {string} text
|
|
315
|
+
* @returns {Map<string, string|null>}
|
|
316
|
+
*/
|
|
317
|
+
export function readPreservedSectionAnchors(text) {
|
|
318
|
+
const out = new Map();
|
|
319
|
+
let inFence = false;
|
|
320
|
+
let currentPhaseId = null;
|
|
321
|
+
let openId = null;
|
|
322
|
+
let openAnchor = null;
|
|
323
|
+
|
|
324
|
+
for (const line of text.split('\n')) {
|
|
325
|
+
if (FENCE_RE.test(line)) {
|
|
326
|
+
inFence = !inFence;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (inFence) continue;
|
|
330
|
+
|
|
331
|
+
// Phase headings inside an open preserved-section do NOT advance the anchor.
|
|
332
|
+
if (openId === null) {
|
|
333
|
+
const phaseMatch = line.match(/^##\s+(.+?)(?:\s+—\s+.+)?\s*$/);
|
|
334
|
+
if (phaseMatch && line.startsWith('## ') && !PRESERVED_OPEN_RE.test(line) && !PRESERVED_CLOSE_RE.test(line)) {
|
|
335
|
+
currentPhaseId = phaseMatch[1].trim();
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const openMatch = line.match(PRESERVED_OPEN_RE);
|
|
341
|
+
if (openMatch) {
|
|
342
|
+
openId = openMatch[1];
|
|
343
|
+
openAnchor = currentPhaseId;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (openId !== null && PRESERVED_CLOSE_RE.test(line)) {
|
|
347
|
+
out.set(openId, openAnchor);
|
|
348
|
+
openId = null;
|
|
349
|
+
openAnchor = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
package/lib/step-prompt.js
CHANGED
|
@@ -93,6 +93,21 @@ export function buildStepPrompt(stepDispatch, context) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// COMP-MCP-MIGRATION: typed-tool enforcement (prompt-only in v1).
|
|
97
|
+
// When `enforcement.mcpForFeatureMgmt` is true in .compose/compose.json,
|
|
98
|
+
// inject a hard instruction telling the agent to use typed MCP tools
|
|
99
|
+
// instead of free-text Edit/Write for ROADMAP/CHANGELOG/feature.json.
|
|
100
|
+
if (context.enforceMcpForFeatureMgmt) {
|
|
101
|
+
sections.push(
|
|
102
|
+
'## Enforcement\n' +
|
|
103
|
+
'Do NOT use Edit, Write, or any shell write that targets `ROADMAP.md`, ' +
|
|
104
|
+
'`CHANGELOG.md`, or any `feature.json` under `docs/features/`. Use the ' +
|
|
105
|
+
'typed MCP tools instead: `add_roadmap_entry`, `set_feature_status`, ' +
|
|
106
|
+
'`add_changelog_entry`, `record_completion`, `propose_followup`, ' +
|
|
107
|
+
'`link_features`, `link_artifact`, `write_journal_entry`.'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
const ctxLines = [
|
|
97
112
|
`Working directory: ${context.cwd}`,
|
|
98
113
|
`Feature: ${context.featureCode}`,
|
package/lib/triage.js
CHANGED
|
@@ -185,7 +185,8 @@ function deriveProfile(signals) {
|
|
|
185
185
|
*/
|
|
186
186
|
export async function runTriage(featureCode, opts = {}) {
|
|
187
187
|
const cwd = opts.cwd ?? process.cwd();
|
|
188
|
-
const
|
|
188
|
+
const featuresDir = opts.featuresDir ?? 'docs/features';
|
|
189
|
+
const featureDir = join(cwd, featuresDir, featureCode);
|
|
189
190
|
|
|
190
191
|
// Collect content from key files
|
|
191
192
|
const candidateFiles = ['plan.md', 'blueprint.md', 'design.md', 'prd.md', 'architecture.md'];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version drift detection — fetch latest @smartmemory/compose from npm
|
|
3
|
+
* and compare to the locally-installed version.
|
|
4
|
+
*
|
|
5
|
+
* Cached 24h at ~/.compose/version-cache.json. Never throws; returns null
|
|
6
|
+
* on any failure (network, parse, etc.) so it never breaks `compose doctor`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
10
|
+
import { join, dirname } from 'path'
|
|
11
|
+
import { homedir } from 'os'
|
|
12
|
+
|
|
13
|
+
const PACKAGE_NAME = '@smartmemory/compose'
|
|
14
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24h
|
|
15
|
+
const FETCH_TIMEOUT_MS = 3000
|
|
16
|
+
|
|
17
|
+
function cachePath() {
|
|
18
|
+
return join(homedir(), '.compose', 'version-cache.json')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readCache() {
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(cachePath(), 'utf-8')
|
|
24
|
+
const parsed = JSON.parse(raw)
|
|
25
|
+
if (typeof parsed?.fetchedAt !== 'number' || typeof parsed?.latest !== 'string') return null
|
|
26
|
+
if (Date.now() - parsed.fetchedAt > CACHE_TTL_MS) return null
|
|
27
|
+
return parsed
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeCache(latest) {
|
|
34
|
+
try {
|
|
35
|
+
const path = cachePath()
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
37
|
+
writeFileSync(path, JSON.stringify({ fetchedAt: Date.now(), latest }, null, 2))
|
|
38
|
+
} catch {
|
|
39
|
+
// best-effort cache; ignore failures
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchLatest() {
|
|
44
|
+
if (typeof fetch !== 'function') return null
|
|
45
|
+
const controller = new AbortController()
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
47
|
+
try {
|
|
48
|
+
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
|
|
49
|
+
const res = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
50
|
+
if (!res.ok) return null
|
|
51
|
+
const data = await res.json()
|
|
52
|
+
return typeof data?.version === 'string' ? data.version : null
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
} finally {
|
|
56
|
+
clearTimeout(timer)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compare semver-ish strings. Returns:
|
|
62
|
+
* -1 if a < b, 0 if equal, 1 if a > b, null if either unparseable.
|
|
63
|
+
* Treats prerelease tags conservatively: "0.1.7-beta" < "0.1.7".
|
|
64
|
+
*/
|
|
65
|
+
export function compareVersions(a, b) {
|
|
66
|
+
if (typeof a !== 'string' || typeof b !== 'string') return null
|
|
67
|
+
const parse = (s) => {
|
|
68
|
+
const [core, pre] = s.split('-')
|
|
69
|
+
const parts = core.split('.').map(n => Number.parseInt(n, 10))
|
|
70
|
+
if (parts.length !== 3 || parts.some(n => Number.isNaN(n))) return null
|
|
71
|
+
return { parts, pre: pre ?? null }
|
|
72
|
+
}
|
|
73
|
+
const pa = parse(a)
|
|
74
|
+
const pb = parse(b)
|
|
75
|
+
if (!pa || !pb) return null
|
|
76
|
+
for (let i = 0; i < 3; i++) {
|
|
77
|
+
if (pa.parts[i] < pb.parts[i]) return -1
|
|
78
|
+
if (pa.parts[i] > pb.parts[i]) return 1
|
|
79
|
+
}
|
|
80
|
+
// cores equal — prerelease < release
|
|
81
|
+
if (pa.pre && !pb.pre) return -1
|
|
82
|
+
if (!pa.pre && pb.pre) return 1
|
|
83
|
+
if (pa.pre === pb.pre) return 0
|
|
84
|
+
return pa.pre < pb.pre ? -1 : 1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns { current, latest, behind, source } or null on failure.
|
|
89
|
+
* behind: true if current < latest, false otherwise.
|
|
90
|
+
* source: 'cache' | 'network'
|
|
91
|
+
*/
|
|
92
|
+
export async function checkLatestVersion(currentVersion, { force = false } = {}) {
|
|
93
|
+
if (!currentVersion) return null
|
|
94
|
+
|
|
95
|
+
if (!force) {
|
|
96
|
+
const cached = readCache()
|
|
97
|
+
if (cached) {
|
|
98
|
+
const cmp = compareVersions(currentVersion, cached.latest)
|
|
99
|
+
if (cmp === null) return null
|
|
100
|
+
return { current: currentVersion, latest: cached.latest, behind: cmp < 0, source: 'cache' }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const latest = await fetchLatest()
|
|
105
|
+
if (!latest) return null
|
|
106
|
+
writeCache(latest)
|
|
107
|
+
const cmp = compareVersions(currentVersion, latest)
|
|
108
|
+
if (cmp === null) return null
|
|
109
|
+
return { current: currentVersion, latest, behind: cmp < 0, source: 'network' }
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9-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",
|
|
@@ -95,7 +95,6 @@
|
|
|
95
95
|
"date-fns": "^3.6.0",
|
|
96
96
|
"diff": "^8.0.4",
|
|
97
97
|
"express": "^4.21.0",
|
|
98
|
-
"ink": "^5.2.1",
|
|
99
98
|
"lucide-react": "^0.563.0",
|
|
100
99
|
"mermaid": "^11.13.0",
|
|
101
100
|
"react": "^19.2.5",
|
|
@@ -9,11 +9,12 @@ import fs from 'node:fs';
|
|
|
9
9
|
import http from 'node:http';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { ArtifactManager, ARTIFACT_SCHEMAS } from './artifact-manager.js';
|
|
12
|
-
import { getTargetRoot, getDataDir, resolveProjectPath } from './project-root.js';
|
|
12
|
+
import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId } from './project-root.js';
|
|
13
|
+
import { resolveWorkspace } from '../lib/resolve-workspace.js';
|
|
14
|
+
import { discoverWorkspaces } from '../lib/discover-workspaces.js';
|
|
13
15
|
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export const SESSIONS_FILE = path.join(getDataDir(), 'sessions.json');
|
|
16
|
+
export function getVisionFile() { return path.join(getDataDir(), 'vision-state.json'); }
|
|
17
|
+
export function getSessionsFile() { return path.join(getDataDir(), 'sessions.json'); }
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Data access
|
|
@@ -21,7 +22,7 @@ export const SESSIONS_FILE = path.join(getDataDir(), 'sessions.json');
|
|
|
21
22
|
|
|
22
23
|
export function loadVisionState() {
|
|
23
24
|
try {
|
|
24
|
-
const raw = fs.readFileSync(
|
|
25
|
+
const raw = fs.readFileSync(getVisionFile(), 'utf-8');
|
|
25
26
|
const state = JSON.parse(raw);
|
|
26
27
|
if (Array.isArray(state.gates)) {
|
|
27
28
|
const seen = new Map();
|
|
@@ -36,7 +37,7 @@ export function loadVisionState() {
|
|
|
36
37
|
|
|
37
38
|
export function loadSessions() {
|
|
38
39
|
try {
|
|
39
|
-
const raw = fs.readFileSync(
|
|
40
|
+
const raw = fs.readFileSync(getSessionsFile(), 'utf-8');
|
|
40
41
|
const sessions = JSON.parse(raw);
|
|
41
42
|
return Array.isArray(sessions) ? sessions : [];
|
|
42
43
|
} catch {
|
|
@@ -236,6 +237,15 @@ export async function toolGetFeatureLinks(args) {
|
|
|
236
237
|
return getFeatureLinks(getTargetRoot(), args);
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Follow-up filing — COMP-MCP-FOLLOWUP
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
export async function toolProposeFollowup(args) {
|
|
245
|
+
const { proposeFollowup } = await import('../lib/followup-writer.js');
|
|
246
|
+
return proposeFollowup(getTargetRoot(), args);
|
|
247
|
+
}
|
|
248
|
+
|
|
239
249
|
// ---------------------------------------------------------------------------
|
|
240
250
|
// Changelog writer — COMP-MCP-CHANGELOG-WRITER
|
|
241
251
|
// ---------------------------------------------------------------------------
|
|
@@ -397,8 +407,13 @@ export async function toolKillFeature({ id, reason }) {
|
|
|
397
407
|
return _postLifecycle(id, 'kill', { reason });
|
|
398
408
|
}
|
|
399
409
|
|
|
400
|
-
export async function toolCompleteFeature({ id }) {
|
|
401
|
-
|
|
410
|
+
export async function toolCompleteFeature({ id, commit_sha, tests_pass, files_changed, notes }) {
|
|
411
|
+
const body = {};
|
|
412
|
+
if (commit_sha !== undefined) body.commit_sha = commit_sha;
|
|
413
|
+
if (tests_pass !== undefined) body.tests_pass = tests_pass;
|
|
414
|
+
if (files_changed !== undefined) body.files_changed = files_changed;
|
|
415
|
+
if (notes !== undefined) body.notes = notes;
|
|
416
|
+
return _postLifecycle(id, 'complete', body);
|
|
402
417
|
}
|
|
403
418
|
|
|
404
419
|
export async function toolIterationStart({ id, loopType, maxIterations }) {
|
|
@@ -452,3 +467,24 @@ export function toolGetPendingGates({ itemId }) {
|
|
|
452
467
|
return { count: pending.length, gates: pending };
|
|
453
468
|
}
|
|
454
469
|
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Workspace binding (MCP session-scoped)
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
let _binding = null;
|
|
475
|
+
|
|
476
|
+
export function toolSetWorkspace({ workspaceId }) {
|
|
477
|
+
const resolved = resolveWorkspace({ workspaceId });
|
|
478
|
+
switchProject(resolved.root);
|
|
479
|
+
setCurrentWorkspaceId(resolved.id);
|
|
480
|
+
_binding = resolved;
|
|
481
|
+
return { id: resolved.id, root: resolved.root, source: 'mcp-binding' };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function toolGetWorkspace() {
|
|
485
|
+
const { candidates } = discoverWorkspaces(process.cwd());
|
|
486
|
+
return { current: _binding, candidates };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function _getBinding() { return _binding?.id ?? null; }
|
|
490
|
+
|