@smartmemory/compose 0.1.4-beta → 0.1.6-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 +279 -0
- package/bin/git-hooks/post-commit.template +61 -0
- package/lib/build.js +31 -3
- package/lib/changelog-writer.js +647 -0
- package/lib/completion-writer.js +465 -0
- package/lib/feature-events.js +114 -0
- package/lib/feature-writer.js +585 -0
- package/lib/idempotency.js +138 -0
- package/lib/journal-writer.js +928 -0
- package/lib/roadmap-parser.js +3 -1
- package/lib/sections.js +188 -0
- package/package.json +5 -1
- package/server/compose-mcp-tools.js +82 -0
- package/server/compose-mcp.js +273 -1
package/lib/roadmap-parser.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* from the markdown table format used by Compose roadmaps.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Statuses that exclude a feature from the buildable list. KILLED is
|
|
9
|
+
// terminal; BLOCKED isn't buildable until unblocked.
|
|
10
|
+
const SKIP_STATUSES = new Set(['COMPLETE', 'SUPERSEDED', 'PARKED', 'KILLED', 'BLOCKED']);
|
|
9
11
|
|
|
10
12
|
const PHASE_HEADING_RE = /^##\s+(.+?)(?:\s+—\s+(.+))?$/;
|
|
11
13
|
const MILESTONE_HEADING_RE = /^###\s+(.+?)(?:\s*:\s*(.+))?$/;
|
package/lib/sections.js
CHANGED
|
@@ -323,3 +323,191 @@ export function appendTrailers({ featureDir, commit, filesChanged, cwd, diffStat
|
|
|
323
323
|
|
|
324
324
|
return result;
|
|
325
325
|
}
|
|
326
|
+
|
|
327
|
+
// ---------- COMP-PLAN-SECTIONS-REPORT: roll-up ----------
|
|
328
|
+
|
|
329
|
+
const SECTION_FILE_RE = /^section-(\d+)-.+\.md$/;
|
|
330
|
+
const ROLLUP_HEADING_RE = /^## Section Roll-up\b/m;
|
|
331
|
+
const ROLLUP_NEXT_HEADING_RE = /^## /m;
|
|
332
|
+
|
|
333
|
+
function parseSectionTitle(content, filename) {
|
|
334
|
+
// Expect H1 like: `# Section NN — <title>`. Fallback to filename slug.
|
|
335
|
+
const m = content.match(/^#\s+Section\s+\d+\s*[—\-:.]\s*(.+?)\s*$/m);
|
|
336
|
+
if (m && m[1]) return m[1].trim();
|
|
337
|
+
// Filename slug fallback: strip section-NN- prefix and .md suffix.
|
|
338
|
+
const fm = filename.match(/^section-\d+-(.+)\.md$/);
|
|
339
|
+
return fm ? fm[1] : filename;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function todayIso() {
|
|
343
|
+
const d = new Date();
|
|
344
|
+
const y = d.getFullYear();
|
|
345
|
+
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
346
|
+
const da = String(d.getDate()).padStart(2, '0');
|
|
347
|
+
return `${y}-${mo}-${da}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* analyzeRollup({ sectionsDir, filesChanged }) — read-only analyzer.
|
|
352
|
+
*
|
|
353
|
+
* Returns null when sectionsDir is absent OR contains no `section-NN-*.md`
|
|
354
|
+
* files. Otherwise returns:
|
|
355
|
+
* {
|
|
356
|
+
* sections: [{ filename, title, declared, changed, missing }],
|
|
357
|
+
* unattributed: string[],
|
|
358
|
+
* sectionCount: number,
|
|
359
|
+
* sectionsWithChanges: number, // declared.length > 0 && changed.length === declared.length
|
|
360
|
+
* sectionsAllUnchanged: number, // declared.length > 0 && changed.length === 0
|
|
361
|
+
* }
|
|
362
|
+
*/
|
|
363
|
+
export function analyzeRollup({ sectionsDir, filesChanged } = {}) {
|
|
364
|
+
if (!sectionsDir || !fs.existsSync(sectionsDir)) return null;
|
|
365
|
+
|
|
366
|
+
const entries = fs
|
|
367
|
+
.readdirSync(sectionsDir)
|
|
368
|
+
.filter(f => SECTION_FILE_RE.test(f))
|
|
369
|
+
.sort();
|
|
370
|
+
if (entries.length === 0) return null;
|
|
371
|
+
|
|
372
|
+
const changedSet = new Set(Array.isArray(filesChanged) ? filesChanged : []);
|
|
373
|
+
const declaredUnion = new Set();
|
|
374
|
+
const sections = [];
|
|
375
|
+
let sectionsWithChanges = 0;
|
|
376
|
+
let sectionsAllUnchanged = 0;
|
|
377
|
+
|
|
378
|
+
for (const filename of entries) {
|
|
379
|
+
const fullPath = path.join(sectionsDir, filename);
|
|
380
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
381
|
+
const declared = readDeclaredFiles(content);
|
|
382
|
+
for (const f of declared) declaredUnion.add(f);
|
|
383
|
+
const changed = declared.filter(f => changedSet.has(f));
|
|
384
|
+
const missing = declared.filter(f => !changedSet.has(f));
|
|
385
|
+
const title = parseSectionTitle(content, filename);
|
|
386
|
+
if (declared.length > 0 && changed.length === declared.length) sectionsWithChanges++;
|
|
387
|
+
if (declared.length > 0 && changed.length === 0) sectionsAllUnchanged++;
|
|
388
|
+
sections.push({ filename, title, declared, changed, missing });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const unattributed = [];
|
|
392
|
+
for (const f of changedSet) {
|
|
393
|
+
if (!declaredUnion.has(f)) unattributed.push(f);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
sections,
|
|
398
|
+
unattributed,
|
|
399
|
+
sectionCount: sections.length,
|
|
400
|
+
sectionsWithChanges,
|
|
401
|
+
sectionsAllUnchanged,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* renderRollupBlock({ analysis, commit, date }) — pure markdown renderer.
|
|
407
|
+
* No I/O. Returns the full `## Section Roll-up` block with trailing newline.
|
|
408
|
+
*/
|
|
409
|
+
export function renderRollupBlock({ analysis, commit, date } = {}) {
|
|
410
|
+
const shortSha = commit && typeof commit === 'string' && commit.length > 0
|
|
411
|
+
? `\`${commit.slice(0, 7)}\``
|
|
412
|
+
: '(commit unavailable)';
|
|
413
|
+
const dateStr = date && typeof date === 'string' && date ? date : todayIso();
|
|
414
|
+
|
|
415
|
+
const sections = analysis?.sections ?? [];
|
|
416
|
+
const unattributed = analysis?.unattributed ?? [];
|
|
417
|
+
const sectionCount = analysis?.sectionCount ?? 0;
|
|
418
|
+
const sectionsWithChanges = analysis?.sectionsWithChanges ?? 0;
|
|
419
|
+
const sectionsAllUnchanged = analysis?.sectionsAllUnchanged ?? 0;
|
|
420
|
+
|
|
421
|
+
// Section NN derived from filename prefix.
|
|
422
|
+
const indexLines = sections.map(s => {
|
|
423
|
+
const numMatch = s.filename.match(/^section-(\d+)-/);
|
|
424
|
+
const nn = numMatch ? numMatch[1] : '??';
|
|
425
|
+
const changedCount = s.changed?.length ?? 0;
|
|
426
|
+
const declaredCount = s.declared?.length ?? 0;
|
|
427
|
+
return `- [Section ${nn} — ${s.title}](sections/${s.filename}) — \`${changedCount}/${declaredCount}\` files changed`;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const unattribLines = unattributed.length
|
|
431
|
+
? unattributed.map(f => `- \`${f}\``)
|
|
432
|
+
: ['None'];
|
|
433
|
+
|
|
434
|
+
const lines = [
|
|
435
|
+
`## Section Roll-up`,
|
|
436
|
+
``,
|
|
437
|
+
`**Commit:** ${shortSha}`,
|
|
438
|
+
`**Date:** ${dateStr}`,
|
|
439
|
+
`**Sections:** ${sectionCount} total — ${sectionsWithChanges} with changes / ${sectionsAllUnchanged} with no declared changes`,
|
|
440
|
+
``,
|
|
441
|
+
`### Index`,
|
|
442
|
+
``,
|
|
443
|
+
...indexLines,
|
|
444
|
+
``,
|
|
445
|
+
`### Unattributed files this commit`,
|
|
446
|
+
``,
|
|
447
|
+
...unattribLines,
|
|
448
|
+
``,
|
|
449
|
+
`### Deviations summary`,
|
|
450
|
+
``,
|
|
451
|
+
`- **Sections with all declared files changed:** ${sectionsWithChanges}`,
|
|
452
|
+
`- **Sections with declared files that did NOT change:** ${sectionsAllUnchanged}`,
|
|
453
|
+
`- **Files changed but undeclared:** ${unattributed.length}`,
|
|
454
|
+
``,
|
|
455
|
+
];
|
|
456
|
+
return lines.join('\n');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* writeRollup({ featureDir, analysis, commit, date }) — atomic same-directory
|
|
461
|
+
* temp+rename writer for `<featureDir>/report.md`.
|
|
462
|
+
*
|
|
463
|
+
* Returns null when analysis is null OR sectionCount === 0.
|
|
464
|
+
* Otherwise replaces an existing `## Section Roll-up` block in place
|
|
465
|
+
* (boundary: heading → next `^## ` heading or EOF) or appends if absent.
|
|
466
|
+
* Returns { written: true, path }.
|
|
467
|
+
*/
|
|
468
|
+
export function writeRollup({ featureDir, analysis, commit, date } = {}) {
|
|
469
|
+
if (!featureDir) return null;
|
|
470
|
+
if (!analysis || analysis.sectionCount === 0) return null;
|
|
471
|
+
|
|
472
|
+
const block = renderRollupBlock({ analysis, commit, date });
|
|
473
|
+
const reportPath = path.join(featureDir, 'report.md');
|
|
474
|
+
const tmpPath = path.join(featureDir, 'report.md.tmp');
|
|
475
|
+
|
|
476
|
+
let existing = '';
|
|
477
|
+
if (fs.existsSync(reportPath)) {
|
|
478
|
+
existing = fs.readFileSync(reportPath, 'utf8');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let next;
|
|
482
|
+
const headingMatch = existing.match(ROLLUP_HEADING_RE);
|
|
483
|
+
if (headingMatch && typeof headingMatch.index === 'number') {
|
|
484
|
+
const start = headingMatch.index;
|
|
485
|
+
const after = existing.slice(start + headingMatch[0].length);
|
|
486
|
+
const nextMatch = after.match(ROLLUP_NEXT_HEADING_RE);
|
|
487
|
+
let endRel;
|
|
488
|
+
if (nextMatch && typeof nextMatch.index === 'number') {
|
|
489
|
+
endRel = start + headingMatch[0].length + nextMatch.index;
|
|
490
|
+
} else {
|
|
491
|
+
endRel = existing.length;
|
|
492
|
+
}
|
|
493
|
+
const before = existing.slice(0, start);
|
|
494
|
+
const tail = existing.slice(endRel);
|
|
495
|
+
next = before + block + (tail.startsWith('\n') || !tail ? tail : '\n' + tail);
|
|
496
|
+
} else if (existing.length === 0) {
|
|
497
|
+
next = block;
|
|
498
|
+
} else {
|
|
499
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
500
|
+
next = existing + sep + block;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
504
|
+
try {
|
|
505
|
+
fs.writeFileSync(tmpPath, next);
|
|
506
|
+
fs.renameSync(tmpPath, reportPath);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
try { if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return { written: true, path: reportPath };
|
|
513
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6-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",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"bin": {
|
|
9
9
|
"compose": "./bin/compose.js"
|
|
10
10
|
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"./mcp": "./server/compose-mcp.js",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
11
15
|
"scripts": {
|
|
12
16
|
"dev": "node server/supervisor.js 2>&1 | tee /tmp/compose-server.log",
|
|
13
17
|
"dev:server": "node server/supervisor.js",
|
|
@@ -196,6 +196,88 @@ export async function toolGetCurrentSession({ featureCode } = {}) {
|
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Roadmap writers — COMP-MCP-ROADMAP-WRITER
|
|
201
|
+
// Pure file-based mutations via lib/feature-writer.js. No HTTP delegation.
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
export async function toolAddRoadmapEntry(args) {
|
|
205
|
+
const { addRoadmapEntry } = await import('../lib/feature-writer.js');
|
|
206
|
+
return addRoadmapEntry(getTargetRoot(), args);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function toolSetFeatureStatus(args) {
|
|
210
|
+
const { setFeatureStatus } = await import('../lib/feature-writer.js');
|
|
211
|
+
return setFeatureStatus(getTargetRoot(), args);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function toolRoadmapDiff(args) {
|
|
215
|
+
const { roadmapDiff } = await import('../lib/feature-writer.js');
|
|
216
|
+
return roadmapDiff(getTargetRoot(), args);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function toolLinkArtifact(args) {
|
|
220
|
+
const { linkArtifact } = await import('../lib/feature-writer.js');
|
|
221
|
+
return linkArtifact(getTargetRoot(), args);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function toolLinkFeatures(args) {
|
|
225
|
+
const { linkFeatures } = await import('../lib/feature-writer.js');
|
|
226
|
+
return linkFeatures(getTargetRoot(), args);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function toolGetFeatureArtifacts(args) {
|
|
230
|
+
const { getFeatureArtifacts } = await import('../lib/feature-writer.js');
|
|
231
|
+
return getFeatureArtifacts(getTargetRoot(), args);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function toolGetFeatureLinks(args) {
|
|
235
|
+
const { getFeatureLinks } = await import('../lib/feature-writer.js');
|
|
236
|
+
return getFeatureLinks(getTargetRoot(), args);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Changelog writer — COMP-MCP-CHANGELOG-WRITER
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
export async function toolAddChangelogEntry(args) {
|
|
244
|
+
const { addChangelogEntry } = await import('../lib/changelog-writer.js');
|
|
245
|
+
return addChangelogEntry(getTargetRoot(), args);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function toolGetChangelogEntries(args) {
|
|
249
|
+
const { getChangelogEntries } = await import('../lib/changelog-writer.js');
|
|
250
|
+
return getChangelogEntries(getTargetRoot(), args);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Journal writer — COMP-MCP-JOURNAL-WRITER
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
export async function toolWriteJournalEntry(args) {
|
|
258
|
+
const { writeJournalEntry } = await import('../lib/journal-writer.js');
|
|
259
|
+
return writeJournalEntry(getTargetRoot(), args);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function toolGetJournalEntries(args) {
|
|
263
|
+
const { getJournalEntries } = await import('../lib/journal-writer.js');
|
|
264
|
+
return getJournalEntries(getTargetRoot(), args);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Completion writer — COMP-MCP-COMPLETION
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export async function toolRecordCompletion(args) {
|
|
272
|
+
const { recordCompletion } = await import('../lib/completion-writer.js');
|
|
273
|
+
return recordCompletion(getTargetRoot(), args);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function toolGetCompletions(args) {
|
|
277
|
+
const { getCompletions } = await import('../lib/completion-writer.js');
|
|
278
|
+
return getCompletions(getTargetRoot(), args);
|
|
279
|
+
}
|
|
280
|
+
|
|
199
281
|
export async function toolBindSession({ featureCode }) {
|
|
200
282
|
const postData = JSON.stringify({ featureCode });
|
|
201
283
|
return new Promise((resolve, reject) => {
|
package/server/compose-mcp.js
CHANGED
|
@@ -43,6 +43,19 @@ import {
|
|
|
43
43
|
toolIterationStart,
|
|
44
44
|
toolIterationReport,
|
|
45
45
|
toolIterationAbort,
|
|
46
|
+
toolAddRoadmapEntry,
|
|
47
|
+
toolSetFeatureStatus,
|
|
48
|
+
toolRoadmapDiff,
|
|
49
|
+
toolLinkArtifact,
|
|
50
|
+
toolLinkFeatures,
|
|
51
|
+
toolGetFeatureArtifacts,
|
|
52
|
+
toolGetFeatureLinks,
|
|
53
|
+
toolAddChangelogEntry,
|
|
54
|
+
toolGetChangelogEntries,
|
|
55
|
+
toolWriteJournalEntry,
|
|
56
|
+
toolGetJournalEntries,
|
|
57
|
+
toolRecordCompletion,
|
|
58
|
+
toolGetCompletions,
|
|
46
59
|
} from './compose-mcp-tools.js';
|
|
47
60
|
|
|
48
61
|
// ---------------------------------------------------------------------------
|
|
@@ -258,6 +271,239 @@ const TOOLS = [
|
|
|
258
271
|
},
|
|
259
272
|
// `agent_run` tool removed 2026-04-18 (STRAT-DEDUP-AGENTRUN v1); LLM-facing
|
|
260
273
|
// dispatch goes through `mcp__stratum__stratum_agent_run`.
|
|
274
|
+
|
|
275
|
+
// -------------------------------------------------------------------------
|
|
276
|
+
// Roadmap writers — COMP-MCP-ROADMAP-WRITER
|
|
277
|
+
// -------------------------------------------------------------------------
|
|
278
|
+
{
|
|
279
|
+
name: 'add_roadmap_entry',
|
|
280
|
+
description: 'Register a new feature in the project. Writes feature.json and regenerates ROADMAP.md (audit-log append is best-effort). Use this instead of editing ROADMAP.md by hand.',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
required: ['code', 'description', 'phase'],
|
|
284
|
+
properties: {
|
|
285
|
+
code: { type: 'string', description: 'Unique feature code (e.g. "COMP-FOO-1"). Must be uppercase A-Z, digits, dashes; cannot start or end with a dash.' },
|
|
286
|
+
description: { type: 'string', description: 'One-line description for the ROADMAP cell' },
|
|
287
|
+
phase: { type: 'string', description: 'Phase heading (e.g. "Phase 6: MCP Writers"). Required.' },
|
|
288
|
+
complexity: { type: 'string', enum: ['S', 'M', 'L', 'XL'] },
|
|
289
|
+
status: { type: 'string', enum: ['PLANNED', 'IN_PROGRESS', 'PARTIAL', 'COMPLETE', 'BLOCKED', 'KILLED', 'PARKED', 'SUPERSEDED'], description: 'Initial status (default PLANNED)' },
|
|
290
|
+
position: { type: 'number', description: 'Sort order within phase' },
|
|
291
|
+
parent: { type: 'string', description: 'Parent feature code, for cross-references' },
|
|
292
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
293
|
+
idempotency_key: { type: 'string', description: 'Optional caller-provided key. Same key replays return the cached result without re-mutating.' },
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'set_feature_status',
|
|
299
|
+
description: 'Flip a feature status. Updates feature.json and regenerates ROADMAP.md. Enforces a transition policy (use force: true to bypass). Appends an audit event (best-effort).',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
required: ['code', 'status'],
|
|
303
|
+
properties: {
|
|
304
|
+
code: { type: 'string' },
|
|
305
|
+
status: { type: 'string', enum: ['PLANNED', 'IN_PROGRESS', 'PARTIAL', 'COMPLETE', 'BLOCKED', 'KILLED', 'PARKED', 'SUPERSEDED'] },
|
|
306
|
+
reason: { type: 'string', description: 'Free-form reason persisted in the audit event' },
|
|
307
|
+
commit_sha: { type: 'string', description: 'Optional commit binding' },
|
|
308
|
+
force: { type: 'boolean', description: 'Bypass the transition policy. Recorded in audit.' },
|
|
309
|
+
idempotency_key: { type: 'string' },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: 'roadmap_diff',
|
|
315
|
+
description: 'Read the feature-management audit log for a window. Returns events plus derived added[] and status_changed[] arrays.',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
properties: {
|
|
319
|
+
since: { type: 'string', description: 'Window: shorthand like "24h"/"7d"/"30m", or an ISO date. Default 24h.' },
|
|
320
|
+
feature_code: { type: 'string' },
|
|
321
|
+
tool: { type: 'string', description: 'Filter to one tool name, e.g. "set_feature_status"' },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
// -------------------------------------------------------------------------
|
|
327
|
+
// Linker — COMP-MCP-ARTIFACT-LINKER
|
|
328
|
+
// -------------------------------------------------------------------------
|
|
329
|
+
{
|
|
330
|
+
name: 'link_artifact',
|
|
331
|
+
description: 'Register a non-canonical artifact (snapshot, journal entry, finding, etc.) on a feature. Canonical artifacts (design.md, plan.md, …) inside the feature folder are auto-discovered and rejected here. Stores in feature.json artifacts[]; dedups on (type, path); appends an audit event (best-effort).',
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
required: ['feature_code', 'artifact_type', 'path'],
|
|
335
|
+
properties: {
|
|
336
|
+
feature_code: { type: 'string' },
|
|
337
|
+
artifact_type: { type: 'string', description: 'e.g. "journal", "snapshot", "finding", "report-supplement", "link", "external"' },
|
|
338
|
+
path: { type: 'string', description: 'Repo-relative path. Must exist; cannot contain ".." after normalization.' },
|
|
339
|
+
status: { type: 'string', enum: ['current', 'superseded', 'historical'] },
|
|
340
|
+
force: { type: 'boolean', description: 'Overwrite an existing entry with the same (type, path)' },
|
|
341
|
+
idempotency_key: { type: 'string' },
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'link_features',
|
|
347
|
+
description: 'Register a typed cross-feature relationship. Stores on the source feature; query the inverse via get_feature_links(direction:"incoming"). Closed enum on kind; self-links rejected; dedups on (kind, to_code).',
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: 'object',
|
|
350
|
+
required: ['from_code', 'to_code', 'kind'],
|
|
351
|
+
properties: {
|
|
352
|
+
from_code: { type: 'string' },
|
|
353
|
+
to_code: { type: 'string', description: 'Target feature code. Need not exist yet (you can link to a code you are about to create).' },
|
|
354
|
+
kind: { type: 'string', enum: ['surfaced_by', 'blocks', 'depends_on', 'follow_up', 'supersedes', 'related'] },
|
|
355
|
+
note: { type: 'string' },
|
|
356
|
+
force: { type: 'boolean' },
|
|
357
|
+
idempotency_key: { type: 'string' },
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'get_feature_artifacts',
|
|
363
|
+
description: 'Read both canonical (auto-discovered: design.md, plan.md, …) and linked (snapshots, journals, findings) artifacts for a feature in one call. Each linked entry includes a current existence check.',
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
required: ['feature_code'],
|
|
367
|
+
properties: {
|
|
368
|
+
feature_code: { type: 'string' },
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'get_feature_links',
|
|
374
|
+
description: 'Read outgoing and/or incoming feature links. Default returns both directions; filter by kind if needed.',
|
|
375
|
+
inputSchema: {
|
|
376
|
+
type: 'object',
|
|
377
|
+
required: ['feature_code'],
|
|
378
|
+
properties: {
|
|
379
|
+
feature_code: { type: 'string' },
|
|
380
|
+
direction: { type: 'string', enum: ['outgoing', 'incoming', 'both'] },
|
|
381
|
+
kind: { type: 'string' },
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
// -------------------------------------------------------------------------
|
|
387
|
+
// Changelog writer — COMP-MCP-CHANGELOG-WRITER
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
{
|
|
390
|
+
name: 'add_changelog_entry',
|
|
391
|
+
description: 'Insert (or replace, with force: true) a typed entry in compose/CHANGELOG.md. Idempotent on (date_or_version, code) at storage level; optional caller-supplied idempotency_key for retry safety. Audit-log append is best-effort. Use this instead of editing CHANGELOG.md by hand.',
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: 'object',
|
|
394
|
+
required: ['date_or_version', 'code', 'summary'],
|
|
395
|
+
properties: {
|
|
396
|
+
date_or_version: { type: 'string', description: 'ISO date "YYYY-MM-DD" or semver "vX.Y.Z"' },
|
|
397
|
+
code: { type: 'string', description: 'Feature code (e.g. "COMP-FOO-1"). Uppercase A-Z, digits, dashes; cannot start or end with a dash.' },
|
|
398
|
+
summary: { type: 'string', description: 'One-line summary; renders as the "— summary" tail of the entry header.' },
|
|
399
|
+
body: { type: 'string', description: 'Free paragraphs between header and labeled subsections.' },
|
|
400
|
+
sections: {
|
|
401
|
+
type: 'object',
|
|
402
|
+
description: 'Optional labeled subsections; emitted in fixed order Added → Changed → Fixed → Snapshot.',
|
|
403
|
+
properties: {
|
|
404
|
+
added: { type: 'array', items: { type: 'string' } },
|
|
405
|
+
changed: { type: 'array', items: { type: 'string' } },
|
|
406
|
+
fixed: { type: 'array', items: { type: 'string' } },
|
|
407
|
+
snapshot: { type: 'array', items: { type: 'string' } },
|
|
408
|
+
},
|
|
409
|
+
additionalProperties: false,
|
|
410
|
+
},
|
|
411
|
+
force: { type: 'boolean', description: 'If true and an entry with the same (date_or_version, code) exists, replace it in place.' },
|
|
412
|
+
idempotency_key: { type: 'string', description: 'Optional caller-supplied key. Same key replays return the cached result without re-mutating.' },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'get_changelog_entries',
|
|
418
|
+
description: 'Read parsed entries from compose/CHANGELOG.md. Filter by code (exact) or since (shorthand "24h"/"7d"/"30m" or ISO date — date-only; version surfaces always pass through).',
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
since: { type: 'string', description: 'Window: shorthand like "24h"/"7d"/"30m" or ISO date. Date-only filter; version surfaces are always returned.' },
|
|
423
|
+
code: { type: 'string' },
|
|
424
|
+
limit: { type: 'number', description: 'Default 50; capped at 500.' },
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
// -------------------------------------------------------------------------
|
|
430
|
+
// Journal writer — COMP-MCP-JOURNAL-WRITER
|
|
431
|
+
// -------------------------------------------------------------------------
|
|
432
|
+
{
|
|
433
|
+
name: 'write_journal_entry',
|
|
434
|
+
description: 'Write a typed entry to compose/docs/journal/ with auto-numbered global session and inserted index row. Idempotent on (date, slug) at storage level; optional caller idempotency_key for retry safety. Audit-log append is best-effort.',
|
|
435
|
+
inputSchema: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
required: ['date', 'slug', 'sections', 'summary_for_index'],
|
|
438
|
+
properties: {
|
|
439
|
+
date: { type: 'string', description: 'ISO date "YYYY-MM-DD".' },
|
|
440
|
+
slug: { type: 'string', description: 'Kebab-case slug for the filename, e.g. "mcp-journal-writer".' },
|
|
441
|
+
sections: {
|
|
442
|
+
type: 'object',
|
|
443
|
+
required: ['what_happened', 'what_we_built', 'what_we_learned', 'open_threads'],
|
|
444
|
+
properties: {
|
|
445
|
+
what_happened: { type: 'string' },
|
|
446
|
+
what_we_built: { type: 'string' },
|
|
447
|
+
what_we_learned: { type: 'string' },
|
|
448
|
+
open_threads: { type: 'string' },
|
|
449
|
+
},
|
|
450
|
+
additionalProperties: false,
|
|
451
|
+
},
|
|
452
|
+
summary_for_index: { type: 'string', description: 'Single-line summary for the README index row. No newlines, no "|".' },
|
|
453
|
+
feature_code: { type: 'string', description: 'Optional feature code stamped in entry frontmatter.' },
|
|
454
|
+
closing_line: { type: 'string', description: 'Optional final italicized one-liner.' },
|
|
455
|
+
force: { type: 'boolean', description: 'If true and an entry with the same (date, slug) exists, overwrite in place.' },
|
|
456
|
+
idempotency_key: { type: 'string', description: 'Optional caller-supplied key. Same key replays return the cached result without re-mutating.' },
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: 'get_journal_entries',
|
|
462
|
+
description: 'Read parsed entries from compose/docs/journal/. Filter by feature_code (exact), session (exact), or since (shorthand "24h"/"7d"/"30m" or ISO date).',
|
|
463
|
+
inputSchema: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
since: { type: 'string' },
|
|
467
|
+
feature_code: { type: 'string' },
|
|
468
|
+
session: { type: 'number' },
|
|
469
|
+
limit: { type: 'number', description: 'Default 50; capped at 500.' },
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
// -------------------------------------------------------------------------
|
|
474
|
+
// Completion writer — COMP-MCP-COMPLETION
|
|
475
|
+
// -------------------------------------------------------------------------
|
|
476
|
+
{
|
|
477
|
+
name: 'record_completion',
|
|
478
|
+
description: 'Record a completion bound to a commit SHA. Stores in feature.json completions[]; idempotent on (feature_code, commit_sha); when set_status:true (default) also flips status to COMPLETE via set_feature_status. Audit append best-effort. Status-flip failure rethrows as STATUS_FLIP_AFTER_COMPLETION_RECORDED with err.cause; the completion record is still persisted.',
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: 'object',
|
|
481
|
+
required: ['feature_code', 'commit_sha', 'tests_pass', 'files_changed'],
|
|
482
|
+
properties: {
|
|
483
|
+
feature_code: { type: 'string' },
|
|
484
|
+
commit_sha: { type: 'string', description: 'Full 40-char hex SHA (Decision 9). Short prefixes are rejected on write. Stored verbatim; commit_sha_short is derived for display only.' },
|
|
485
|
+
tests_pass: { type: 'boolean' },
|
|
486
|
+
files_changed: { type: 'array', items: { type: 'string' } },
|
|
487
|
+
notes: { type: 'string' },
|
|
488
|
+
set_status: { type: 'boolean', description: 'Default true. When true, flips status to COMPLETE via set_feature_status.' },
|
|
489
|
+
force: { type: 'boolean', description: 'If true and a record with the same (feature_code, commit_sha) exists, replace it in place.' },
|
|
490
|
+
idempotency_key: { type: 'string' },
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'get_completions',
|
|
496
|
+
description: 'Read completion records from feature.json files. Filter by feature_code (exact), commit_sha (short or full prefix), or since (shorthand or ISO date).',
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: 'object',
|
|
499
|
+
properties: {
|
|
500
|
+
feature_code: { type: 'string' },
|
|
501
|
+
commit_sha: { type: 'string' },
|
|
502
|
+
since: { type: 'string' },
|
|
503
|
+
limit: { type: 'number', description: 'Default 50; capped at 500.' },
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
261
507
|
];
|
|
262
508
|
|
|
263
509
|
// ---------------------------------------------------------------------------
|
|
@@ -295,6 +541,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
295
541
|
case 'scaffold_feature': result = toolScaffoldFeature(args); break;
|
|
296
542
|
case 'approve_gate': result = await toolApproveGate(args); break;
|
|
297
543
|
case 'get_pending_gates': result = toolGetPendingGates(args); break;
|
|
544
|
+
case 'add_roadmap_entry': result = await toolAddRoadmapEntry(args); break;
|
|
545
|
+
case 'set_feature_status': result = await toolSetFeatureStatus(args); break;
|
|
546
|
+
case 'roadmap_diff': result = await toolRoadmapDiff(args); break;
|
|
547
|
+
case 'link_artifact': result = await toolLinkArtifact(args); break;
|
|
548
|
+
case 'link_features': result = await toolLinkFeatures(args); break;
|
|
549
|
+
case 'get_feature_artifacts': result = await toolGetFeatureArtifacts(args); break;
|
|
550
|
+
case 'get_feature_links': result = await toolGetFeatureLinks(args); break;
|
|
551
|
+
case 'add_changelog_entry': result = await toolAddChangelogEntry(args); break;
|
|
552
|
+
case 'get_changelog_entries': result = await toolGetChangelogEntries(args); break;
|
|
553
|
+
case 'write_journal_entry': result = await toolWriteJournalEntry(args); break;
|
|
554
|
+
case 'get_journal_entries': result = await toolGetJournalEntries(args); break;
|
|
555
|
+
case 'record_completion': result = await toolRecordCompletion(args); break;
|
|
556
|
+
case 'get_completions': result = await toolGetCompletions(args); break;
|
|
298
557
|
// agent_run removed — STRAT-DEDUP-AGENTRUN v1. Use mcp__stratum__stratum_agent_run.
|
|
299
558
|
default:
|
|
300
559
|
return {
|
|
@@ -306,8 +565,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
306
565
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
307
566
|
};
|
|
308
567
|
} catch (err) {
|
|
568
|
+
// Surface typed error codes (e.g. INVALID_INPUT, CHANGELOG_FORMAT) when
|
|
569
|
+
// tools attach them, so MCP callers can branch deterministically. Plain
|
|
570
|
+
// errors fall back to the original "Error: <message>" shape.
|
|
571
|
+
// When err.cause is an Error-shaped object, append it so callers can
|
|
572
|
+
// distinguish partial-write sub-errors (e.g. rollback succeeded vs failed).
|
|
573
|
+
let text = err && err.code
|
|
574
|
+
? `Error [${err.code}]: ${err.message}`
|
|
575
|
+
: `Error: ${err.message}`;
|
|
576
|
+
if (err && err.cause && typeof err.cause.message === 'string') {
|
|
577
|
+
text += err.cause.code
|
|
578
|
+
? `\n Caused by [${err.cause.code}]: ${err.cause.message}`
|
|
579
|
+
: `\n Caused by: ${err.cause.message}`;
|
|
580
|
+
}
|
|
309
581
|
return {
|
|
310
|
-
content: [{ type: 'text', text
|
|
582
|
+
content: [{ type: 'text', text }],
|
|
311
583
|
isError: true,
|
|
312
584
|
};
|
|
313
585
|
}
|