@smartmemory/compose 0.2.40-beta → 0.2.41-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/bin/compose.js +52 -4
- package/lib/get-roadmap.js +22 -3
- package/lib/state-migrations.js +217 -0
- package/package.json +1 -1
- package/server/compose-mcp.js +4 -3
package/bin/compose.js
CHANGED
|
@@ -329,14 +329,16 @@ async function runDoctor(flags = []) {
|
|
|
329
329
|
// compose init — project-local setup
|
|
330
330
|
// ---------------------------------------------------------------------------
|
|
331
331
|
|
|
332
|
-
async function runInit(flags) {
|
|
332
|
+
async function runInit(flags, cwdOverride) {
|
|
333
333
|
const noStratum = flags.includes('--no-stratum')
|
|
334
334
|
const noLifecycle = flags.includes('--no-lifecycle')
|
|
335
335
|
// init creates the workspace — never go through resolveCwdWithWorkspace (which
|
|
336
336
|
// requires one to exist). Strip --workspace if present to avoid leaving it in
|
|
337
337
|
// the shared args array for downstream subcommands.
|
|
338
338
|
getWorkspaceFlag(args)
|
|
339
|
-
|
|
339
|
+
// cwdOverride lets callers (e.g. runUpdate) target the resolved workspace root
|
|
340
|
+
// instead of process.cwd(), which differs when run from a subdirectory.
|
|
341
|
+
const cwd = cwdOverride || process.cwd()
|
|
340
342
|
|
|
341
343
|
// 1. Create .compose/ directory
|
|
342
344
|
const composeDir = join(cwd, '.compose')
|
|
@@ -373,8 +375,10 @@ async function runInit(flags) {
|
|
|
373
375
|
// 4. Write .compose/compose.json (merge with existing if present)
|
|
374
376
|
const configPath = join(composeDir, 'compose.json')
|
|
375
377
|
let existing = {}
|
|
378
|
+
let existingConfigCorrupt = false
|
|
376
379
|
if (existsSync(configPath)) {
|
|
377
|
-
try { existing = JSON.parse(readFileSync(configPath, 'utf-8')) }
|
|
380
|
+
try { existing = JSON.parse(readFileSync(configPath, 'utf-8')) }
|
|
381
|
+
catch { existingConfigCorrupt = true }
|
|
378
382
|
}
|
|
379
383
|
|
|
380
384
|
const agentsConfig = {}
|
|
@@ -388,6 +392,10 @@ async function runInit(flags) {
|
|
|
388
392
|
}
|
|
389
393
|
|
|
390
394
|
const config = {
|
|
395
|
+
// Preserve unknown top-level keys (tracker, roadmap, stateVersion, …) — the
|
|
396
|
+
// explicit keys below override the merged sub-objects. Without this spread,
|
|
397
|
+
// init/upgrade silently drops e.g. `roadmap.narrative` and `tracker` config.
|
|
398
|
+
...existing,
|
|
391
399
|
version: 2,
|
|
392
400
|
capabilities: {
|
|
393
401
|
...(existing.capabilities || {}),
|
|
@@ -418,6 +426,23 @@ async function runInit(flags) {
|
|
|
418
426
|
// 5. Create .compose/data/
|
|
419
427
|
mkdirSync(join(composeDir, 'data'), { recursive: true })
|
|
420
428
|
|
|
429
|
+
// 5a. Run pending feature.json state migrations (COMP-MIGRATE-ON-UPGRADE).
|
|
430
|
+
// Uses init's own resolved cwd; never aborts init on a migration fault. Skip
|
|
431
|
+
// when the prior compose.json was corrupt — init just normalized it to a
|
|
432
|
+
// default, so its tracker/paths can't be trusted to scope the migration.
|
|
433
|
+
if (existingConfigCorrupt) {
|
|
434
|
+
console.warn('state migration skipped: prior compose.json was unreadable (normalized to default; re-run `compose migrate-state` after verifying config)')
|
|
435
|
+
} else {
|
|
436
|
+
try {
|
|
437
|
+
const { runStateMigrations, summarizeMigrationReport } = await import('../lib/state-migrations.js')
|
|
438
|
+
const rep = runStateMigrations(cwd, {})
|
|
439
|
+
const line = summarizeMigrationReport(rep)
|
|
440
|
+
if (line && !rep.noop) console.log(line)
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.warn(`state migration skipped: ${err.message}`)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
421
446
|
// 5b. Scaffold docs/context/ with ambient context templates
|
|
422
447
|
const contextDir = join(cwd, config.paths.context)
|
|
423
448
|
mkdirSync(contextDir, { recursive: true })
|
|
@@ -669,7 +694,11 @@ async function runUpdate(flags) {
|
|
|
669
694
|
if (existsSync(join(cwd, '.compose', 'compose.json'))) {
|
|
670
695
|
console.log('')
|
|
671
696
|
console.log(`Refreshing project at ${cwd}...`)
|
|
672
|
-
|
|
697
|
+
// Thread the RESOLVED workspace root into runInit (it otherwise uses
|
|
698
|
+
// process.cwd(), which differs when upgrading from a subdirectory). This
|
|
699
|
+
// makes all of init's refresh — including its state-migration step — target
|
|
700
|
+
// the right workspace (COMP-MIGRATE-ON-UPGRADE finding 6).
|
|
701
|
+
await runInit([], cwd)
|
|
673
702
|
}
|
|
674
703
|
|
|
675
704
|
console.log('')
|
|
@@ -704,6 +733,25 @@ if (cmd === 'update' || cmd === 'upgrade') {
|
|
|
704
733
|
process.exit(0)
|
|
705
734
|
}
|
|
706
735
|
|
|
736
|
+
if (cmd === 'migrate-state') {
|
|
737
|
+
// COMP-MIGRATE-ON-UPGRADE — run pending feature.json state migrations explicitly.
|
|
738
|
+
// Distinct from `compose roadmap migrate` (ROADMAP→feature.json backfill).
|
|
739
|
+
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
740
|
+
const dryRun = args.includes('--dry-run')
|
|
741
|
+
const { runStateMigrations, summarizeMigrationReport } = await import('../lib/state-migrations.js')
|
|
742
|
+
const rep = runStateMigrations(cwd, { dryRun })
|
|
743
|
+
if (rep.skipped) {
|
|
744
|
+
console.log(`migrate-state: skipped (${rep.skipped})`)
|
|
745
|
+
process.exit(0)
|
|
746
|
+
}
|
|
747
|
+
console.log(summarizeMigrationReport(rep))
|
|
748
|
+
for (const m of rep.perMigration) {
|
|
749
|
+
if (m.touched.length) console.log(` [${m.id}] v${m.version}: ${m.touched.join(', ')}`)
|
|
750
|
+
}
|
|
751
|
+
for (const e of rep.parseErrors) console.error(` unparseable: ${e.path} — ${e.message}`)
|
|
752
|
+
process.exit(0)
|
|
753
|
+
}
|
|
754
|
+
|
|
707
755
|
if (cmd === 'install') {
|
|
708
756
|
// Backwards-compat: run both init + setup
|
|
709
757
|
await runInit(args)
|
package/lib/get-roadmap.js
CHANGED
|
@@ -46,13 +46,15 @@ function stripVolatile(text) {
|
|
|
46
46
|
/**
|
|
47
47
|
* @param {string} root - Project root.
|
|
48
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.
|
|
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
51
|
* @param {'summary'|'markdown'} [opts.format='summary'] - Omit or include raw markdown.
|
|
52
52
|
* @param {boolean} [opts.check_drift=true] - Compare render vs on-disk ROADMAP.md.
|
|
53
|
+
* @param {number} [opts.limit=50] - Cap on the general `rows` list (emitted only when
|
|
54
|
+
* a status/phase filter or an explicit limit is supplied — keeps the default call token-safe).
|
|
53
55
|
*/
|
|
54
56
|
export function getRoadmap(root, opts = {}) {
|
|
55
|
-
const { status, phase, format = 'summary', check_drift = true } = opts ?? {};
|
|
57
|
+
const { status, phase, format = 'summary', check_drift = true, limit } = opts ?? {};
|
|
56
58
|
|
|
57
59
|
const roadmapPath = join(root, 'ROADMAP.md');
|
|
58
60
|
const onDisk = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : '';
|
|
@@ -103,6 +105,23 @@ export function getRoadmap(root, opts = {}) {
|
|
|
103
105
|
|
|
104
106
|
const out = { source, path: roadmapPath, summary, active, blocked };
|
|
105
107
|
|
|
108
|
+
// General filtered rows list — emitted only when the caller signals intent via a
|
|
109
|
+
// status/phase filter or an explicit limit. This is what lets `/roadmap next` read
|
|
110
|
+
// the PLANNED list structured instead of re-parsing the markdown. The no-arg summary
|
|
111
|
+
// call stays token-safe (no rows key). Anonymous rows are excluded, same as the lists.
|
|
112
|
+
const wantsRows = status != null || phase != null || limit != null;
|
|
113
|
+
if (wantsRows) {
|
|
114
|
+
// Resolve the row cap with predictable semantics for malformed input: a finite
|
|
115
|
+
// number is floored and clamped to >= 0 (so limit:-1 → 0 rows, limit:1.5 → 1),
|
|
116
|
+
// never silently widened. Anything non-finite (no limit, just a status/phase
|
|
117
|
+
// filter) falls back to the default 50.
|
|
118
|
+
const cap = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 50;
|
|
119
|
+
const matched = named.filter(matchFilter).map(pick);
|
|
120
|
+
out.rowsTotal = matched.length;
|
|
121
|
+
out.rows = matched.slice(0, cap);
|
|
122
|
+
out.rowsTruncated = matched.length > cap;
|
|
123
|
+
}
|
|
124
|
+
|
|
106
125
|
if (check_drift) {
|
|
107
126
|
if (narrative) {
|
|
108
127
|
out.stale = false; // the content IS the file
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMP-MIGRATE-ON-UPGRADE — versioned, eager feature.json state migration.
|
|
3
|
+
*
|
|
4
|
+
* `compose upgrade` refreshes code but historically ran no state migration:
|
|
5
|
+
* vision-state is migrated lazily on every load, but feature.json cold data
|
|
6
|
+
* (e.g. legacy `complexity: null` that fails the schema oneOf) is never touched
|
|
7
|
+
* unless the file happens to be rewritten. This runner walks every feature.json
|
|
8
|
+
* eagerly, applies an ordered registry of pure transforms, and records progress
|
|
9
|
+
* in a durable, dedicated state file.
|
|
10
|
+
*
|
|
11
|
+
* Design: docs/features/COMP-MIGRATE-ON-UPGRADE/design.md (rev 2).
|
|
12
|
+
*
|
|
13
|
+
* Invariants:
|
|
14
|
+
* - migrateFeature transforms are PURE and TOTAL: no I/O, never throw on
|
|
15
|
+
* valid-JSON input. (A throw means a migration-code bug → fail-fast.)
|
|
16
|
+
* - The runner owns all I/O, atomicity (temp+rename), and reporting.
|
|
17
|
+
* - The stamp lives in .compose/data/migration-state.json — NOT compose.json
|
|
18
|
+
* (runInit rewrites compose.json from a fixed shape and would drop it; and a
|
|
19
|
+
* torn write to tracker config would break workspace resolution).
|
|
20
|
+
* - Convergent: corrupt/unparseable feature.json is REPORTED, not a permanent
|
|
21
|
+
* block — the stamp still advances, so one bad file can't re-run the whole
|
|
22
|
+
* stack forever.
|
|
23
|
+
* - local-provider only; narrative-safe (never regenerates ROADMAP.md).
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
readFileSync, writeFileSync, existsSync, mkdirSync,
|
|
27
|
+
renameSync, unlinkSync, readdirSync,
|
|
28
|
+
} from 'fs';
|
|
29
|
+
import { join } from 'path';
|
|
30
|
+
import { loadFeaturesDir } from './project-paths.js';
|
|
31
|
+
import { writeFeature } from './feature-json.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ordered, append-only migration registry. Each `migrateFeature` is pure/total.
|
|
35
|
+
* @type {{version:number, id:string, describe:string,
|
|
36
|
+
* migrateFeature:(f:object)=>{changed:boolean, feature:object}}[]}
|
|
37
|
+
*/
|
|
38
|
+
// Legacy free-text complexity → schema enum (S/M/L/XL), preserving ordinal intent.
|
|
39
|
+
const COMPLEXITY_SYNONYMS = {
|
|
40
|
+
xs: 'S', s: 'S', small: 'S', low: 'S', trivial: 'S',
|
|
41
|
+
m: 'M', medium: 'M', med: 'M', moderate: 'M',
|
|
42
|
+
l: 'L', large: 'L', high: 'L',
|
|
43
|
+
xl: 'XL', 'extra-large': 'XL', 'extra large': 'XL', 'very-high': 'XL', 'very high': 'XL',
|
|
44
|
+
};
|
|
45
|
+
const VALID_COMPLEXITY = new Set(['S', 'M', 'L', 'XL']);
|
|
46
|
+
|
|
47
|
+
export const MIGRATIONS = [
|
|
48
|
+
{
|
|
49
|
+
version: 1,
|
|
50
|
+
id: 'normalize-complexity',
|
|
51
|
+
describe: 'Normalize legacy free-text complexity to the S/M/L/XL enum; drop null/unmappable (fails the string|number oneOf)',
|
|
52
|
+
migrateFeature(f) {
|
|
53
|
+
// Total on any parseable JSON value: a non-object (scalar/array/null)
|
|
54
|
+
// feature.json is malformed data, not a migration target — never throw.
|
|
55
|
+
if (!f || typeof f !== 'object' || Array.isArray(f)) return { changed: false, feature: f };
|
|
56
|
+
if (!('complexity' in f)) return { changed: false, feature: f };
|
|
57
|
+
const c = f.complexity;
|
|
58
|
+
// Already schema-valid: a number or an enum member — leave untouched.
|
|
59
|
+
if (typeof c === 'number') return { changed: false, feature: f };
|
|
60
|
+
if (typeof c === 'string' && VALID_COMPLEXITY.has(c)) return { changed: false, feature: f };
|
|
61
|
+
// Mappable legacy free-text → enum.
|
|
62
|
+
if (typeof c === 'string') {
|
|
63
|
+
const mapped = COMPLEXITY_SYNONYMS[c.trim().toLowerCase()];
|
|
64
|
+
if (mapped) return { changed: true, feature: { ...f, complexity: mapped } };
|
|
65
|
+
}
|
|
66
|
+
// null or unmappable (object/garbage string) → drop the optional key.
|
|
67
|
+
const { complexity, ...rest } = f;
|
|
68
|
+
return { changed: true, feature: rest };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function stateFilePath(cwd) {
|
|
74
|
+
return join(cwd, '.compose', 'data', 'migration-state.json');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Read the durable stamp. Absent or corrupt ⇒ {stateVersion:0, applied:[]}. */
|
|
78
|
+
export function readMigrationState(cwd) {
|
|
79
|
+
const p = stateFilePath(cwd);
|
|
80
|
+
if (!existsSync(p)) return { stateVersion: 0, applied: [] };
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(readFileSync(p, 'utf-8'));
|
|
83
|
+
return {
|
|
84
|
+
stateVersion: Number.isInteger(parsed?.stateVersion) ? parsed.stateVersion : 0,
|
|
85
|
+
applied: Array.isArray(parsed?.applied) ? parsed.applied : [],
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
// A corrupt state file is treated as version 0; re-running is idempotent.
|
|
89
|
+
return { stateVersion: 0, applied: [] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeMigrationStateAtomic(cwd, state) {
|
|
94
|
+
const dir = join(cwd, '.compose', 'data');
|
|
95
|
+
mkdirSync(dir, { recursive: true });
|
|
96
|
+
const p = join(dir, 'migration-state.json');
|
|
97
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
98
|
+
try {
|
|
99
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n');
|
|
100
|
+
renameSync(tmp, p);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
try { unlinkSync(tmp); } catch { /* tmp may not exist */ }
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Tracker classification from `.compose/compose.json`:
|
|
109
|
+
* 'local' — tracker absent/null or provider==='local'
|
|
110
|
+
* 'non-local' — an explicit non-local provider (e.g. github)
|
|
111
|
+
* 'unreadable' — config present but not parseable (mirrors factory.js failing
|
|
112
|
+
* fast rather than silently assuming local)
|
|
113
|
+
*/
|
|
114
|
+
function trackerKind(cwd) {
|
|
115
|
+
let cfg;
|
|
116
|
+
try {
|
|
117
|
+
cfg = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf-8'));
|
|
118
|
+
} catch {
|
|
119
|
+
return 'unreadable';
|
|
120
|
+
}
|
|
121
|
+
const t = cfg.tracker;
|
|
122
|
+
if (t === undefined || t === null || !t.provider || t.provider === 'local') return 'local';
|
|
123
|
+
return 'non-local';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run pending feature.json state migrations for the workspace at `cwd`.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} cwd - workspace root
|
|
130
|
+
* @param {{dryRun?: boolean}} [opts]
|
|
131
|
+
* @returns {object} report — one of:
|
|
132
|
+
* {skipped:'no-workspace'|'non-local-tracker'}
|
|
133
|
+
* {from, to, dryRun, noop:true, perMigration:[], parseErrors:[]}
|
|
134
|
+
* {from, to, dryRun, perMigration:[{id,version,touched:[]}], parseErrors:[{path,message}]}
|
|
135
|
+
* @throws if a migrateFeature transform throws (migration-code bug) — aborts
|
|
136
|
+
* WITHOUT advancing the stamp.
|
|
137
|
+
*/
|
|
138
|
+
export function runStateMigrations(cwd, opts = {}) {
|
|
139
|
+
const dryRun = !!opts.dryRun;
|
|
140
|
+
|
|
141
|
+
// Guard: never write stray state into a non-workspace cwd (e.g. when runUpdate
|
|
142
|
+
// → runInit lands on a process.cwd() that isn't the resolved workspace root).
|
|
143
|
+
if (!existsSync(join(cwd, '.compose', 'compose.json'))) {
|
|
144
|
+
return { skipped: 'no-workspace' };
|
|
145
|
+
}
|
|
146
|
+
const kind = trackerKind(cwd);
|
|
147
|
+
if (kind === 'non-local') return { skipped: 'non-local-tracker' };
|
|
148
|
+
if (kind === 'unreadable') return { skipped: 'unreadable-config' };
|
|
149
|
+
|
|
150
|
+
const { stateVersion: from, applied } = readMigrationState(cwd);
|
|
151
|
+
const pending = MIGRATIONS
|
|
152
|
+
.filter((m) => m.version > from)
|
|
153
|
+
.sort((a, b) => a.version - b.version);
|
|
154
|
+
if (pending.length === 0) {
|
|
155
|
+
return { from, to: from, dryRun, noop: true, perMigration: [], parseErrors: [] };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const featuresDir = loadFeaturesDir(cwd); // honors paths.features override
|
|
159
|
+
const featuresRoot = join(cwd, featuresDir);
|
|
160
|
+
const perMigration = pending.map((m) => ({ id: m.id, version: m.version, touched: [] }));
|
|
161
|
+
const parseErrors = [];
|
|
162
|
+
|
|
163
|
+
// Own directory walk — do NOT use listFeatures(): it silently skips unreadable
|
|
164
|
+
// files, which would hide exactly the cold-data corruption we must surface.
|
|
165
|
+
let dirs = [];
|
|
166
|
+
try {
|
|
167
|
+
dirs = readdirSync(featuresRoot, { withFileTypes: true })
|
|
168
|
+
.filter((e) => e.isDirectory())
|
|
169
|
+
.map((e) => e.name);
|
|
170
|
+
} catch {
|
|
171
|
+
dirs = []; // features dir may not exist yet
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const code of dirs) {
|
|
175
|
+
const fpath = join(featuresRoot, code, 'feature.json');
|
|
176
|
+
if (!existsSync(fpath)) continue;
|
|
177
|
+
let feature;
|
|
178
|
+
try {
|
|
179
|
+
feature = JSON.parse(readFileSync(fpath, 'utf-8'));
|
|
180
|
+
} catch (err) {
|
|
181
|
+
parseErrors.push({ path: fpath, message: err.message });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
let changedAny = false;
|
|
185
|
+
pending.forEach((m, i) => {
|
|
186
|
+
const res = m.migrateFeature(feature); // pure/total; a throw = migration-code bug → fail-fast
|
|
187
|
+
if (res.changed) {
|
|
188
|
+
feature = res.feature;
|
|
189
|
+
changedAny = true;
|
|
190
|
+
perMigration[i].touched.push(feature.code || code);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (changedAny && !dryRun) {
|
|
194
|
+
writeFeature(cwd, feature, featuresDir, { validate: false });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const to = pending[pending.length - 1].version;
|
|
199
|
+
if (!dryRun) {
|
|
200
|
+
writeMigrationStateAtomic(cwd, {
|
|
201
|
+
stateVersion: to,
|
|
202
|
+
applied: applied.concat(pending.map((m) => ({ version: m.version, id: m.id }))),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return { from, to, dryRun, perMigration, parseErrors };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One-line human summary for the upgrade/init narration. */
|
|
209
|
+
export function summarizeMigrationReport(report) {
|
|
210
|
+
if (!report || report.skipped) return null;
|
|
211
|
+
if (report.noop) return `state up to date (v${report.to})`;
|
|
212
|
+
const touched = report.perMigration.reduce((n, m) => n + m.touched.length, 0);
|
|
213
|
+
const errs = report.parseErrors.length;
|
|
214
|
+
const dry = report.dryRun ? ' (dry-run)' : '';
|
|
215
|
+
return `migrated ${touched} feature.json across ${report.perMigration.length} migration(s) `
|
|
216
|
+
+ `→ stateVersion ${report.to}${errs ? `, ${errs} unparseable (reported)` : ''}${dry}`;
|
|
217
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41-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",
|
package/server/compose-mcp.js
CHANGED
|
@@ -361,14 +361,15 @@ const TOOLS = [
|
|
|
361
361
|
},
|
|
362
362
|
{
|
|
363
363
|
name: 'get_roadmap',
|
|
364
|
-
description: 'Read the current roadmap rendered from canon (feature.json) WITHOUT writing. Returns a status summary
|
|
364
|
+
description: 'Read the current roadmap rendered from canon (feature.json) WITHOUT writing. Returns a status summary, the active/blocked convenience lists, and a staleness flag vs on-disk ROADMAP.md. Pass a status/phase filter or a limit to also get a general `rows` list (e.g. {status:"PLANNED", limit:10} for "what to work on next") — structured rows so callers never re-parse the markdown. Narrative-owned workspaces return the hand-authored file verbatim. Read-only — prefer this over reading ROADMAP.md directly.',
|
|
365
365
|
inputSchema: {
|
|
366
366
|
type: 'object',
|
|
367
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)' },
|
|
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
370
|
format: { type: 'string', description: '"summary" (default — counts + lists, token-safe) or "markdown" (full rendered text)' },
|
|
371
371
|
check_drift: { type: 'boolean', description: 'Compare the render against on-disk ROADMAP.md and set stale/drift (default true)' },
|
|
372
|
+
limit: { type: 'integer', minimum: 0, description: 'Cap on the general `rows` list (default 50). Supplying status/phase/limit emits rows[]/rowsTotal/rowsTruncated; without any of them rows is omitted (token-safe summary). A finite value is floored and clamped to ≥ 0.' },
|
|
372
373
|
},
|
|
373
374
|
},
|
|
374
375
|
},
|