@smartmemory/compose 0.2.39-beta → 0.2.40-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/lib/get-roadmap.js +121 -0
- package/package.json +1 -1
- package/server/compose-mcp-tools.js +7 -0
- package/server/compose-mcp.js +15 -0
- package/server/mcp-tool-policy.js +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get-roadmap.js — COMP-MCP-ROADMAP-READ.
|
|
3
|
+
*
|
|
4
|
+
* Read-only roadmap reader for the compose MCP `get_roadmap` tool. Renders the
|
|
5
|
+
* roadmap from canon (feature.json) WITHOUT writing any file, parses rows via the
|
|
6
|
+
* shared parseRoadmap, and reports a staleness flag vs the on-disk ROADMAP.md.
|
|
7
|
+
*
|
|
8
|
+
* Invariants:
|
|
9
|
+
* - Never mutates the filesystem (no writeRoadmap / renderRoadmap).
|
|
10
|
+
* - Narrative-owned workspaces read ROADMAP.md verbatim (no console.warn path),
|
|
11
|
+
* and are never reported stale (the file IS the canon).
|
|
12
|
+
* - Row parsing reuses parseRoadmap — no second hand-rolled regex.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { generateRoadmap } from './roadmap-gen.js';
|
|
18
|
+
import { isNarrativeOwned } from './roadmap-config.js';
|
|
19
|
+
import { parseRoadmap, parseStatusToken } from './roadmap-parser.js';
|
|
20
|
+
|
|
21
|
+
// Whole `**Last updated:** <date>` line — date-only and per-day, so it is
|
|
22
|
+
// stripped (not literal-matched) before drift comparison.
|
|
23
|
+
const LAST_UPDATED_RE = /^\*\*Last updated:\*\*.*$/m;
|
|
24
|
+
|
|
25
|
+
const ACTIVE_STATUSES = new Set(['IN_PROGRESS', 'PARTIAL']);
|
|
26
|
+
|
|
27
|
+
// Normalized status token -> summary bucket.
|
|
28
|
+
const BUCKET = {
|
|
29
|
+
COMPLETE: 'complete',
|
|
30
|
+
IN_PROGRESS: 'active',
|
|
31
|
+
PARTIAL: 'active',
|
|
32
|
+
PLANNED: 'planned',
|
|
33
|
+
BLOCKED: 'blocked',
|
|
34
|
+
PARKED: 'parked',
|
|
35
|
+
SUPERSEDED: 'superseded',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function emptySummary() {
|
|
39
|
+
return { complete: 0, active: 0, planned: 0, blocked: 0, parked: 0, superseded: 0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripVolatile(text) {
|
|
43
|
+
return text.replace(LAST_UPDATED_RE, '').trimEnd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} root - Project root.
|
|
48
|
+
* @param {object} [opts]
|
|
49
|
+
* @param {string} [opts.status] - Comma-list status filter applied to active/blocked rows.
|
|
50
|
+
* @param {string} [opts.phase] - Exact phaseId filter applied to active/blocked rows.
|
|
51
|
+
* @param {'summary'|'markdown'} [opts.format='summary'] - Omit or include raw markdown.
|
|
52
|
+
* @param {boolean} [opts.check_drift=true] - Compare render vs on-disk ROADMAP.md.
|
|
53
|
+
*/
|
|
54
|
+
export function getRoadmap(root, opts = {}) {
|
|
55
|
+
const { status, phase, format = 'summary', check_drift = true } = opts ?? {};
|
|
56
|
+
|
|
57
|
+
const roadmapPath = join(root, 'ROADMAP.md');
|
|
58
|
+
const onDisk = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : '';
|
|
59
|
+
|
|
60
|
+
// Branch on narrative ownership ourselves: read the file directly for narrative
|
|
61
|
+
// workspaces (avoids generateRoadmap's console.warn and is a true no-op read).
|
|
62
|
+
const narrative = isNarrativeOwned(root);
|
|
63
|
+
const markdown = narrative ? onDisk : generateRoadmap(root, {});
|
|
64
|
+
const source = narrative ? 'narrative' : 'rendered';
|
|
65
|
+
|
|
66
|
+
const rows = parseRoadmap(markdown);
|
|
67
|
+
|
|
68
|
+
// Summary counts over ALL rows (anonymous included).
|
|
69
|
+
const summary = emptySummary();
|
|
70
|
+
for (const r of rows) {
|
|
71
|
+
const bucket = BUCKET[parseStatusToken(r.status)];
|
|
72
|
+
if (bucket) summary[bucket]++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// active/blocked lists exclude anonymous rows. parseRoadmap rewrites
|
|
76
|
+
// codeless rows to `_anon_${position}` (roadmap-parser.js), so filter on that
|
|
77
|
+
// prefix — not the raw '—' glyph, which never reaches us.
|
|
78
|
+
const named = rows.filter((r) => r.code && !r.code.startsWith('_anon_'));
|
|
79
|
+
|
|
80
|
+
const matchFilter = (r) => {
|
|
81
|
+
if (status) {
|
|
82
|
+
const wanted = status.split(',').map((s) => s.trim().toUpperCase());
|
|
83
|
+
if (!wanted.includes(parseStatusToken(r.status))) return false;
|
|
84
|
+
}
|
|
85
|
+
if (phase && r.phaseId !== phase) return false;
|
|
86
|
+
return true;
|
|
87
|
+
};
|
|
88
|
+
const pick = (r) => ({
|
|
89
|
+
code: r.code,
|
|
90
|
+
description: r.description,
|
|
91
|
+
status: parseStatusToken(r.status),
|
|
92
|
+
phaseId: r.phaseId,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const active = named
|
|
96
|
+
.filter((r) => ACTIVE_STATUSES.has(parseStatusToken(r.status)))
|
|
97
|
+
.filter(matchFilter)
|
|
98
|
+
.map(pick);
|
|
99
|
+
const blocked = named
|
|
100
|
+
.filter((r) => parseStatusToken(r.status) === 'BLOCKED')
|
|
101
|
+
.filter(matchFilter)
|
|
102
|
+
.map(pick);
|
|
103
|
+
|
|
104
|
+
const out = { source, path: roadmapPath, summary, active, blocked };
|
|
105
|
+
|
|
106
|
+
if (check_drift) {
|
|
107
|
+
if (narrative) {
|
|
108
|
+
out.stale = false; // the content IS the file
|
|
109
|
+
} else {
|
|
110
|
+
const drifted = stripVolatile(markdown) !== stripVolatile(onDisk);
|
|
111
|
+
out.stale = drifted;
|
|
112
|
+
if (drifted) {
|
|
113
|
+
out.drift = 'ROADMAP.md differs from the feature.json render (run validate_project --fix to reconcile)';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (format === 'markdown') out.markdown = markdown;
|
|
119
|
+
|
|
120
|
+
return out;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.40-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",
|
|
@@ -11,6 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import { ArtifactManager, ARTIFACT_SCHEMAS } from './artifact-manager.js';
|
|
12
12
|
import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, loadProjectConfig } from './project-root.js';
|
|
13
13
|
import { resolveProfile, isToolAllowed } from './mcp-tool-policy.js';
|
|
14
|
+
import { getRoadmap } from '../lib/get-roadmap.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* COMP-MCP-ENFORCE Slice 3 — kill the `force` escape hatch at the MCP tool
|
|
@@ -152,6 +153,12 @@ export function loadSessions() {
|
|
|
152
153
|
// Tool implementations
|
|
153
154
|
// ---------------------------------------------------------------------------
|
|
154
155
|
|
|
156
|
+
// COMP-MCP-ROADMAP-READ — read-only roadmap reader. Thin wrapper over
|
|
157
|
+
// lib/get-roadmap.js; never mutates the filesystem.
|
|
158
|
+
export function toolGetRoadmap(args = {}) {
|
|
159
|
+
return getRoadmap(getTargetRoot(), args ?? {});
|
|
160
|
+
}
|
|
161
|
+
|
|
155
162
|
export function toolGetVisionItems({ phase, status, type, keyword, limit = 30 }) {
|
|
156
163
|
const { items } = loadVisionState();
|
|
157
164
|
|
package/server/compose-mcp.js
CHANGED
|
@@ -28,6 +28,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
28
28
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
29
29
|
import {
|
|
30
30
|
toolGetVisionItems,
|
|
31
|
+
toolGetRoadmap,
|
|
31
32
|
toolGetItemDetail,
|
|
32
33
|
toolGetPhasesSummary,
|
|
33
34
|
toolGetBlockedItems,
|
|
@@ -358,6 +359,19 @@ const TOOLS = [
|
|
|
358
359
|
},
|
|
359
360
|
},
|
|
360
361
|
},
|
|
362
|
+
{
|
|
363
|
+
name: 'get_roadmap',
|
|
364
|
+
description: 'Read the current roadmap rendered from canon (feature.json) WITHOUT writing. Returns a status summary plus active/blocked rows, and a staleness flag comparing the render against on-disk ROADMAP.md. Narrative-owned workspaces return the hand-authored file verbatim. Read-only — prefer this over reading ROADMAP.md directly.',
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
properties: {
|
|
368
|
+
status: { type: 'string', description: 'Filter active/blocked rows by status (comma-separated): PLANNED, IN_PROGRESS, PARTIAL, BLOCKED, COMPLETE, …' },
|
|
369
|
+
phase: { type: 'string', description: 'Filter active/blocked rows to a single phase (matched against phaseId)' },
|
|
370
|
+
format: { type: 'string', description: '"summary" (default — counts + lists, token-safe) or "markdown" (full rendered text)' },
|
|
371
|
+
check_drift: { type: 'boolean', description: 'Compare the render against on-disk ROADMAP.md and set stale/drift (default true)' },
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
361
375
|
{
|
|
362
376
|
name: 'validate_feature',
|
|
363
377
|
description: 'Cross-check a single feature against ROADMAP, vision-state, feature.json, folder contents, linked artifacts, and cross-references. Returns structured findings with severity (error/warning/info). FEATURE_NOT_FOUND emitted as a finding (not thrown) when the code matches strict regex but exists in no source.',
|
|
@@ -733,6 +747,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
733
747
|
case 'add_roadmap_entry': result = await toolAddRoadmapEntry(args); break;
|
|
734
748
|
case 'set_feature_status': result = await toolSetFeatureStatus(args); break;
|
|
735
749
|
case 'roadmap_diff': result = await toolRoadmapDiff(args); break;
|
|
750
|
+
case 'get_roadmap': result = toolGetRoadmap(args); break;
|
|
736
751
|
case 'link_artifact': result = await toolLinkArtifact(args); break;
|
|
737
752
|
case 'link_features': result = await toolLinkFeatures(args); break;
|
|
738
753
|
case 'get_feature_artifacts': result = await toolGetFeatureArtifacts(args); break;
|
|
@@ -34,7 +34,7 @@ const REVIEWER_ALLOW = [
|
|
|
34
34
|
'get_vision_items', 'get_item_detail', 'get_phase_summary', 'get_blocked_items',
|
|
35
35
|
'get_current_session', 'get_feature_lifecycle', 'get_feature_artifacts', 'get_feature_links',
|
|
36
36
|
'get_pending_gates', 'get_changelog_entries', 'get_journal_entries', 'get_completions',
|
|
37
|
-
'validate_feature', 'validate_project', 'roadmap_diff', 'roadmap_graph_check', 'assess_feature_artifacts',
|
|
37
|
+
'validate_feature', 'validate_project', 'roadmap_diff', 'get_roadmap', 'roadmap_graph_check', 'assess_feature_artifacts',
|
|
38
38
|
'set_workspace', 'get_workspace', 'bind_session',
|
|
39
39
|
];
|
|
40
40
|
|