@smartmemory/compose 0.1.5-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/changelog-writer.js +647 -0
- package/lib/completion-writer.js +465 -0
- package/lib/feature-writer.js +324 -4
- package/lib/journal-writer.js +928 -0
- package/package.json +5 -1
- package/server/compose-mcp-tools.js +62 -0
- package/server/compose-mcp.js +216 -1
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",
|
|
@@ -216,6 +216,68 @@ export async function toolRoadmapDiff(args) {
|
|
|
216
216
|
return roadmapDiff(getTargetRoot(), args);
|
|
217
217
|
}
|
|
218
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
|
+
|
|
219
281
|
export async function toolBindSession({ featureCode }) {
|
|
220
282
|
const postData = JSON.stringify({ featureCode });
|
|
221
283
|
return new Promise((resolve, reject) => {
|
package/server/compose-mcp.js
CHANGED
|
@@ -46,6 +46,16 @@ import {
|
|
|
46
46
|
toolAddRoadmapEntry,
|
|
47
47
|
toolSetFeatureStatus,
|
|
48
48
|
toolRoadmapDiff,
|
|
49
|
+
toolLinkArtifact,
|
|
50
|
+
toolLinkFeatures,
|
|
51
|
+
toolGetFeatureArtifacts,
|
|
52
|
+
toolGetFeatureLinks,
|
|
53
|
+
toolAddChangelogEntry,
|
|
54
|
+
toolGetChangelogEntries,
|
|
55
|
+
toolWriteJournalEntry,
|
|
56
|
+
toolGetJournalEntries,
|
|
57
|
+
toolRecordCompletion,
|
|
58
|
+
toolGetCompletions,
|
|
49
59
|
} from './compose-mcp-tools.js';
|
|
50
60
|
|
|
51
61
|
// ---------------------------------------------------------------------------
|
|
@@ -312,6 +322,188 @@ const TOOLS = [
|
|
|
312
322
|
},
|
|
313
323
|
},
|
|
314
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
|
+
},
|
|
315
507
|
];
|
|
316
508
|
|
|
317
509
|
// ---------------------------------------------------------------------------
|
|
@@ -352,6 +544,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
352
544
|
case 'add_roadmap_entry': result = await toolAddRoadmapEntry(args); break;
|
|
353
545
|
case 'set_feature_status': result = await toolSetFeatureStatus(args); break;
|
|
354
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;
|
|
355
557
|
// agent_run removed — STRAT-DEDUP-AGENTRUN v1. Use mcp__stratum__stratum_agent_run.
|
|
356
558
|
default:
|
|
357
559
|
return {
|
|
@@ -363,8 +565,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
363
565
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
364
566
|
};
|
|
365
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
|
+
}
|
|
366
581
|
return {
|
|
367
|
-
content: [{ type: 'text', text
|
|
582
|
+
content: [{ type: 'text', text }],
|
|
368
583
|
isError: true,
|
|
369
584
|
};
|
|
370
585
|
}
|