@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.1.5-beta",
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) => {
@@ -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: `Error: ${err.message}` }],
582
+ content: [{ type: 'text', text }],
368
583
  isError: true,
369
584
  };
370
585
  }