@smartmemory/compose 0.2.49-beta → 0.2.51-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/compose/SKILL.md +24 -1
- package/.claude/skills/compose/references/hermes-tools.md +80 -0
- package/README.md +1 -1
- package/bin/compose.js +340 -70
- package/dist/assets/App-CT0vXPgd.js +724 -0
- package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
- package/dist/assets/{arc-B1TqAC_R.js → arc-BXht3LyE.js} +1 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
- package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
- package/dist/assets/{c4Diagram-AAUBKEIU-Dc6swoQo.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
- package/dist/assets/channel-Dd4XaiYv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CdqXgd68.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
- package/dist/assets/{chunk-55IACEB6-D7VauOmU.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
- package/dist/assets/{chunk-2J33WTMH-6HKDSnOw.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C1Z5m8DP.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
- package/dist/assets/{chunk-QZHKN3VN-DY9GU2uD.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-DmqZMEQe.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
- package/dist/assets/clone-CBsbNGAa.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B34g9xzV.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
- package/dist/assets/{erDiagram-TEJ5UH35-C05_Iqmb.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-lXpnb-Ch.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
- package/dist/assets/graph-DZe55uk8.js +331 -0
- package/dist/assets/graph-Tq_bs_r0.js +1 -0
- package/dist/assets/index-8UhRLbGq.js +123 -0
- package/dist/assets/index-CRqB9els.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CgdsWdgu.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
- package/dist/assets/{journeyDiagram-JHISSGLW-CyvHmE3z.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-Bqjdyv0T.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-qtUgN9BC.js +1 -0
- package/dist/assets/{linear-DecnRXf0.js → linear-D158F7OT.js} +1 -1
- package/dist/assets/min-hESxN-c0.js +1 -0
- package/dist/assets/{mindmap-definition-RKZ34NQL-DM1x9vNY.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
- package/dist/assets/mobile-1gVCT0OK.css +1 -0
- package/dist/assets/mobile-Chw8RWyH.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
- package/dist/assets/{requirementDiagram-4Y6WPE33-DD-Gg_y5.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-BPm2AEQj.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-DQCDWwlj.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
- package/dist/assets/{xychartDiagram-2RQKCTM6-Ba05SXXF.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
- package/dist/index.html +4 -4
- package/lib/build-all.js +2 -1
- package/lib/build.js +363 -38
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/codex-preflight.js +151 -0
- package/lib/completion-writer.js +59 -15
- package/lib/feature-json.js +14 -3
- package/lib/feature-validator.js +27 -10
- package/lib/feature-write-guard.js +5 -7
- package/lib/feature-writer.js +3 -3
- package/lib/followup-writer.js +2 -2
- package/lib/get-roadmap.js +2 -2
- package/lib/gsd.js +2 -1
- package/lib/hooks-status.js +135 -0
- package/lib/ideabox.js +3 -2
- package/lib/install-agent-defs.js +22 -0
- package/lib/journal-writer.js +3 -3
- package/lib/migrate-anon.js +277 -0
- package/lib/migrate-roadmap.js +2 -3
- package/lib/paths-core.js +47 -0
- package/lib/project-paths.js +46 -37
- package/lib/roadmap-gen.js +3 -4
- package/lib/roadmap-graph/collect.js +5 -5
- package/lib/roadmap-graph/index.js +2 -2
- package/lib/roadmap-parser.js +1 -1
- package/lib/state-migrations.js +3 -3
- package/lib/test-bootstrap.js +165 -2
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/vocabulary-inject.js +103 -0
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +433 -0
- package/pipelines/build.stratum.yaml +60 -3
- package/scripts/watch-server.sh +32 -0
- package/server/build-routes.js +89 -9
- package/server/build-stream-bridge.js +12 -0
- package/server/compose-mcp-tools.js +73 -1
- package/server/compose-mcp.js +27 -0
- package/server/design-routes.js +9 -4
- package/server/drift-axes.js +5 -3
- package/server/feature-scaffold-routes.js +82 -0
- package/server/feature-scan.js +8 -2
- package/server/file-watcher.js +13 -7
- package/server/health-routes.js +261 -0
- package/server/ideabox-routes.js +12 -11
- package/server/project-root.js +7 -4
- package/server/qa-scope-routes.js +96 -0
- package/server/session-routes.js +2 -2
- package/server/stratum-sync.js +85 -9
- package/server/validate-routes.js +70 -0
- package/server/vision-routes.js +46 -2
- package/server/vision-server.js +25 -3
- package/server/vision-utils.js +23 -4
- package/dist/assets/App-DOerhoDp.js +0 -706
- package/dist/assets/architectureDiagram-3BPJPVTR-C4hfxNOw.js +0 -36
- package/dist/assets/blockDiagram-GPEHLZMM-DRqkGncy.js +0 -132
- package/dist/assets/channel-KsY1x-7s.js +0 -1
- package/dist/assets/chunk-727SXJPM-CeKpXDif.js +0 -206
- package/dist/assets/chunk-AQP2D5EJ-y2XL-YHV.js +0 -231
- package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +0 -1
- package/dist/assets/dagre-BM42HDAG-klZ_GOcw.js +0 -4
- package/dist/assets/diagram-2AECGRRQ-D0FmM6Wf.js +0 -43
- package/dist/assets/diagram-5GNKFQAL-DYuhby6q.js +0 -10
- package/dist/assets/diagram-KO2AKTUF-CCtQZgal.js +0 -3
- package/dist/assets/diagram-LMA3HP47-Cd1OqEln.js +0 -24
- package/dist/assets/diagram-OG6HWLK6-CiJrhoxo.js +0 -24
- package/dist/assets/ganttDiagram-6RSMTGT7-EPbL_2Hr.js +0 -292
- package/dist/assets/gitGraphDiagram-PVQCEYII-COCoF_vW.js +0 -106
- package/dist/assets/graph-BBXaecIU.js +0 -331
- package/dist/assets/graph-CAnANduQ.js +0 -1
- package/dist/assets/index-BwLfbbOu.css +0 -1
- package/dist/assets/index-DlgxA8dX.js +0 -123
- package/dist/assets/infoDiagram-5YYISTIA-B1sJw6j5.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DGIYPm2g.js +0 -1
- package/dist/assets/mobile-Cag5dHlF.css +0 -1
- package/dist/assets/mobile-DwmxS_O4.js +0 -17
- package/dist/assets/pieDiagram-4H26LBE5-CRtmCvwV.js +0 -30
- package/dist/assets/quadrantDiagram-W4KKPZXB-CsBLWg27.js +0 -7
- package/dist/assets/sankeyDiagram-5OEKKPKP-BPdMN8UU.js +0 -40
- package/dist/assets/sequenceDiagram-3UESZ5HK-D4AENdze.js +0 -162
- package/dist/assets/stateDiagram-AJRCARHV-DDplpCf_.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +0 -1
- package/dist/assets/wardley-L42UT6IY-WERqOmJt.js +0 -173
- package/dist/assets/wardleyDiagram-YWT4CUSO-5teUSOiI.js +0 -78
package/server/build-routes.js
CHANGED
|
@@ -13,15 +13,36 @@
|
|
|
13
13
|
* tests; it lets a caller inject mock `runBuild` / `abortBuild` / `getDataDir`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
16
18
|
import { runBuild as defaultRunBuild, abortBuild as defaultAbortBuild } from '../lib/build.js';
|
|
19
|
+
import { runNew as defaultRunNew } from '../lib/new.js';
|
|
20
|
+
import { runBuildAll as defaultRunBuildAll } from '../lib/build-all.js';
|
|
21
|
+
import { runGsd as defaultRunGsd } from '../lib/gsd.js';
|
|
17
22
|
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
18
|
-
import { getDataDir as defaultGetDataDir } from './project-root.js';
|
|
23
|
+
import { getDataDir as defaultGetDataDir, getTargetRoot as defaultGetTargetRoot } from './project-root.js';
|
|
19
24
|
import { readBuildHistory } from '../lib/build-history.js';
|
|
20
25
|
|
|
26
|
+
// A launcher conflict (another build/GSD run already owns the feature) is a
|
|
27
|
+
// user-facing 409, not a 500. runBuild surfaces "already active"; runGsd
|
|
28
|
+
// refuses a concurrent run ("Refusing to start a concurrent run").
|
|
29
|
+
function launcherErrorStatus(err) {
|
|
30
|
+
const msg = err && err.message ? err.message : '';
|
|
31
|
+
return /already active|already running|concurrent run|refusing to start/i.test(msg) ? 409 : 500;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export function attachBuildRoutes(app, deps = {}) {
|
|
22
35
|
const runBuild = deps.runBuild || defaultRunBuild;
|
|
23
36
|
const abortBuild = deps.abortBuild || defaultAbortBuild;
|
|
37
|
+
const runNew = deps.runNew || defaultRunNew;
|
|
38
|
+
const runBuildAll = deps.runBuildAll || defaultRunBuildAll;
|
|
39
|
+
const runGsd = deps.runGsd || defaultRunGsd;
|
|
24
40
|
const getDataDir = deps.getDataDir || defaultGetDataDir;
|
|
41
|
+
// Bind build dispatch to the active project root (the same global target the
|
|
42
|
+
// vision store + getDataDir() use). Without this the runners default to
|
|
43
|
+
// process.cwd(), which switchProject() does not chdir — so after a project
|
|
44
|
+
// switch the cockpit and a launched build could target different workspaces.
|
|
45
|
+
const getTargetRoot = deps.getTargetRoot || defaultGetTargetRoot;
|
|
25
46
|
|
|
26
47
|
// COMP-COCKPIT-3: read-only past-builds history. No sensitive token —
|
|
27
48
|
// mirrors GET /api/build/state (read-only, no secrets in the records).
|
|
@@ -38,22 +59,81 @@ export function attachBuildRoutes(app, deps = {}) {
|
|
|
38
59
|
|
|
39
60
|
app.post('/api/build/start', requireSensitiveToken, async (req, res) => {
|
|
40
61
|
const body = req.body || {};
|
|
41
|
-
const { featureCode, mode = 'feature', description = '' } = body;
|
|
42
|
-
|
|
62
|
+
const { featureCode, mode = 'feature', description = '', resume = false } = body;
|
|
63
|
+
|
|
64
|
+
// featureCode is required for feature/bug/gsd; 'new' takes a free-text
|
|
65
|
+
// intent (no code yet) and 'all' sweeps every PLANNED feature.
|
|
66
|
+
if (!featureCode && mode !== 'all' && mode !== 'new') {
|
|
43
67
|
return res.status(400).json({ error: 'featureCode required' });
|
|
44
68
|
}
|
|
45
|
-
if (
|
|
46
|
-
return res.status(400).json({ error: "mode must be 'feature' or '
|
|
69
|
+
if (!['feature', 'bug', 'new', 'all', 'gsd'].includes(mode)) {
|
|
70
|
+
return res.status(400).json({ error: "mode must be 'feature', 'bug', 'new', 'all', or 'gsd'" });
|
|
47
71
|
}
|
|
72
|
+
|
|
73
|
+
// PARITY-2: launch the `new` lifecycle from a description (intent).
|
|
74
|
+
if (mode === 'new') {
|
|
75
|
+
const intent = (description || '').trim();
|
|
76
|
+
if (!intent) {
|
|
77
|
+
return res.status(400).json({ error: 'intent (description) required for mode=new' });
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const result = await runNew(intent, { cwd: getTargetRoot() });
|
|
81
|
+
return res.json(result ?? { ok: true });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return res.status(500).json({ error: err.message || String(err) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// PARITY-8: build-all sweep.
|
|
88
|
+
if (mode === 'all') {
|
|
89
|
+
try {
|
|
90
|
+
const result = await runBuildAll({ cwd: getTargetRoot() });
|
|
91
|
+
return res.json(result ?? { ok: true });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return res.status(launcherErrorStatus(err)).json({ error: err.message || String(err) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// PARITY-8: GSD autonomous run for a single feature.
|
|
98
|
+
if (mode === 'gsd') {
|
|
99
|
+
try {
|
|
100
|
+
const result = await runGsd(featureCode, { cwd: getTargetRoot() });
|
|
101
|
+
return res.json(result ?? { ok: true });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return res.status(launcherErrorStatus(err)).json({ error: err.message || String(err) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// PARITY-2: resume an interrupted bug-fix flow from active-build.json.
|
|
108
|
+
let resumeFlowId = null;
|
|
109
|
+
if (resume === true) {
|
|
110
|
+
if (mode !== 'bug') {
|
|
111
|
+
return res.status(400).json({ error: 'resume is only supported for mode=bug' });
|
|
112
|
+
}
|
|
113
|
+
let active = null;
|
|
114
|
+
try {
|
|
115
|
+
const activePath = path.join(getDataDir(), 'active-build.json');
|
|
116
|
+
active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
|
|
117
|
+
} catch {
|
|
118
|
+
active = null;
|
|
119
|
+
}
|
|
120
|
+
if (!active || active.featureCode !== featureCode || !active.flowId) {
|
|
121
|
+
return res.status(409).json({ error: 'no resumable build for featureCode' });
|
|
122
|
+
}
|
|
123
|
+
if (active.mode && active.mode !== 'bug') {
|
|
124
|
+
return res.status(409).json({ error: `active build is mode=${active.mode}, not resumable as bug` });
|
|
125
|
+
}
|
|
126
|
+
resumeFlowId = active.flowId;
|
|
127
|
+
}
|
|
128
|
+
|
|
48
129
|
try {
|
|
49
130
|
const opts = mode === 'bug'
|
|
50
|
-
? { mode, template: 'bug-fix', description }
|
|
51
|
-
: { mode, description };
|
|
131
|
+
? { mode, template: 'bug-fix', description, cwd: getTargetRoot(), ...(resumeFlowId ? { resumeFlowId } : {}) }
|
|
132
|
+
: { mode, description, cwd: getTargetRoot() };
|
|
52
133
|
const result = await runBuild(featureCode, opts);
|
|
53
134
|
res.json(result ?? { ok: true });
|
|
54
135
|
} catch (err) {
|
|
55
|
-
|
|
56
|
-
res.status(code).json({ error: err.message || String(err) });
|
|
136
|
+
res.status(launcherErrorStatus(err)).json({ error: err.message || String(err) });
|
|
57
137
|
}
|
|
58
138
|
});
|
|
59
139
|
|
|
@@ -471,6 +471,18 @@ export class BuildStreamBridge {
|
|
|
471
471
|
};
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
// COMP-TEST-BOOTSTRAP-4-1: advisory review of the tests the coverage step
|
|
475
|
+
// generated this build. Forwarded so the cockpit can surface the findings
|
|
476
|
+
// for human verification (the step never blocks ship).
|
|
477
|
+
case 'test_review':
|
|
478
|
+
return {
|
|
479
|
+
type: 'system', subtype: 'test_review',
|
|
480
|
+
clean: event.clean ?? true,
|
|
481
|
+
summary: event.summary ?? '',
|
|
482
|
+
findings: event.findings ?? [],
|
|
483
|
+
_source: 'build',
|
|
484
|
+
};
|
|
485
|
+
|
|
474
486
|
// COMP-HEALTH item 118: health score after build completion
|
|
475
487
|
case 'health_score':
|
|
476
488
|
return {
|
|
@@ -359,10 +359,82 @@ export async function toolProposeFollowup(args) {
|
|
|
359
359
|
// propose_followup also accepts a caller-supplied `status` and routes to
|
|
360
360
|
// addRoadmapEntry — gate lifecycle-owned terminal statuses the same way.
|
|
361
361
|
assertTerminalStatusAuthorized(args, 'propose_followup');
|
|
362
|
-
|
|
362
|
+
let proposeFollowup;
|
|
363
|
+
try {
|
|
364
|
+
({ proposeFollowup } = await import('../lib/followup-writer.js'));
|
|
365
|
+
} catch (err) {
|
|
366
|
+
// COMP-MCP-FOLLOWUP-1: a long-running stdio MCP server caches ESM module
|
|
367
|
+
// records. If lib/ changed under it (e.g. a later commit added an export
|
|
368
|
+
// this lazily-imported module now depends on), linking fails with a cryptic
|
|
369
|
+
// "does not provide an export named ..." even though the on-disk code is
|
|
370
|
+
// correct — the running server just needs to reload. Rethrow with an
|
|
371
|
+
// actionable hint instead of the raw ESM error.
|
|
372
|
+
if (/does not provide an export named|Cannot find module/i.test(err?.message || '')) {
|
|
373
|
+
const hint = new Error(
|
|
374
|
+
`propose_followup could not load lib/followup-writer.js: ${err.message}. ` +
|
|
375
|
+
`This usually means the compose MCP server is running stale code (module-cache skew after a ` +
|
|
376
|
+
`code change) — reconnect the MCP server (/mcp) to reload it. If it persists after reconnecting, ` +
|
|
377
|
+
`an export is genuinely missing on disk.`,
|
|
378
|
+
);
|
|
379
|
+
hint.cause = err;
|
|
380
|
+
throw hint;
|
|
381
|
+
}
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
363
384
|
return proposeFollowup(getTargetRoot(), args);
|
|
364
385
|
}
|
|
365
386
|
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Eager boot-time module preload — COMP-MCP-FOLLOWUP-1-1
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Modules reachable only via a lazy `import()` on a hot tool path, which
|
|
393
|
+
* therefore escape boot-time ESM linking. Eager-importing them at server start
|
|
394
|
+
* makes a genuine on-disk break (missing module OR missing export) fail fast at
|
|
395
|
+
* boot instead of surfacing on first tool invocation — complementing the
|
|
396
|
+
* COMP-MCP-FOLLOWUP-1 CI import-graph test.
|
|
397
|
+
*
|
|
398
|
+
* The default set is exactly one module: `lib/followup-writer.js`, lazily
|
|
399
|
+
* imported by `toolProposeFollowup`. Its own dynamic import
|
|
400
|
+
* (`server/artifact-manager.js`) is already statically imported at the top of
|
|
401
|
+
* this file, so it links at boot regardless — no need to list it here.
|
|
402
|
+
*/
|
|
403
|
+
export const EAGER_PRELOAD_MODULES = [
|
|
404
|
+
{ specifier: '../lib/followup-writer.js', expect: 'proposeFollowup' },
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Eagerly import each spec and assert its expected export is a function.
|
|
409
|
+
* Throws (naming the offending module) on a missing module or missing export.
|
|
410
|
+
*
|
|
411
|
+
* `await import()` does NOT throw when a module loads but no longer provides the
|
|
412
|
+
* expected named export — it resolves with that binding `undefined`. So the
|
|
413
|
+
* binding check, not the import alone, is what turns a missing export into a
|
|
414
|
+
* boot failure (the exact COMP-MCP-FOLLOWUP-1 failure class).
|
|
415
|
+
*
|
|
416
|
+
* `specs` is injectable for testing and for the `COMPOSE_PRELOAD_PROBE` boot
|
|
417
|
+
* seam; entries without an `expect` are import-only checks.
|
|
418
|
+
*/
|
|
419
|
+
export async function preloadEagerModules(specs = EAGER_PRELOAD_MODULES) {
|
|
420
|
+
const loaded = {};
|
|
421
|
+
for (const { specifier, expect } of specs) {
|
|
422
|
+
let mod;
|
|
423
|
+
try {
|
|
424
|
+
mod = await import(specifier);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
throw new Error(`eager preload failed for ${specifier}: ${err.message}`, { cause: err });
|
|
427
|
+
}
|
|
428
|
+
if (expect && typeof mod[expect] !== 'function') {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`eager preload failed for ${specifier}: expected export '${expect}' to be a function, got ${typeof mod[expect]}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
loaded[specifier] = mod;
|
|
434
|
+
}
|
|
435
|
+
return loaded;
|
|
436
|
+
}
|
|
437
|
+
|
|
366
438
|
// ---------------------------------------------------------------------------
|
|
367
439
|
// Checkpoints / resume — COMP-RESUME
|
|
368
440
|
// ---------------------------------------------------------------------------
|
package/server/compose-mcp.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
27
27
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
28
28
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
29
|
+
import { writeSync } from 'node:fs';
|
|
29
30
|
import {
|
|
30
31
|
toolGetVisionItems,
|
|
31
32
|
toolGetRoadmap,
|
|
@@ -71,6 +72,8 @@ import {
|
|
|
71
72
|
assertToolPhaseAllowed,
|
|
72
73
|
_getSessionProfile,
|
|
73
74
|
resolveBoundPhase,
|
|
75
|
+
preloadEagerModules,
|
|
76
|
+
EAGER_PRELOAD_MODULES,
|
|
74
77
|
} from './compose-mcp-tools.js';
|
|
75
78
|
import { isToolAllowed } from './mcp-tool-policy.js';
|
|
76
79
|
import { switchProject, getTargetRoot, loadProjectConfig } from './project-root.js';
|
|
@@ -817,6 +820,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
817
820
|
// Start
|
|
818
821
|
// ---------------------------------------------------------------------------
|
|
819
822
|
|
|
823
|
+
// COMP-MCP-FOLLOWUP-1-1: eager-preload lazily-imported hot-path modules so a
|
|
824
|
+
// genuine on-disk break (missing module/export) fails fast at boot instead of
|
|
825
|
+
// on first tool call. COMPOSE_PRELOAD_PROBE is an ops/test seam: comma-separated
|
|
826
|
+
// entries appended to the default set, each `specifier` or `specifier#export`
|
|
827
|
+
// (the `#export` form also exercises the export assertion). Absent → default.
|
|
828
|
+
const probeExtra = (process.env.COMPOSE_PRELOAD_PROBE || '')
|
|
829
|
+
.split(',')
|
|
830
|
+
.map((s) => s.trim())
|
|
831
|
+
.filter(Boolean)
|
|
832
|
+
.map((entry) => {
|
|
833
|
+
const hash = entry.indexOf('#');
|
|
834
|
+
return hash === -1
|
|
835
|
+
? { specifier: entry }
|
|
836
|
+
: { specifier: entry.slice(0, hash), expect: entry.slice(hash + 1) };
|
|
837
|
+
});
|
|
838
|
+
try {
|
|
839
|
+
await preloadEagerModules([...EAGER_PRELOAD_MODULES, ...probeExtra]);
|
|
840
|
+
} catch (err) {
|
|
841
|
+
// writeSync (not stderr.write) so the line is guaranteed flushed before
|
|
842
|
+
// process.exit — an async pipe write can otherwise be truncated/dropped.
|
|
843
|
+
writeSync(2, `[compose-mcp] boot aborted: ${err.message}\n`);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
|
|
820
847
|
const transport = new StdioServerTransport();
|
|
821
848
|
await server.connect(transport);
|
|
822
849
|
// Server runs until stdin closes — no explicit exit needed
|
package/server/design-routes.js
CHANGED
|
@@ -16,6 +16,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
16
16
|
import { parseDecisionBlocks } from '../src/components/vision/designSessionState.js';
|
|
17
17
|
import { StratumMcpClient } from '../lib/stratum-mcp-client.js';
|
|
18
18
|
import { KNOWN_VERSIONS } from '../lib/build-stream-schema.js';
|
|
19
|
+
import { getTargetRoot, resolveProjectPath } from './project-root.js';
|
|
20
|
+
import { relForDisplay } from '../lib/project-paths.js';
|
|
19
21
|
|
|
20
22
|
// Lazy singleton — design conversations share one stratum-mcp connection
|
|
21
23
|
// across the server process lifetime. Concurrent runs are correlation-id scoped.
|
|
@@ -444,16 +446,19 @@ Output ONLY the Markdown content, no code fences.`;
|
|
|
444
446
|
return;
|
|
445
447
|
}
|
|
446
448
|
|
|
447
|
-
// Determine the output path
|
|
448
|
-
|
|
449
|
+
// Determine the output path. Resolve the absolute write target first
|
|
450
|
+
// (features may be relocated outside the root — COMP-PATHS-EXTERNAL),
|
|
451
|
+
// then derive the display-relative string from it.
|
|
452
|
+
let absPath, designDocPath;
|
|
449
453
|
if (scope === 'feature' && featureCode) {
|
|
450
|
-
|
|
454
|
+
absPath = path.join(resolveProjectPath('features'), featureCode, 'design.md');
|
|
455
|
+
designDocPath = relForDisplay(getTargetRoot(), absPath);
|
|
451
456
|
} else {
|
|
452
457
|
designDocPath = path.join('docs', 'design.md');
|
|
458
|
+
absPath = path.join(projectRoot, designDocPath);
|
|
453
459
|
}
|
|
454
460
|
|
|
455
461
|
// Guard: never overwrite an existing doc with empty content
|
|
456
|
-
const absPath = path.join(projectRoot, designDocPath);
|
|
457
462
|
if (!docContent.trim()) {
|
|
458
463
|
console.error('[design] Generated doc is empty — refusing to overwrite');
|
|
459
464
|
res.status(500).json({ error: 'Doc generation produced empty content' });
|
package/server/drift-axes.js
CHANGED
|
@@ -19,6 +19,7 @@ import fs from 'node:fs';
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { execSync } from 'node:child_process';
|
|
21
21
|
import { diffContracts } from './contract-diff.js';
|
|
22
|
+
import { resolveFeaturesPath } from '../lib/project-paths.js';
|
|
22
23
|
|
|
23
24
|
// ── Threshold constants (Decision 2) ─────────────────────────────────────────
|
|
24
25
|
|
|
@@ -353,9 +354,10 @@ export function computeDriftAxes(item, projectRoot, now) {
|
|
|
353
354
|
|
|
354
355
|
const ts = now || new Date().toISOString();
|
|
355
356
|
|
|
356
|
-
// Resolve the
|
|
357
|
-
//
|
|
358
|
-
|
|
357
|
+
// Resolve the features/<FC> directory against projectRoot's own config
|
|
358
|
+
// (relocatable artifact paths, COMP-PATHS-EXTERNAL). Byte-identical to
|
|
359
|
+
// projectRoot/docs/features/<FC> for the in-root default.
|
|
360
|
+
const featurePath = path.join(resolveFeaturesPath(projectRoot), featureCode);
|
|
359
361
|
|
|
360
362
|
const pathAxis = computePathDrift(item, projectRoot, featurePath, ts);
|
|
361
363
|
const contractAxis = computeContractDrift(item, projectRoot, featurePath, ts);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feature-scaffold-routes.js — POST /api/features/scaffold.
|
|
3
|
+
*
|
|
4
|
+
* Cockpit equivalent of `compose feature <CODE>`: scaffolds docs/features/<CODE>/
|
|
5
|
+
* (feature.json + seed design.md) and the ROADMAP.md row. Reuses the typed writer
|
|
6
|
+
* addRoadmapEntry (lib/feature-writer.js) — never reimplements scaffolding and
|
|
7
|
+
* never echoes the regenerated roadmap back (the lib return is already compact).
|
|
8
|
+
*
|
|
9
|
+
* Sensitive (mutating). attachFeatureScaffoldRoutes(app, deps?) — deps is for tests.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
14
|
+
import { addRoadmapEntry as defaultAddRoadmapEntry } from '../lib/feature-writer.js';
|
|
15
|
+
import { isFeatureCode } from '../lib/feature-code.js';
|
|
16
|
+
import { getTargetRoot as defaultGetProjectRoot } from './project-root.js';
|
|
17
|
+
import { resolveFeaturesPathFromConfig, relForDisplay } from '../lib/project-paths.js';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PHASE = 'Backlog';
|
|
20
|
+
|
|
21
|
+
export function attachFeatureScaffoldRoutes(app, deps = {}) {
|
|
22
|
+
const addRoadmapEntry = deps.addRoadmapEntry || defaultAddRoadmapEntry;
|
|
23
|
+
const getProjectRoot = deps.getProjectRoot || defaultGetProjectRoot;
|
|
24
|
+
|
|
25
|
+
app.post('/api/features/scaffold', requireSensitiveToken, async (req, res) => {
|
|
26
|
+
const body = req.body || {};
|
|
27
|
+
const code = typeof body.code === 'string' ? body.code.trim().toUpperCase() : '';
|
|
28
|
+
const description = typeof body.description === 'string' ? body.description.trim() : '';
|
|
29
|
+
const phase = (typeof body.phase === 'string' && body.phase.trim())
|
|
30
|
+
|| (typeof body.group === 'string' && body.group.trim())
|
|
31
|
+
|| DEFAULT_PHASE;
|
|
32
|
+
|
|
33
|
+
if (!isFeatureCode(code)) {
|
|
34
|
+
return res.status(400).json({ error: 'Invalid feature code (e.g. COMP-FOO-1)' });
|
|
35
|
+
}
|
|
36
|
+
if (!description) {
|
|
37
|
+
return res.status(400).json({ error: 'description is required' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const projectRoot = getProjectRoot();
|
|
41
|
+
try {
|
|
42
|
+
// 1. feature.json + ROADMAP.md row (compact return — no roadmap echo).
|
|
43
|
+
const result = await addRoadmapEntry(projectRoot, { code, description, phase });
|
|
44
|
+
|
|
45
|
+
// 2. Seed design.md stub (addRoadmapEntry does not write it). Idempotent.
|
|
46
|
+
// Resolve the ABSOLUTE features dir (may be relocated) — mirror ideabox.
|
|
47
|
+
let featurePath = null;
|
|
48
|
+
try {
|
|
49
|
+
const composeJsonPath = path.join(projectRoot, '.compose', 'compose.json');
|
|
50
|
+
let cfg = {};
|
|
51
|
+
if (fs.existsSync(composeJsonPath)) {
|
|
52
|
+
try { cfg = JSON.parse(fs.readFileSync(composeJsonPath, 'utf-8')); } catch { /* default cfg */ }
|
|
53
|
+
}
|
|
54
|
+
const featuresBase = resolveFeaturesPathFromConfig(projectRoot, cfg);
|
|
55
|
+
const featureDir = path.join(featuresBase, code);
|
|
56
|
+
const designPath = path.join(featureDir, 'design.md');
|
|
57
|
+
if (!fs.existsSync(designPath)) {
|
|
58
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
59
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
60
|
+
fs.writeFileSync(designPath,
|
|
61
|
+
`# ${code}: ${description}\n\n**Status:** PLANNED\n**Created:** ${today}\n\n---\n\n## Intent\n\n${description}\n\n---\n\n## Notes\n\n_Seed design doc created by the New Feature dialog. \`compose build\` will expand it._\n`);
|
|
62
|
+
}
|
|
63
|
+
featurePath = relForDisplay(projectRoot, featureDir);
|
|
64
|
+
} catch { /* design seed is best-effort; feature.json + roadmap already written */ }
|
|
65
|
+
|
|
66
|
+
// COMPACT result — never the full roadmap (memory: MCP add_roadmap_entry echo
|
|
67
|
+
// blows the token cap; the lib return is small, keep it small here too).
|
|
68
|
+
return res.json({
|
|
69
|
+
ok: true,
|
|
70
|
+
code: result.code,
|
|
71
|
+
phase: result.phase,
|
|
72
|
+
position: result.position,
|
|
73
|
+
roadmap_path: result.roadmap_path,
|
|
74
|
+
featurePath,
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const msg = err && err.message ? err.message : String(err);
|
|
78
|
+
const code409 = /already exists/i.test(msg);
|
|
79
|
+
return res.status(code409 ? 409 : 400).json({ error: msg });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
package/server/feature-scan.js
CHANGED
|
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
|
|
18
18
|
import { getTargetRoot, resolveProjectPath } from './project-root.js';
|
|
19
|
+
import { relForDisplay } from '../lib/project-paths.js';
|
|
19
20
|
import { assertValidLinkShape } from '../lib/feature-write-guard.js';
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
@@ -608,6 +609,11 @@ export function seedFromRoadmapGraph(store) {
|
|
|
608
609
|
export function seedFeatures(features, store) {
|
|
609
610
|
const seeded = { features: 0, updated: 0, connections: 0 };
|
|
610
611
|
const featureItemMap = new Map(); // featureCode → itemId
|
|
612
|
+
const root = getTargetRoot();
|
|
613
|
+
const featuresBase = resolveProjectPath('features');
|
|
614
|
+
// Root-relative for the in-root default (relForDisplay guarantees byte-identity
|
|
615
|
+
// there); absolute when the features dir is relocated outside the workspace root.
|
|
616
|
+
const artifactPath = (feature, a) => relForDisplay(root, path.join(featuresBase, feature.name, a));
|
|
611
617
|
|
|
612
618
|
// First pass: create/update items
|
|
613
619
|
for (const feature of features) {
|
|
@@ -623,7 +629,7 @@ export function seedFeatures(features, store) {
|
|
|
623
629
|
status: feature.status || 'planned',
|
|
624
630
|
phase: feature.phase || 'planning',
|
|
625
631
|
confidence: feature.confidence,
|
|
626
|
-
files: feature.artifacts.map(a =>
|
|
632
|
+
files: feature.artifacts.map(a => artifactPath(feature, a)),
|
|
627
633
|
...(feature.group ? { group: feature.group } : {}),
|
|
628
634
|
});
|
|
629
635
|
try {
|
|
@@ -643,7 +649,7 @@ export function seedFeatures(features, store) {
|
|
|
643
649
|
if (feature.confidence > (featureItem.confidence || 0)) {
|
|
644
650
|
updates.confidence = feature.confidence;
|
|
645
651
|
}
|
|
646
|
-
const newFiles = feature.artifacts.map(a =>
|
|
652
|
+
const newFiles = feature.artifacts.map(a => artifactPath(feature, a));
|
|
647
653
|
if (JSON.stringify(newFiles) !== JSON.stringify(featureItem.files || [])) {
|
|
648
654
|
updates.files = newFiles;
|
|
649
655
|
}
|
package/server/file-watcher.js
CHANGED
|
@@ -10,6 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
|
|
12
12
|
import { getTargetRoot, loadProjectConfig, ensureDataDir } from './project-root.js';
|
|
13
|
+
import { resolveDocsPathFromConfig, resolveFeaturesPathFromConfig } from '../lib/project-paths.js';
|
|
13
14
|
|
|
14
15
|
const PROJECT_ROOT = getTargetRoot();
|
|
15
16
|
|
|
@@ -69,7 +70,8 @@ export class FileWatcherServer {
|
|
|
69
70
|
app.get('/api/files', (_req, res) => {
|
|
70
71
|
const config = loadProjectConfig();
|
|
71
72
|
const docsPrefix = config.paths?.docs || 'docs';
|
|
72
|
-
|
|
73
|
+
// COMP-PATHS-EXTERNAL: list the RESOLVED docs dir (may be relocated).
|
|
74
|
+
const docsDir = resolveDocsPathFromConfig(PROJECT_ROOT, config);
|
|
73
75
|
try {
|
|
74
76
|
const files = this.listMarkdownFiles(docsDir, docsPrefix);
|
|
75
77
|
res.json({ files });
|
|
@@ -164,7 +166,10 @@ export class FileWatcherServer {
|
|
|
164
166
|
if (!filename || !fileFilter(filename)) return;
|
|
165
167
|
|
|
166
168
|
const relativePath = path.join(prefix, filename);
|
|
167
|
-
|
|
169
|
+
// COMP-PATHS-EXTERNAL: derive the real path from the WATCHED dir, not
|
|
170
|
+
// by re-rooting under PROJECT_ROOT — the dir may be relocated outside
|
|
171
|
+
// the workspace. Byte-identical to the old form for an in-root dir.
|
|
172
|
+
const fullPath = path.join(dir, filename);
|
|
168
173
|
|
|
169
174
|
// Debounce: ignore events within 100ms of each other for the same file
|
|
170
175
|
const now = Date.now();
|
|
@@ -180,10 +185,11 @@ export class FileWatcherServer {
|
|
|
180
185
|
}
|
|
181
186
|
};
|
|
182
187
|
|
|
183
|
-
// Watch docs/ — broadcast fileChanged events
|
|
188
|
+
// Watch docs/ — broadcast fileChanged events. COMP-PATHS-EXTERNAL: watch
|
|
189
|
+
// the RESOLVED absolute dir (may be relocated outside PROJECT_ROOT).
|
|
184
190
|
const config = loadProjectConfig();
|
|
185
191
|
const docsPrefix = config.paths?.docs || 'docs';
|
|
186
|
-
watchDir(
|
|
192
|
+
watchDir(resolveDocsPathFromConfig(PROJECT_ROOT, config), docsPrefix, (relativePath, fullPath) => {
|
|
187
193
|
try {
|
|
188
194
|
if (!fs.existsSync(fullPath)) return;
|
|
189
195
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -195,9 +201,9 @@ export class FileWatcherServer {
|
|
|
195
201
|
|
|
196
202
|
// Watch features/ — notify for auto-reseed into vision store
|
|
197
203
|
const featuresPrefix = config.paths?.features || 'docs/features';
|
|
198
|
-
watchDir(
|
|
199
|
-
// Also broadcast as fileChanged (features are docs)
|
|
200
|
-
|
|
204
|
+
watchDir(resolveFeaturesPathFromConfig(PROJECT_ROOT, config), featuresPrefix, (relativePath, fullPath) => {
|
|
205
|
+
// Also broadcast as fileChanged (features are docs). fullPath comes from
|
|
206
|
+
// the watched dir (COMP-PATHS-EXTERNAL) — do not re-root under PROJECT_ROOT.
|
|
201
207
|
try {
|
|
202
208
|
if (fs.existsSync(fullPath)) {
|
|
203
209
|
const content = fs.readFileSync(fullPath, 'utf-8');
|