@smartmemory/compose 0.2.2-beta → 0.2.4-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.
Files changed (70) hide show
  1. package/contracts/checkpoint.schema.json +85 -0
  2. package/dist/assets/{App-QGVt8tH2.js → App-j8fWZcGr.js} +5 -5
  3. package/dist/assets/{arc-yX1Dy9Ls.js → arc-BFqOo_jJ.js} +1 -1
  4. package/dist/assets/{architectureDiagram-3BPJPVTR-BhtVN7Go.js → architectureDiagram-3BPJPVTR-D722w0RE.js} +1 -1
  5. package/dist/assets/{blockDiagram-GPEHLZMM-Do_uWvAL.js → blockDiagram-GPEHLZMM-B4w0mOAJ.js} +1 -1
  6. package/dist/assets/{c4Diagram-AAUBKEIU-DhjfNEZ_.js → c4Diagram-AAUBKEIU-D6LE8-j8.js} +1 -1
  7. package/dist/assets/channel-BD-5_hPW.js +1 -0
  8. package/dist/assets/{chunk-2J33WTMH-ZLuzLSr5.js → chunk-2J33WTMH-CrazA7xu.js} +1 -1
  9. package/dist/assets/{chunk-4BX2VUAB-BkfYx42O.js → chunk-4BX2VUAB-Cp90GiCM.js} +1 -1
  10. package/dist/assets/{chunk-55IACEB6-UGWHui37.js → chunk-55IACEB6-Bnais1SK.js} +1 -1
  11. package/dist/assets/{chunk-727SXJPM-DENLKVEd.js → chunk-727SXJPM-kD07Sqp5.js} +1 -1
  12. package/dist/assets/{chunk-AQP2D5EJ-BV-AIq0h.js → chunk-AQP2D5EJ-DmIxhJc8.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-BO5q1BN_.js → chunk-FMBD7UC4-Jti_und8.js} +1 -1
  14. package/dist/assets/{chunk-ND2GUHAM-rAAtIsqf.js → chunk-ND2GUHAM-Ipx3noKz.js} +1 -1
  15. package/dist/assets/{chunk-QZHKN3VN-DmZo3sCU.js → chunk-QZHKN3VN-CeblRnPF.js} +1 -1
  16. package/dist/assets/classDiagram-4FO5ZUOK-mSW5R7DY.js +1 -0
  17. package/dist/assets/classDiagram-v2-Q7XG4LA2-mSW5R7DY.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-BlBShGhI.js → cose-bilkent-S5V4N54A-fNQlSmHt.js} +1 -1
  19. package/dist/assets/{dagre-BM42HDAG-urcL7_B8.js → dagre-BM42HDAG-D27D6YAL.js} +1 -1
  20. package/dist/assets/{diagram-2AECGRRQ-DlfCvqLR.js → diagram-2AECGRRQ-CtXeohzN.js} +1 -1
  21. package/dist/assets/{diagram-5GNKFQAL-h5gTsYDo.js → diagram-5GNKFQAL-C_BqZkx0.js} +1 -1
  22. package/dist/assets/{diagram-KO2AKTUF-BbEbRrVo.js → diagram-KO2AKTUF-B29ynQz4.js} +1 -1
  23. package/dist/assets/{diagram-LMA3HP47-jog00Zl2.js → diagram-LMA3HP47-DAYJMc2I.js} +1 -1
  24. package/dist/assets/{diagram-OG6HWLK6-B0JVsR6S.js → diagram-OG6HWLK6-CBJMis3l.js} +1 -1
  25. package/dist/assets/{erDiagram-TEJ5UH35-DkUnalKg.js → erDiagram-TEJ5UH35-nd3GWiPn.js} +1 -1
  26. package/dist/assets/{flowDiagram-I6XJVG4X-DewNd_kM.js → flowDiagram-I6XJVG4X-HFUno_nV.js} +1 -1
  27. package/dist/assets/{ganttDiagram-6RSMTGT7-DzDBcVj5.js → ganttDiagram-6RSMTGT7-CPPAAjwR.js} +1 -1
  28. package/dist/assets/{gitGraphDiagram-PVQCEYII-D0CX5WgP.js → gitGraphDiagram-PVQCEYII-NBq1F6K2.js} +1 -1
  29. package/dist/assets/{index-D4GJb_6L.js → index-uHKnp74B.js} +2 -2
  30. package/dist/assets/{infoDiagram-5YYISTIA-B1zzuW9l.js → infoDiagram-5YYISTIA-D-TOBtCq.js} +1 -1
  31. package/dist/assets/{ishikawaDiagram-YF4QCWOH-3hFmuv1F.js → ishikawaDiagram-YF4QCWOH-nXOztZiZ.js} +1 -1
  32. package/dist/assets/{journeyDiagram-JHISSGLW-w9c-l95A.js → journeyDiagram-JHISSGLW-Bko3tTdh.js} +1 -1
  33. package/dist/assets/{kanban-definition-UN3LZRKU-9cL90JL0.js → kanban-definition-UN3LZRKU-1e-7i8st.js} +1 -1
  34. package/dist/assets/{linear-DyDb5wz8.js → linear-Dx5ZJB7F.js} +1 -1
  35. package/dist/assets/{mindmap-definition-RKZ34NQL-DBQqsZiD.js → mindmap-definition-RKZ34NQL-CNwNkDqN.js} +1 -1
  36. package/dist/assets/{pieDiagram-4H26LBE5-BbIHZku5.js → pieDiagram-4H26LBE5-C5fvCej-.js} +1 -1
  37. package/dist/assets/{quadrantDiagram-W4KKPZXB-DEQSG_lM.js → quadrantDiagram-W4KKPZXB-4NoQsF61.js} +1 -1
  38. package/dist/assets/{requirementDiagram-4Y6WPE33-BeVnwIwF.js → requirementDiagram-4Y6WPE33-q5WxB9LO.js} +1 -1
  39. package/dist/assets/{sankeyDiagram-5OEKKPKP-Be-ROw_I.js → sankeyDiagram-5OEKKPKP-DlQNB367.js} +1 -1
  40. package/dist/assets/{sequenceDiagram-3UESZ5HK-E-tnxahu.js → sequenceDiagram-3UESZ5HK-BzHclOKt.js} +1 -1
  41. package/dist/assets/{stateDiagram-AJRCARHV-3rgFN7hL.js → stateDiagram-AJRCARHV-BvWRI9zK.js} +1 -1
  42. package/dist/assets/stateDiagram-v2-BHNVJYJU-CDlF0VA8.js +1 -0
  43. package/dist/assets/{timeline-definition-PNZ67QCA-Dcs4QFbE.js → timeline-definition-PNZ67QCA-j2wKjAti.js} +1 -1
  44. package/dist/assets/{vennDiagram-CIIHVFJN-BstUQ900.js → vennDiagram-CIIHVFJN-B77g7htC.js} +1 -1
  45. package/dist/assets/{wardley-L42UT6IY-CO77hXwj.js → wardley-L42UT6IY-83Im2mo2.js} +1 -1
  46. package/dist/assets/{wardleyDiagram-YWT4CUSO-BvrP3shF.js → wardleyDiagram-YWT4CUSO-CK-XB-bO.js} +1 -1
  47. package/dist/assets/{xychartDiagram-2RQKCTM6-Btu4JcQO.js → xychartDiagram-2RQKCTM6-D42FcVOY.js} +1 -1
  48. package/dist/index.html +1 -1
  49. package/lib/checkpoint/anchor.js +66 -0
  50. package/lib/checkpoint/atomic.js +83 -0
  51. package/lib/checkpoint/checkpoint-writer.js +131 -0
  52. package/lib/checkpoint/fingerprint.js +145 -0
  53. package/lib/checkpoint/git.js +58 -0
  54. package/lib/checkpoint/prompts.js +206 -0
  55. package/lib/checkpoint/reconciler.js +207 -0
  56. package/lib/checkpoint/render.js +107 -0
  57. package/lib/checkpoint/store/index.js +67 -0
  58. package/lib/checkpoint/store/jsonl.js +80 -0
  59. package/package.json +1 -1
  60. package/server/compose-mcp-tools.js +30 -0
  61. package/server/compose-mcp.js +40 -0
  62. package/server/lifecycle-guard.js +225 -0
  63. package/server/session-routes.js +65 -0
  64. package/server/stratum-client.js +140 -0
  65. package/server/vision-routes.js +68 -20
  66. package/server/vision-server.js +2 -0
  67. package/dist/assets/channel-D6hNrRZ2.js +0 -1
  68. package/dist/assets/classDiagram-4FO5ZUOK-DvsVLUph.js +0 -1
  69. package/dist/assets/classDiagram-v2-Q7XG4LA2-DvsVLUph.js +0 -1
  70. package/dist/assets/stateDiagram-v2-BHNVJYJU-DY_OtnIg.js +0 -1
@@ -0,0 +1,207 @@
1
+ /**
2
+ * lib/checkpoint/reconciler.js — COMP-RESUME slice S8: resume reconciliation.
3
+ *
4
+ * CORRECTED BOUNDARY (per the Codex boundary review, blueprint.md "Boundary
5
+ * review" note + design.md Decision 5):
6
+ *
7
+ * `reconcile()` is DETERMINISTIC. It does NOT:
8
+ * - write to the CheckpointStore,
9
+ * - mutate any persistent DB (vision-state, etc.),
10
+ * - call an LLM / agent connector.
11
+ *
12
+ * It computes and RETURNS a `ReconcileResult`. Persistence and the
13
+ * reconciliation agent-run happen at the CALLER:
14
+ * - the route (S10 `POST /api/session/bind/reconcile`) applies
15
+ * `result.lifecycleMutations` via `store.updateLifecycle` and persists,
16
+ * - the orchestrator (S9 `compose_resume` consumer) runs the reconciliation
17
+ * agent with `result.reconcilePrompt` when `action === 'needs-sync'`, then
18
+ * writes the synced checkpoint and re-decides via `decideAfterSync`.
19
+ *
20
+ * `reconcile()` does NOT mutate the passed-in `item` either — it only emits
21
+ * plain mutation descriptors in `result.lifecycleMutations`. The route is the
22
+ * SINGLE application point (it calls `appendPhaseHistory`, producing the correct
23
+ * dual-shape entry, then persists). Mutating here as well double-applied and
24
+ * malformed the entry (Codex impl review #1). It must not perform broad state
25
+ * surgery — only the well-defined backfill below.
26
+ *
27
+ * @see docs/features/COMP-RESUME/blueprint.md (slice S8 + Boundary review note)
28
+ * @see docs/features/COMP-RESUME/design.md (Decision 5)
29
+ *
30
+ * ReconcileResult = {
31
+ * action: 'resume' | 'needs-sync' | 'gate',
32
+ * drift: 'clean' | 'advanced' | 'diverged',
33
+ * lifecycleMutations: Array<LifecycleMutation>,
34
+ * checkpoint: object | null, // the latest recorded checkpoint (or null)
35
+ * rendered: string | null, // renderCheckpoint(checkpoint) — human-readable resume view
36
+ * nextStep?: string | null, // present on the 'resume' path
37
+ * reconcilePrompt?: string, // present on the 'needs-sync' path
38
+ * gatePayload?: object, // reserved for the orchestrator's gate path
39
+ * }
40
+ *
41
+ * LifecycleMutation (plain descriptor the caller applies + persists):
42
+ * { type: 'phaseHistory.append', entry: { from, to, outcome, timestamp } }
43
+ */
44
+
45
+ import { existsSync, readFileSync } from 'node:fs';
46
+ import { join } from 'node:path';
47
+ import { captureFingerprint, classify } from './fingerprint.js';
48
+ import { reconcilePrompt } from './prompts.js';
49
+ import { renderCheckpoint } from './render.js';
50
+
51
+ /**
52
+ * Read `<dataDir>/active-build.json` if present. Tolerates an absent or corrupt
53
+ * file (returns null) — a crash can leave a torn write, and the reconciler must
54
+ * degrade gracefully rather than throw.
55
+ *
56
+ * @param {string|null} dataDir
57
+ * @returns {object|null}
58
+ */
59
+ function readActiveBuild(dataDir) {
60
+ if (!dataDir) return null;
61
+ const file = join(dataDir, 'active-build.json');
62
+ if (!existsSync(file)) return null;
63
+ try {
64
+ return JSON.parse(readFileSync(file, 'utf8'));
65
+ } catch {
66
+ return null; // corrupt / partially-written — tolerate
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Rebuild derived state in memory and collect plain mutation descriptors the
72
+ * caller will apply + persist. Deliberately minimal and well-defined:
73
+ *
74
+ * - phaseHistory backfill: if `item.lifecycle.phaseHistory` is missing/empty
75
+ * AND `item.lifecycle.currentPhase` is set, surface a single
76
+ * `phaseHistory.append` for `{ from:null, to:currentPhase, outcome:'resumed' }`.
77
+ * This repairs the known gap where phaseHistory is never reconstructed after
78
+ * a crash from a prior session (design Decision 5 step 1; blueprint C5).
79
+ *
80
+ * Returns descriptors ONLY; does not mutate `item`. The route applies them via
81
+ * appendPhaseHistory and persists (single application point).
82
+ *
83
+ * @param {object} item — vision item; inspected, not mutated.
84
+ * @param {object|null} _activeBuild — parsed active-build.json (reserved for
85
+ * future derived-pointer rebuild; not used for broad surgery in v1).
86
+ * @returns {Array<object>} lifecycleMutations
87
+ */
88
+ function rebuildDerivedState(item, _activeBuild) {
89
+ const mutations = [];
90
+ const lifecycle = item && item.lifecycle;
91
+ if (!lifecycle) return mutations;
92
+
93
+ const history = lifecycle.phaseHistory;
94
+ const historyEmpty = !Array.isArray(history) || history.length === 0;
95
+ const currentPhase = lifecycle.currentPhase;
96
+
97
+ if (historyEmpty && currentPhase) {
98
+ // Return a descriptor ONLY — do NOT mutate item.lifecycle.phaseHistory here.
99
+ // The route (S10) is the single application point: it calls
100
+ // appendPhaseHistory(item, entry), which produces the correct dual-shape
101
+ // entry (legacy phase/step/enteredAt/exitedAt + new from/to/outcome/timestamp)
102
+ // and persists it. Mutating here too would double-append AND the direct push
103
+ // would omit the legacy fields existing readers expect (Codex impl review #1).
104
+ const entry = {
105
+ from: null,
106
+ to: currentPhase,
107
+ outcome: 'resumed',
108
+ timestamp: new Date().toISOString(),
109
+ };
110
+ mutations.push({ type: 'phaseHistory.append', entry });
111
+ }
112
+
113
+ return mutations;
114
+ }
115
+
116
+ /**
117
+ * Reconcile recorded intent against the live environment and return a
118
+ * ReconcileResult. Deterministic — no store writes, no DB mutation, no LLM.
119
+ *
120
+ * @param {object} args
121
+ * @param {string} args.featureCode
122
+ * @param {object} args.item — vision item (may be mutated in memory; see above).
123
+ * @param {string} args.cwd — repo / working dir to fingerprint.
124
+ * @param {string} args.featureDir — dir holding phase artifacts.
125
+ * @param {string|null} [args.composeDir] — `.compose` dir for build-stream lookup.
126
+ * @param {string|null} [args.dataDir] — `.compose/data` dir.
127
+ * @param {object} args.store — a CheckpointStore (read-only here).
128
+ * @param {number} [args.confidenceThreshold=0.6] — passed through for the caller.
129
+ * @returns {object} ReconcileResult
130
+ */
131
+ export function reconcile({
132
+ featureCode,
133
+ item,
134
+ cwd,
135
+ featureDir,
136
+ composeDir = null,
137
+ dataDir = null,
138
+ store,
139
+ confidenceThreshold = 0.6,
140
+ }) {
141
+ // 1. Rebuild derived state (in-memory, deterministic). Read active-build.json
142
+ // tolerantly; collect lifecycle mutation descriptors for the caller.
143
+ const activeBuild = readActiveBuild(dataDir);
144
+ const lifecycleMutations = rebuildDerivedState(item, activeBuild);
145
+
146
+ // 2. Load latest recorded intent (read-only). Opportunistically use
147
+ // semanticRecall if the backend advertises it; the jsonl floor does not, so
148
+ // this falls through to readLatest.
149
+ let latest = null;
150
+ const caps = typeof store.capabilities === 'function' ? store.capabilities() : new Set();
151
+ if (caps && caps.has && caps.has('semanticRecall') && typeof store.semanticRecall === 'function') {
152
+ const hits = store.semanticRecall(featureCode, '', { limit: 1 });
153
+ latest = Array.isArray(hits) && hits.length ? hits[0] : null;
154
+ } else {
155
+ latest = store.readLatest(featureCode);
156
+ }
157
+
158
+ // 3. Capture the live fingerprint and classify drift against the checkpoint's.
159
+ const live = captureFingerprint(cwd, { featureDir, composeDir, dataDir });
160
+ const drift = classify(latest?.fingerprint ?? null, live);
161
+
162
+ // A human-readable view of the recorded intent, for the resume UX (wires
163
+ // renderCheckpoint into the production path — Codex impl review #3).
164
+ const rendered = latest ? renderCheckpoint(latest) : null;
165
+
166
+ // 4. clean / advanced → resume at the recorded nextStep (no agent).
167
+ if (drift === 'clean' || drift === 'advanced') {
168
+ return {
169
+ action: 'resume',
170
+ nextStep: latest?.soft?.nextStep ?? null,
171
+ drift,
172
+ lifecycleMutations,
173
+ checkpoint: latest,
174
+ rendered,
175
+ };
176
+ }
177
+
178
+ // 5. diverged → needs-sync. The orchestrator runs the reconciliation agent
179
+ // with this prompt, then writes the synced checkpoint and calls
180
+ // decideAfterSync. reconcile itself does NOT run the agent or persist.
181
+ return {
182
+ action: 'needs-sync',
183
+ drift,
184
+ lifecycleMutations,
185
+ checkpoint: latest,
186
+ rendered,
187
+ reconcilePrompt: reconcilePrompt({
188
+ staleCheckpoint: latest,
189
+ liveFingerprint: live,
190
+ envScan: '',
191
+ }),
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Pure gate decision made by the orchestrator AFTER it has run the
197
+ * reconciliation agent (on the needs-sync path) and obtained a confidence.
198
+ * Kept here, separate from any LLM call, so the decision is unit-testable.
199
+ *
200
+ * @param {object} args
201
+ * @param {number} args.confidence — agent-reported confidence in [0,1].
202
+ * @param {number} [args.confidenceThreshold=0.6]
203
+ * @returns {'resume'|'gate'} resume when confidence >= threshold (inclusive), else gate.
204
+ */
205
+ export function decideAfterSync({ confidence, confidenceThreshold = 0.6 }) {
206
+ return confidence >= confidenceThreshold ? 'resume' : 'gate';
207
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * COMP-RESUME S11-render — render a Checkpoint to Markdown for human inspection.
3
+ *
4
+ * PURE: no fs, no git, no network, no imports from other checkpoint modules.
5
+ * Markdown is NOT the store of record (the JSONL backend is); this view exists
6
+ * only so a human can read a checkpoint (e.g. in `compose_resume` output).
7
+ *
8
+ * Anchor checkpoints (soft === null) render an explicit "(anchor — no narrative)"
9
+ * marker in place of the Intent body. Narrative checkpoints render their
10
+ * agent-authored goal/nextStep/risks. The Environment section reflects the
11
+ * deterministic fingerprint (records, never interprets).
12
+ *
13
+ * @see docs/features/COMP-RESUME/blueprint.md (slice S11)
14
+ * @see contracts/checkpoint.schema.json
15
+ */
16
+
17
+ const SHORT_SHA_LEN = 7;
18
+
19
+ /**
20
+ * Shorten a git sha for display; passes through non-sha / null values.
21
+ * @param {string|null|undefined} sha
22
+ * @returns {string}
23
+ */
24
+ function shortSha(sha) {
25
+ if (!sha || typeof sha !== 'string') return '(none)';
26
+ return sha.slice(0, SHORT_SHA_LEN);
27
+ }
28
+
29
+ /**
30
+ * Render a markdown bullet list, or a placeholder line when empty.
31
+ * @param {string[]} items
32
+ * @param {string} emptyText
33
+ * @returns {string}
34
+ */
35
+ function bulletList(items, emptyText) {
36
+ if (!Array.isArray(items) || items.length === 0) return emptyText;
37
+ return items.map((i) => `- ${i}`).join('\n');
38
+ }
39
+
40
+ /**
41
+ * Render a checkpoint to a Markdown string for human inspection.
42
+ *
43
+ * @param {object} cp - A Checkpoint (see contracts/checkpoint.schema.json).
44
+ * @returns {string} markdown
45
+ */
46
+ export function renderCheckpoint(cp) {
47
+ const checkpoint = cp || {};
48
+ const fp = checkpoint.fingerprint || {};
49
+ const git = fp.git || {};
50
+ const artifacts = fp.phaseArtifacts || {};
51
+
52
+ const lines = [];
53
+
54
+ // Heading: featureCode + phase + createdAt
55
+ const featureCode = checkpoint.featureCode ?? '(unknown feature)';
56
+ const phase = checkpoint.phase ?? '(unknown phase)';
57
+ lines.push(`# Checkpoint — ${featureCode} · ${phase}`);
58
+ lines.push('');
59
+ const trigger = checkpoint.trigger ? ` · trigger: \`${checkpoint.trigger}\`` : '';
60
+ lines.push(`_Created: ${checkpoint.createdAt ?? '(unknown)'}${trigger}_`);
61
+ lines.push('');
62
+
63
+ // Intent section
64
+ lines.push('## Intent');
65
+ if (checkpoint.soft) {
66
+ const soft = checkpoint.soft;
67
+ lines.push(`- **Goal:** ${soft.goal ?? ''}`);
68
+ lines.push(`- **Next step:** ${soft.nextStep ?? ''}`);
69
+ lines.push('- **Risks:**');
70
+ lines.push(bulletList(soft.risks, ' - (none recorded)'));
71
+ } else {
72
+ lines.push('(anchor — no narrative)');
73
+ }
74
+ lines.push('');
75
+
76
+ // Environment section (deterministic fingerprint)
77
+ lines.push('## Environment');
78
+ lines.push(`- **Git head:** \`${shortSha(git.head)}\``);
79
+ lines.push(`- **Branch:** ${git.branch ?? '(none)'}`);
80
+ lines.push(`- **Tree:** ${git.dirty ? 'dirty' : 'clean'}`);
81
+
82
+ // Present phaseArtifacts only (skip null/empty)
83
+ const present = [];
84
+ if (artifacts.design) present.push(`design: \`${artifacts.design}\``);
85
+ if (artifacts.blueprint) present.push(`blueprint: \`${artifacts.blueprint}\``);
86
+ if (artifacts.plan) present.push(`plan: \`${artifacts.plan}\``);
87
+ if (Array.isArray(artifacts.implementFiles) && artifacts.implementFiles.length) {
88
+ present.push(`implement: ${artifacts.implementFiles.map((f) => `\`${f}\``).join(', ')}`);
89
+ }
90
+ if (Array.isArray(artifacts.contracts) && artifacts.contracts.length) {
91
+ present.push(`contracts: ${artifacts.contracts.map((f) => `\`${f}\``).join(', ')}`);
92
+ }
93
+ lines.push('- **Artifacts present:**');
94
+ lines.push(present.length ? present.map((p) => ` - ${p}`).join('\n') : ' - (none)');
95
+
96
+ if (fp.testRef) {
97
+ lines.push(`- **Test output (raw):** \`${fp.testRef}\``);
98
+ }
99
+
100
+ // Confidence line — only when present (resume-sync checkpoints).
101
+ if (typeof checkpoint.confidence === 'number') {
102
+ lines.push('');
103
+ lines.push(`**Confidence:** ${checkpoint.confidence}`);
104
+ }
105
+
106
+ return lines.join('\n');
107
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * lib/checkpoint/store/index.js — CheckpointStore interface + backend registry.
3
+ *
4
+ * COMP-RESUME slice S4. The base contract every backend implements:
5
+ * - write(cp) → persist a Checkpoint, idempotent on cp.id; returns the cp
6
+ * - readLatest(featureCode) → most recent Checkpoint or null
7
+ * - list(featureCode, {limit})→ Checkpoints newest-first, sliced to limit
8
+ * - capabilities() → Set<string> of optional capabilities
9
+ *
10
+ * Optional (advertised via capabilities(), absent on the jsonl floor):
11
+ * - semanticRecall, temporalRange, procedureMatch
12
+ *
13
+ * Backend selection defaults to 'jsonl' (read from loadProjectConfig().checkpoint?.backend
14
+ * by the caller). 'smartmemory' and 'memory-pointer' are registered seams that
15
+ * throw NOT_IMPLEMENTED in v1.
16
+ */
17
+
18
+ import { JsonlCheckpointBackend } from './jsonl.js';
19
+
20
+ /**
21
+ * Construct a checkpoint store backend.
22
+ *
23
+ * @param {string} [backendId='jsonl'] — one of 'jsonl' | 'smartmemory' | 'memory-pointer'
24
+ * @param {{ dataDir?: string }} [opts]
25
+ * @returns {object} a CheckpointStore
26
+ */
27
+ export function createCheckpointStore(backendId = 'jsonl', { dataDir } = {}) {
28
+ switch (backendId) {
29
+ case 'jsonl':
30
+ return new JsonlCheckpointBackend({ dataDir });
31
+
32
+ case 'smartmemory':
33
+ // ── SmartMemory backend — intended interface mapping (NOT built in v1) ──
34
+ //
35
+ // When implemented, map the CheckpointStore contract onto the SmartMemory SDK:
36
+ //
37
+ // write(cp) → ReasoningTracesAPI.store({ trace, artifactIds })
38
+ // (cp.soft + fingerprint serialize into `trace`;
39
+ // returned trace/artifact ids fill cp.artifactIds)
40
+ // readLatest(featureCode) → TemporalAPI (most-recent trace for the feature scope)
41
+ // list(featureCode, {limit}) → TemporalAPI (temporal range / latest-N for the scope)
42
+ // semanticRecall({query,limit}) → query({ query, limit }) (capability: 'semanticRecall')
43
+ // procedureMatch(...) → ProcedureMatchAPI (capability: 'procedureMatch')
44
+ //
45
+ // Requires adding `smart-memory-sdk-js` as an OPTIONAL peer dep and a LAZY
46
+ // (dynamic) import here so the dep is only loaded when this backend is
47
+ // actually selected. Deliberately NOT done in v1 (SmartMemory was scoped
48
+ // out — see blueprint correction C2).
49
+ throw Object.assign(
50
+ new Error('SmartMemory checkpoint backend not implemented'),
51
+ { code: 'NOT_IMPLEMENTED' }
52
+ );
53
+
54
+ case 'memory-pointer':
55
+ // Registered seam, deferred. Intended to store only a pointer/ref to an
56
+ // external memory record rather than the full checkpoint inline.
57
+ throw Object.assign(
58
+ new Error('memory-pointer checkpoint backend not implemented'),
59
+ { code: 'NOT_IMPLEMENTED' }
60
+ );
61
+
62
+ default:
63
+ throw new Error(
64
+ `Unknown checkpoint backend '${backendId}'. Valid ids: jsonl, smartmemory, memory-pointer.`
65
+ );
66
+ }
67
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * lib/checkpoint/store/jsonl.js — the floor checkpoint backend.
3
+ *
4
+ * COMP-RESUME slices S4/S5 (correction C4): checkpoints are stored in
5
+ * feature-scoped JSONL files `checkpoints-<featureCode>.jsonl` under the data
6
+ * dir. Feature-scoping keeps `readLatest`/`list` cheap (no global filter) while
7
+ * each record still carries its own `featureCode`.
8
+ *
9
+ * This backend is "floor-only": it implements the base CheckpointStore contract
10
+ * (write / readLatest / list / capabilities) and advertises no optional
11
+ * capabilities. Richer backends (e.g. SmartMemory) layer semanticRecall /
12
+ * temporalRange / procedureMatch on top — see ./index.js.
13
+ */
14
+
15
+ import { join } from 'node:path';
16
+ import { appendJsonl, readJsonl } from '../atomic.js';
17
+
18
+ export class JsonlCheckpointBackend {
19
+ /**
20
+ * @param {{ dataDir: string }} opts — dataDir is passed per-construction so the
21
+ * backend honors project switches (server re-creates the store on switch).
22
+ */
23
+ constructor({ dataDir } = {}) {
24
+ if (!dataDir) throw new Error('JsonlCheckpointBackend requires { dataDir }');
25
+ this.dataDir = dataDir;
26
+ }
27
+
28
+ /** Absolute path to the JSONL file for a given feature. */
29
+ _file(featureCode) {
30
+ return join(this.dataDir, `checkpoints-${featureCode}.jsonl`);
31
+ }
32
+
33
+ /**
34
+ * Append a checkpoint. Idempotent on `cp.id`: if a record with the same id
35
+ * already exists in this feature's file, the existing record is returned and
36
+ * nothing is appended. Otherwise the cp is appended and returned.
37
+ *
38
+ * @param {object} cp — a Checkpoint (must have .id and .featureCode)
39
+ * @returns {object} the persisted checkpoint
40
+ */
41
+ write(cp) {
42
+ if (!cp || !cp.id) throw new Error('checkpoint requires an id');
43
+ if (!cp.featureCode) throw new Error('checkpoint requires a featureCode');
44
+ const file = this._file(cp.featureCode);
45
+
46
+ // Idempotency scan. Volume per feature is bounded, so a linear scan is fine.
47
+ const existing = readJsonl(file);
48
+ const prior = existing.find((r) => r.id === cp.id);
49
+ if (prior) return prior;
50
+
51
+ appendJsonl(file, cp);
52
+ return cp;
53
+ }
54
+
55
+ /**
56
+ * @param {string} featureCode
57
+ * @returns {object|null} the most-recently-written checkpoint, or null.
58
+ */
59
+ readLatest(featureCode) {
60
+ const all = readJsonl(this._file(featureCode));
61
+ return all.length ? all[all.length - 1] : null;
62
+ }
63
+
64
+ /**
65
+ * @param {string} featureCode
66
+ * @param {{ limit?: number }} [opts]
67
+ * @returns {object[]} checkpoints for the feature, NEWEST-FIRST, sliced to limit.
68
+ */
69
+ list(featureCode, { limit } = {}) {
70
+ const newestFirst = readJsonl(this._file(featureCode)).reverse();
71
+ return limit !== undefined ? newestFirst.slice(0, limit) : newestFirst;
72
+ }
73
+
74
+ /**
75
+ * @returns {Set<string>} optional capabilities — empty for the jsonl floor.
76
+ */
77
+ capabilities() {
78
+ return new Set();
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.2.2-beta",
3
+ "version": "0.2.4-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",
@@ -238,6 +238,36 @@ export async function toolProposeFollowup(args) {
238
238
  return proposeFollowup(getTargetRoot(), args);
239
239
  }
240
240
 
241
+ // ---------------------------------------------------------------------------
242
+ // Checkpoints / resume — COMP-RESUME
243
+ // ---------------------------------------------------------------------------
244
+
245
+ // write_checkpoint reads/writes directly from disk so it works even when the
246
+ // Compose server is down (same stance as the rest of this file).
247
+ export async function toolWriteCheckpoint(args) {
248
+ const { writeCheckpoint } = await import('../lib/checkpoint/checkpoint-writer.js');
249
+ return writeCheckpoint(getTargetRoot(), args);
250
+ }
251
+
252
+ // compose_resume HTTP-delegates: reconcile must run server-side where the live
253
+ // vision item / lifecycle state and broadcasts exist (mirrors toolBindSession).
254
+ export async function toolComposeResume({ featureCode }) {
255
+ let result;
256
+ try {
257
+ result = await _httpRequest('POST', '/api/session/bind/reconcile', { featureCode });
258
+ } catch (err) {
259
+ throw new Error(`Compose server unreachable: ${err.message}`);
260
+ }
261
+ const { status, body } = result;
262
+ if (status >= 400) {
263
+ const errMsg = (body && typeof body === 'object' && body.error)
264
+ ? body.error
265
+ : `HTTP ${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`;
266
+ throw new Error(errMsg);
267
+ }
268
+ return body;
269
+ }
270
+
241
271
  // ---------------------------------------------------------------------------
242
272
  // Changelog writer — COMP-MCP-CHANGELOG-WRITER
243
273
  // ---------------------------------------------------------------------------
@@ -61,6 +61,8 @@ import {
61
61
  toolValidateProject,
62
62
  toolSetWorkspace,
63
63
  toolGetWorkspace,
64
+ toolWriteCheckpoint,
65
+ toolComposeResume,
64
66
  _getBinding,
65
67
  } from './compose-mcp-tools.js';
66
68
  import { switchProject, getTargetRoot } from './project-root.js';
@@ -578,6 +580,42 @@ const TOOLS = [
578
580
  },
579
581
  },
580
582
  },
583
+ {
584
+ name: 'write_checkpoint',
585
+ description: 'COMP-RESUME: write a durable build checkpoint anchored to a deterministic environment fingerprint. Omit `soft` for a cheap anchor checkpoint — the response then includes a `scribePrompt` you can answer (returning {goal,nextStep,risks} anchored to the fingerprint) and re-submit as `soft` for a narrative checkpoint. Pass `soft` directly to skip that. Returns {checkpoint, scribePrompt}. Direct-to-disk (works when the server is down).',
586
+ inputSchema: {
587
+ type: 'object',
588
+ required: ['featureCode', 'trigger'],
589
+ properties: {
590
+ featureCode: { type: 'string' },
591
+ trigger: { type: 'string', enum: ['phase-transition', 'pre-risky-action', 'iteration-complete', 'gate-resolution', 'manual', 'resume-sync'] },
592
+ phase: { type: 'string', description: 'Lifecycle phase label; falls back to feature.json then "unknown".' },
593
+ soft: {
594
+ type: 'object',
595
+ description: 'Narrative intent; omit for an anchor checkpoint. Every factual claim should reference a fingerprint anchor.',
596
+ required: ['goal', 'nextStep'],
597
+ properties: {
598
+ goal: { type: 'string' },
599
+ nextStep: { type: 'string' },
600
+ risks: { type: 'array', items: { type: 'string' } },
601
+ },
602
+ },
603
+ flowId: { type: 'string', description: 'Stratum flow id, if any.' },
604
+ confidence: { type: 'number', description: 'Only on resume-sync checkpoints (0..1).' },
605
+ },
606
+ },
607
+ },
608
+ {
609
+ name: 'compose_resume',
610
+ description: 'COMP-RESUME: reconcile a build against ground-truth environment state and return the resume decision. Rebuilds derived state, classifies drift (clean/advanced/diverged), and returns action resume|needs-sync|gate. On needs-sync the caller runs the reconciliation agent with the returned prompt. Requires the Compose server (reconciles live lifecycle state).',
611
+ inputSchema: {
612
+ type: 'object',
613
+ required: ['featureCode'],
614
+ properties: {
615
+ featureCode: { type: 'string' },
616
+ },
617
+ },
618
+ },
581
619
  ];
582
620
 
583
621
  // ---------------------------------------------------------------------------
@@ -639,6 +677,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
639
677
  case 'validate_feature': result = await toolValidateFeature(args); break;
640
678
  case 'validate_project': result = await toolValidateProject(args); break;
641
679
  case 'propose_followup': result = await toolProposeFollowup(args); break;
680
+ case 'write_checkpoint': result = await toolWriteCheckpoint(args); break;
681
+ case 'compose_resume': result = await toolComposeResume(args); break;
642
682
  // agent_run removed — STRAT-DEDUP-AGENTRUN v1. Use mcp__stratum__stratum_agent_run.
643
683
  default:
644
684
  return {