@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.
- package/contracts/checkpoint.schema.json +85 -0
- package/dist/assets/{App-QGVt8tH2.js → App-j8fWZcGr.js} +5 -5
- package/dist/assets/{arc-yX1Dy9Ls.js → arc-BFqOo_jJ.js} +1 -1
- package/dist/assets/{architectureDiagram-3BPJPVTR-BhtVN7Go.js → architectureDiagram-3BPJPVTR-D722w0RE.js} +1 -1
- package/dist/assets/{blockDiagram-GPEHLZMM-Do_uWvAL.js → blockDiagram-GPEHLZMM-B4w0mOAJ.js} +1 -1
- package/dist/assets/{c4Diagram-AAUBKEIU-DhjfNEZ_.js → c4Diagram-AAUBKEIU-D6LE8-j8.js} +1 -1
- package/dist/assets/channel-BD-5_hPW.js +1 -0
- package/dist/assets/{chunk-2J33WTMH-ZLuzLSr5.js → chunk-2J33WTMH-CrazA7xu.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-BkfYx42O.js → chunk-4BX2VUAB-Cp90GiCM.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-UGWHui37.js → chunk-55IACEB6-Bnais1SK.js} +1 -1
- package/dist/assets/{chunk-727SXJPM-DENLKVEd.js → chunk-727SXJPM-kD07Sqp5.js} +1 -1
- package/dist/assets/{chunk-AQP2D5EJ-BV-AIq0h.js → chunk-AQP2D5EJ-DmIxhJc8.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BO5q1BN_.js → chunk-FMBD7UC4-Jti_und8.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-rAAtIsqf.js → chunk-ND2GUHAM-Ipx3noKz.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DmZo3sCU.js → chunk-QZHKN3VN-CeblRnPF.js} +1 -1
- package/dist/assets/classDiagram-4FO5ZUOK-mSW5R7DY.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-mSW5R7DY.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BlBShGhI.js → cose-bilkent-S5V4N54A-fNQlSmHt.js} +1 -1
- package/dist/assets/{dagre-BM42HDAG-urcL7_B8.js → dagre-BM42HDAG-D27D6YAL.js} +1 -1
- package/dist/assets/{diagram-2AECGRRQ-DlfCvqLR.js → diagram-2AECGRRQ-CtXeohzN.js} +1 -1
- package/dist/assets/{diagram-5GNKFQAL-h5gTsYDo.js → diagram-5GNKFQAL-C_BqZkx0.js} +1 -1
- package/dist/assets/{diagram-KO2AKTUF-BbEbRrVo.js → diagram-KO2AKTUF-B29ynQz4.js} +1 -1
- package/dist/assets/{diagram-LMA3HP47-jog00Zl2.js → diagram-LMA3HP47-DAYJMc2I.js} +1 -1
- package/dist/assets/{diagram-OG6HWLK6-B0JVsR6S.js → diagram-OG6HWLK6-CBJMis3l.js} +1 -1
- package/dist/assets/{erDiagram-TEJ5UH35-DkUnalKg.js → erDiagram-TEJ5UH35-nd3GWiPn.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-DewNd_kM.js → flowDiagram-I6XJVG4X-HFUno_nV.js} +1 -1
- package/dist/assets/{ganttDiagram-6RSMTGT7-DzDBcVj5.js → ganttDiagram-6RSMTGT7-CPPAAjwR.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PVQCEYII-D0CX5WgP.js → gitGraphDiagram-PVQCEYII-NBq1F6K2.js} +1 -1
- package/dist/assets/{index-D4GJb_6L.js → index-uHKnp74B.js} +2 -2
- package/dist/assets/{infoDiagram-5YYISTIA-B1zzuW9l.js → infoDiagram-5YYISTIA-D-TOBtCq.js} +1 -1
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-3hFmuv1F.js → ishikawaDiagram-YF4QCWOH-nXOztZiZ.js} +1 -1
- package/dist/assets/{journeyDiagram-JHISSGLW-w9c-l95A.js → journeyDiagram-JHISSGLW-Bko3tTdh.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-9cL90JL0.js → kanban-definition-UN3LZRKU-1e-7i8st.js} +1 -1
- package/dist/assets/{linear-DyDb5wz8.js → linear-Dx5ZJB7F.js} +1 -1
- package/dist/assets/{mindmap-definition-RKZ34NQL-DBQqsZiD.js → mindmap-definition-RKZ34NQL-CNwNkDqN.js} +1 -1
- package/dist/assets/{pieDiagram-4H26LBE5-BbIHZku5.js → pieDiagram-4H26LBE5-C5fvCej-.js} +1 -1
- package/dist/assets/{quadrantDiagram-W4KKPZXB-DEQSG_lM.js → quadrantDiagram-W4KKPZXB-4NoQsF61.js} +1 -1
- package/dist/assets/{requirementDiagram-4Y6WPE33-BeVnwIwF.js → requirementDiagram-4Y6WPE33-q5WxB9LO.js} +1 -1
- package/dist/assets/{sankeyDiagram-5OEKKPKP-Be-ROw_I.js → sankeyDiagram-5OEKKPKP-DlQNB367.js} +1 -1
- package/dist/assets/{sequenceDiagram-3UESZ5HK-E-tnxahu.js → sequenceDiagram-3UESZ5HK-BzHclOKt.js} +1 -1
- package/dist/assets/{stateDiagram-AJRCARHV-3rgFN7hL.js → stateDiagram-AJRCARHV-BvWRI9zK.js} +1 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-CDlF0VA8.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-Dcs4QFbE.js → timeline-definition-PNZ67QCA-j2wKjAti.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-BstUQ900.js → vennDiagram-CIIHVFJN-B77g7htC.js} +1 -1
- package/dist/assets/{wardley-L42UT6IY-CO77hXwj.js → wardley-L42UT6IY-83Im2mo2.js} +1 -1
- package/dist/assets/{wardleyDiagram-YWT4CUSO-BvrP3shF.js → wardleyDiagram-YWT4CUSO-CK-XB-bO.js} +1 -1
- package/dist/assets/{xychartDiagram-2RQKCTM6-Btu4JcQO.js → xychartDiagram-2RQKCTM6-D42FcVOY.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/checkpoint/anchor.js +66 -0
- package/lib/checkpoint/atomic.js +83 -0
- package/lib/checkpoint/checkpoint-writer.js +131 -0
- package/lib/checkpoint/fingerprint.js +145 -0
- package/lib/checkpoint/git.js +58 -0
- package/lib/checkpoint/prompts.js +206 -0
- package/lib/checkpoint/reconciler.js +207 -0
- package/lib/checkpoint/render.js +107 -0
- package/lib/checkpoint/store/index.js +67 -0
- package/lib/checkpoint/store/jsonl.js +80 -0
- package/package.json +1 -1
- package/server/compose-mcp-tools.js +30 -0
- package/server/compose-mcp.js +40 -0
- package/server/lifecycle-guard.js +225 -0
- package/server/session-routes.js +65 -0
- package/server/stratum-client.js +140 -0
- package/server/vision-routes.js +68 -20
- package/server/vision-server.js +2 -0
- package/dist/assets/channel-D6hNrRZ2.js +0 -1
- package/dist/assets/classDiagram-4FO5ZUOK-DvsVLUph.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-DvsVLUph.js +0 -1
- 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.
|
|
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
|
// ---------------------------------------------------------------------------
|
package/server/compose-mcp.js
CHANGED
|
@@ -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 {
|