@smartmemory/compose 0.2.52-beta → 0.2.54-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/.claude/skills/compose/SKILL.md +0 -8
- package/README.md +39 -0
- package/bin/compose.js +378 -3
- package/dist/assets/App-D_tvTs_n.js +896 -0
- package/dist/assets/abnfDiagram-VRR7QNED-Dhb7O5rp.js +1 -0
- package/dist/assets/arc-C4oVdnk2.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DqHrb5_F.js +36 -0
- package/dist/assets/blockDiagram-677ZJIJ3-DUyBayYK.js +132 -0
- package/dist/assets/{browser-CnKiSnlr.js → browser-Cr0recrN.js} +6 -6
- package/dist/assets/{c4Diagram-AHTNJAMY-DtDSNlOX.js → c4Diagram-LMCZKHZV-CxlkWMRK.js} +1 -1
- package/dist/assets/channel-xb4mQfuL.js +1 -0
- package/dist/assets/{chunk-QZHKN3VN-BMWFLZ8t.js → chunk-2Q5K7J3B-C6QJsyOQ.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-CqjNXooj.js → chunk-32BRIVSS-D9NgYu9W.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BAa7Edp6.js → chunk-5VM5RSS4-DbD86rhK.js} +1 -1
- package/dist/assets/chunk-EX3LRPZG-E4m4xNMw.js +231 -0
- package/dist/assets/{chunk-4BX2VUAB-oXU2npoL.js → chunk-JWPE2WC7-DHGlRWbw.js} +1 -1
- package/dist/assets/chunk-MOJQB5TN-BblYnFUq.js +88 -0
- package/dist/assets/chunk-RYQCIY6F-CGT-88vL.js +1 -0
- package/dist/assets/chunk-V7JOEXUC-BJJmX0mc.js +206 -0
- package/dist/assets/{chunk-EDXVE4YY-Bse2kdRh.js → chunk-VR4S4FIN-B4ygduCD.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BEEk2FdH.js → chunk-XXDRQBXY-7APXuI2s.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BIvgPTgh.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BIvgPTgh.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-NYnH3mVW.js → cose-bilkent-JH36ORCC-DnVcT6R6.js} +1 -1
- package/dist/assets/cynefin-VYW2F7L2-C9qiUSfX.js +178 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-BowkQhEu.js +62 -0
- package/dist/assets/dagre-VKFMJZFB-Y5V-7U3p.js +4 -0
- package/dist/assets/diagram-FQU43EPY-I7wDE-Xp.js +3 -0
- package/dist/assets/diagram-G47NLZAW-DapGa1HI.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-CHO1JntO.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-mBWP2D1G.js +30 -0
- package/dist/assets/diagram-WEI45ONY-C8KN8YnF.js +41 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-ZkJG9NOL.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-DGnL3kW1.js +85 -0
- package/dist/assets/flowDiagram-23GEKE2U-DHYupfOu.js +156 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-B1B2zpwR.js +292 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-B9_OP4Fs.js +106 -0
- package/dist/assets/graph-C9eacEi8.js +1 -0
- package/dist/assets/graph-xkel59g2.js +331 -0
- package/dist/assets/index-0Hq9436Q.js +119 -0
- package/dist/assets/index-LIwREYgH.css +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-D-a34TqE.js +2 -0
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-CK2IFFAP.js → ishikawaDiagram-FXEZZL3T-h0bUvXGV.js} +5 -5
- package/dist/assets/{journeyDiagram-VCZTEJTY-DAH5Stkf.js → journeyDiagram-5HDEW3XC-B4lm0RBa.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bi0aCqkv.js → kanban-definition-HUTT4EX6-msvfouRX.js} +7 -7
- package/dist/assets/katex-C5jXJg4s.js +257 -0
- package/dist/assets/layout-DEXfKzaS.js +1 -0
- package/dist/assets/{linear-ClGEGlS2.js → linear-B0trv2xN.js} +1 -1
- package/dist/assets/map-Czzmt4hB.js +1 -0
- package/dist/assets/{mindmap-definition-QFDTVHPH-Wjr-PgC6.js → mindmap-definition-LN4V7U3C-CvswbRgd.js} +7 -7
- package/dist/assets/{mobile-Chw8RWyH.js → mobile-CNLMhdFP.js} +2 -2
- package/dist/assets/pegDiagram-2B236MQR-DZEmIYC7.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-1ZfqxHkL.js +39 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-Owx49uiJ.js +7 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-B7RiaIGf.js +1 -0
- package/dist/assets/{requirementDiagram-MS252O5E-CU9Rlyq5.js → requirementDiagram-TGXJPOKE-fqmHs_5o.js} +3 -3
- package/dist/assets/sankeyDiagram-HTMAVEWB-D0Q3FeKV.js +40 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-CnPuB3fy.js +162 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-CsHoR37V.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-GVwpflU5.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DCGIHFu9.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-DRR41dsQ.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-C4RhLkZo.js +8 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-C8SZmaLp.js → timeline-definition-FHXFAJF6-ePaEemL7.js} +3 -3
- package/dist/assets/vennDiagram-L72KCM5P-BAYP8dNF.js +34 -0
- package/dist/assets/wardleyDiagram-EHGQE667-jNVH5Adm.js +78 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-BYDWGXbZ.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +34 -0
- package/lib/build.js +542 -148
- package/lib/experiment-judge.js +145 -0
- package/lib/experiment-metrics.js +265 -0
- package/lib/experiment-pricing.js +60 -0
- package/lib/experiment-report.js +306 -0
- package/lib/experiment-sandbox.js +147 -0
- package/lib/experiment.js +539 -0
- package/lib/feature-events.js +10 -0
- package/lib/feature-json.js +1 -1
- package/lib/feature-writer.js +30 -0
- package/lib/flow-state.js +36 -0
- package/lib/gate-prompt.js +66 -6
- package/lib/lifecycle-modes.js +213 -0
- package/lib/new.js +6 -2
- package/lib/roadmap-graph/index.js +26 -28
- package/lib/roadmap-graph/vision-adapter.js +126 -0
- package/lib/smartmemory-client.js +139 -0
- package/lib/smartmemory-config.js +50 -0
- package/lib/smartmemory-ingest.js +115 -0
- package/lib/smartmemory-sync.js +275 -0
- package/lib/stratum-mcp-client.js +9 -0
- package/lib/triage.js +7 -1
- package/lib/vision-writer.js +30 -6
- package/package.json +1 -1
- package/pipelines/plan.stratum.yaml +161 -0
- package/server/artifact-manager.js +30 -3
- package/server/compose-mcp-tools.js +11 -9
- package/server/compose-mcp.js +4 -0
- package/server/feature-scan.js +87 -173
- package/server/gate-log-store.js +11 -1
- package/server/graph-export.js +0 -0
- package/server/index.js +3 -5
- package/server/lifecycle-guard.js +62 -25
- package/server/roadmap-graph-vision.js +138 -0
- package/server/smartmemory-routes.js +113 -0
- package/server/status-snapshot.js +8 -7
- package/server/vision-routes.js +64 -33
- package/server/vision-server.js +6 -0
- package/server/vision-store.js +13 -4
- package/.claude/skills/compose/references/hermes-tools.md +0 -80
- package/dist/assets/App-Ddhfx18X.js +0 -889
- package/dist/assets/_baseUniq-CoEVVYyC.js +0 -1
- package/dist/assets/arc-DFnGxuCB.js +0 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-66rx01Tn.js +0 -36
- package/dist/assets/blockDiagram-DXYQGD6D-AE5PKSak.js +0 -132
- package/dist/assets/channel-CScyWprq.js +0 -1
- package/dist/assets/chunk-4TB4RGXK-5w2lZAjW.js +0 -206
- package/dist/assets/chunk-OYMX7WX6-CGr_YiAc.js +0 -231
- package/dist/assets/classDiagram-6PBFFD2Q-DibVYYdr.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DibVYYdr.js +0 -1
- package/dist/assets/clone-ZMALIFmw.js +0 -1
- package/dist/assets/dagre-KV5264BT--mrfaTHR.js +0 -4
- package/dist/assets/diagram-5BDNPKRD-DjXvyFzh.js +0 -10
- package/dist/assets/diagram-G4DWMVQ6-BrrexEvm.js +0 -24
- package/dist/assets/diagram-MMDJMWI5-D3Q0OT2M.js +0 -43
- package/dist/assets/diagram-TYMM5635-DKTntmxE.js +0 -24
- package/dist/assets/erDiagram-SMLLAGMA-BWbR0dPj.js +0 -85
- package/dist/assets/flowDiagram-DWJPFMVM-Dj5M5XaY.js +0 -162
- package/dist/assets/ganttDiagram-T4ZO3ILL-DKppFh_5.js +0 -292
- package/dist/assets/gitGraphDiagram-UUTBAWPF-9xQ9O25V.js +0 -106
- package/dist/assets/graph-Cgmhvu1T.js +0 -1
- package/dist/assets/graph-DZe55uk8.js +0 -331
- package/dist/assets/index-BxRamj_i.css +0 -1
- package/dist/assets/index-D8uDfn8y.js +0 -123
- package/dist/assets/infoDiagram-42DDH7IO-DQYWVzVN.js +0 -2
- package/dist/assets/katex-DkKDou_j.js +0 -257
- package/dist/assets/layout-DN13RkA1.js +0 -1
- package/dist/assets/min-CXr-fUFS.js +0 -1
- package/dist/assets/pieDiagram-DEJITSTG-CVipbIlr.js +0 -30
- package/dist/assets/quadrantDiagram-34T5L4WZ-CHJFyhm-.js +0 -7
- package/dist/assets/sankeyDiagram-XADWPNL6-BSULeqST.js +0 -10
- package/dist/assets/sequenceDiagram-FGHM5R23-CCOKXvjj.js +0 -157
- package/dist/assets/stateDiagram-FHFEXIEX-B91gKA7Q.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dc8ur9ta.js +0 -1
- package/dist/assets/vennDiagram-DHZGUBPP-YvEPDlsM.js +0 -34
- package/dist/assets/wardley-RL74JXVD-TMBUq_w1.js +0 -162
- package/dist/assets/wardleyDiagram-NUSXRM2D-DzEkPrri.js +0 -20
- package/dist/assets/xychartDiagram-5P7HB3ND-BIiCeiYk.js +0 -7
- package/lib/roadmap-graph/collect.js +0 -178
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-client.js — COMP-SMARTMEMORY-INGEST S02
|
|
3
|
+
*
|
|
4
|
+
* Raw HTTP client for the SmartMemory ingest/search wire contract. Global
|
|
5
|
+
* `fetch` + `AbortController` timeout. No SDK, no new dependency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Thrown on non-2xx from ingest/search, OR on a 2xx whose body doesn't match
|
|
10
|
+
* the expected shape (non-JSON, or missing the field the caller depends on —
|
|
11
|
+
* `status` for ingest, `results` for search). The latter case sets
|
|
12
|
+
* `kind: 'malformed-response'` so callers can tell "the service said no" from
|
|
13
|
+
* "the service said something we can't trust" if they want to, while both
|
|
14
|
+
* still surface as one failure type upstream (sync: `failed`; emitter:
|
|
15
|
+
* counts toward the circuit breaker).
|
|
16
|
+
*/
|
|
17
|
+
export class SmartmemoryHttpError extends Error {
|
|
18
|
+
constructor(message, status, kind) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'SmartmemoryHttpError';
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.kind = kind;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a client bound to a resolved config. The API key is read from
|
|
28
|
+
* process.env[cfg.apiKeyEnv] at call time (missing ⇒ treated as unreachable).
|
|
29
|
+
* @param {{ baseUrl: string, apiKeyEnv?: string, timeoutMs?: number }} cfg
|
|
30
|
+
* @returns {{ health(): Promise<{ok:boolean,status?:number}>, ingest(content:string,ctx:object): Promise<{status:string,unchanged:boolean,raw:object}>, search(query:string,opts?:object): Promise<object> }}
|
|
31
|
+
*/
|
|
32
|
+
export function createSmartmemoryClient(cfg) {
|
|
33
|
+
const baseUrl = cfg.baseUrl;
|
|
34
|
+
const timeoutMs = cfg.timeoutMs ?? 3000;
|
|
35
|
+
|
|
36
|
+
async function withTimeout(fn) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
|
+
try {
|
|
40
|
+
return await fn(controller.signal);
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function authHeader() {
|
|
47
|
+
const key = cfg.apiKeyEnv ? process.env[cfg.apiKeyEnv] : undefined;
|
|
48
|
+
if (!key) {
|
|
49
|
+
throw new SmartmemoryHttpError('smartmemory: missing api key', 0);
|
|
50
|
+
}
|
|
51
|
+
return `Bearer ${key}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function health() {
|
|
55
|
+
try {
|
|
56
|
+
const res = await withTimeout((signal) => fetch(`${baseUrl}/health`, { signal }));
|
|
57
|
+
return { ok: res.ok, status: res.status };
|
|
58
|
+
} catch {
|
|
59
|
+
return { ok: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function ingest(content, ctx) {
|
|
64
|
+
const auth = authHeader(); // throws BEFORE any fetch when key is missing
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
res = await withTimeout((signal) => fetch(`${baseUrl}/memory/ingest?mode=sync`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
signal,
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
Authorization: auth,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ content, context: ctx }),
|
|
75
|
+
}));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new SmartmemoryHttpError(`smartmemory: ingest request failed: ${err.message}`, 0);
|
|
78
|
+
}
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new SmartmemoryHttpError(`smartmemory: ingest failed (HTTP ${res.status})`, res.status);
|
|
81
|
+
}
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = await res.json();
|
|
85
|
+
} catch {
|
|
86
|
+
throw new SmartmemoryHttpError(
|
|
87
|
+
`smartmemory: ingest returned a 2xx (HTTP ${res.status}) with a non-JSON body`,
|
|
88
|
+
res.status, 'malformed-response',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (typeof raw?.status !== 'string') {
|
|
92
|
+
throw new SmartmemoryHttpError(
|
|
93
|
+
`smartmemory: ingest returned a 2xx (HTTP ${res.status}) body missing a "status" field`,
|
|
94
|
+
res.status, 'malformed-response',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const unchanged = raw.status === 'unchanged' || raw.unchanged === true;
|
|
98
|
+
return { status: raw.status, unchanged, raw };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function search(query, opts = {}) {
|
|
102
|
+
const auth = authHeader();
|
|
103
|
+
let res;
|
|
104
|
+
try {
|
|
105
|
+
res = await withTimeout((signal) => fetch(`${baseUrl}/memory/search`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
signal,
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
Authorization: auth,
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({ query, ...opts }),
|
|
113
|
+
}));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
throw new SmartmemoryHttpError(`smartmemory: search request failed: ${err.message}`, 0);
|
|
116
|
+
}
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
throw new SmartmemoryHttpError(`smartmemory: search failed (HTTP ${res.status})`, res.status);
|
|
119
|
+
}
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
raw = await res.json();
|
|
123
|
+
} catch {
|
|
124
|
+
throw new SmartmemoryHttpError(
|
|
125
|
+
`smartmemory: search returned a 2xx (HTTP ${res.status}) with a non-JSON body`,
|
|
126
|
+
res.status, 'malformed-response',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(raw?.results)) {
|
|
130
|
+
throw new SmartmemoryHttpError(
|
|
131
|
+
`smartmemory: search returned a 2xx (HTTP ${res.status}) body missing a "results" array`,
|
|
132
|
+
res.status, 'malformed-response',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return raw;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { health, ingest, search };
|
|
139
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-config.js — COMP-SMARTMEMORY-INGEST S01
|
|
3
|
+
*
|
|
4
|
+
* Shared reader + provenance helpers for the optional SmartMemory coupling.
|
|
5
|
+
* Leaf module: no fetch, no compose-state writes → safe to import eagerly at
|
|
6
|
+
* both hook sites (lib/feature-events.js, server/gate-log-store.js).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { deriveId } from './discover-workspaces.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read `.compose/compose.json` → `smartmemory` block. Uncached direct read,
|
|
15
|
+
* try/catch → {} on missing/malformed. Returns the raw block; consumers gate
|
|
16
|
+
* on `.enabled === true`. Does NOT resolve the API key.
|
|
17
|
+
* @param {string} cwd
|
|
18
|
+
* @returns {{ enabled?: boolean, baseUrl?: string, apiKeyEnv?: string, timeoutMs?: number }}
|
|
19
|
+
*/
|
|
20
|
+
export function getSmartmemoryConfig(cwd) {
|
|
21
|
+
const cfgPath = join(cwd, '.compose', 'compose.json');
|
|
22
|
+
try {
|
|
23
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
24
|
+
return cfg.smartmemory ?? {};
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Canonical per-project provenance tag. Wraps deriveId: compose.json
|
|
32
|
+
* #workspaceId when valid, else basename(cwd). Same value for emitters, sync,
|
|
33
|
+
* and RECALL → exact string-equality badge comparison end-to-end.
|
|
34
|
+
* @param {string} cwd
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveProjectTag(cwd) {
|
|
38
|
+
return deriveId({ root: cwd }).id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Deterministic provenance path: `compose/<project>/<repoRel>`. Used for
|
|
43
|
+
* every source_path (events: provenance only; files: dedupe key). Pure/total.
|
|
44
|
+
* @param {string} projectTag
|
|
45
|
+
* @param {string} repoRel repo-relative path (forward slashes)
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function sourcePathFor(projectTag, repoRel) {
|
|
49
|
+
return `compose/${projectTag}/${repoRel}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-ingest.js — COMP-SMARTMEMORY-INGEST S03
|
|
3
|
+
*
|
|
4
|
+
* Live emitter + shared pure renderers/context builders. Lazy-imported by the
|
|
5
|
+
* two hook sites (lib/feature-events.js, server/gate-log-store.js) only when
|
|
6
|
+
* smartmemory.enabled === true, so the fetch-capable module never loads when
|
|
7
|
+
* the feature is off.
|
|
8
|
+
*
|
|
9
|
+
* Fail-open: emitFeatureEvent/emitGateLogEntry never throw and never affect
|
|
10
|
+
* the caller — the local durable write has already completed by the time
|
|
11
|
+
* this runs. A circuit breaker disables further attempts after 3 consecutive
|
|
12
|
+
* failures so a dead service doesn't spam logs for the rest of the process
|
|
13
|
+
* lifetime.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getSmartmemoryConfig, resolveProjectTag, sourcePathFor } from './smartmemory-config.js';
|
|
17
|
+
import { createSmartmemoryClient } from './smartmemory-client.js';
|
|
18
|
+
|
|
19
|
+
// ── module state ──
|
|
20
|
+
let consecutiveFailures = 0;
|
|
21
|
+
let disabled = false;
|
|
22
|
+
const pending = new Set();
|
|
23
|
+
|
|
24
|
+
/** PURE. One deterministic line per feature-event row. Shared with sync. */
|
|
25
|
+
export function renderFeatureEventContent(row, projectTag) {
|
|
26
|
+
return `[compose:${projectTag}] ${row.ts} ${row.tool} ${row.code ?? row.feature_code ?? '-'} by ${row.actor ?? 'mcp:agent'}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** PURE. One deterministic line per gate-log entry. Shared with sync. */
|
|
30
|
+
export function renderGateLogContent(entry, projectTag) {
|
|
31
|
+
return `[compose:${projectTag}] ${entry.timestamp} gate:${entry.decision} ${entry.feature_code ?? '-'} ${entry.id}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** PURE. context for an event item (cli:compose regime — content-hash dedupe). */
|
|
35
|
+
export function buildFeatureEventContext(projectTag, row) {
|
|
36
|
+
return {
|
|
37
|
+
origin: 'cli:compose',
|
|
38
|
+
project: projectTag,
|
|
39
|
+
source_path: sourcePathFor(projectTag, '.compose/data/feature-events.jsonl'),
|
|
40
|
+
event: row,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildGateLogContext(projectTag, entry) {
|
|
45
|
+
return {
|
|
46
|
+
origin: 'cli:compose',
|
|
47
|
+
project: projectTag,
|
|
48
|
+
source_path: sourcePathFor(projectTag, '.compose/data/gate-log.jsonl'),
|
|
49
|
+
event: entry,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function onFailure(err) {
|
|
54
|
+
consecutiveFailures++;
|
|
55
|
+
if (consecutiveFailures === 1) {
|
|
56
|
+
console.warn('[smartmemory] ingest failed (fail-open): ' + err.message);
|
|
57
|
+
}
|
|
58
|
+
if (consecutiveFailures >= 3) {
|
|
59
|
+
disabled = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function emit(cwd, item, render, buildContext) {
|
|
64
|
+
if (disabled) return;
|
|
65
|
+
let cfg;
|
|
66
|
+
try {
|
|
67
|
+
cfg = getSmartmemoryConfig(cwd);
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (cfg.enabled !== true) return; // no fetch — byte-identity when off
|
|
72
|
+
|
|
73
|
+
let p;
|
|
74
|
+
try {
|
|
75
|
+
const tag = resolveProjectTag(cwd);
|
|
76
|
+
const content = render(item, tag);
|
|
77
|
+
const ctx = buildContext(tag, item);
|
|
78
|
+
const client = createSmartmemoryClient(cfg);
|
|
79
|
+
p = client.ingest(content, ctx);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Synchronous throw (e.g. missing api key) — treat as a failure, never rethrow.
|
|
82
|
+
onFailure(err);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pending.add(p);
|
|
87
|
+
p.then(
|
|
88
|
+
() => { consecutiveFailures = 0; },
|
|
89
|
+
(err) => onFailure(err),
|
|
90
|
+
).finally(() => pending.delete(p));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Fire-and-forget live emit after a durable local append. Never throws. */
|
|
94
|
+
export function emitFeatureEvent(cwd, row) {
|
|
95
|
+
emit(cwd, row, renderFeatureEventContent, buildFeatureEventContext);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function emitGateLogEntry(cwd, entry) {
|
|
99
|
+
emit(cwd, entry, renderGateLogContent, buildGateLogContext);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Await in-flight POSTs (bounded by timeoutMs). Drain point for callers that exit. */
|
|
103
|
+
export async function flushPending(timeoutMs = 3000) {
|
|
104
|
+
if (pending.size === 0) return;
|
|
105
|
+
const settleAll = Promise.allSettled([...pending]);
|
|
106
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
107
|
+
await Promise.race([settleAll, timeout]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Test hook: clear circuit breaker + pending between cases. */
|
|
111
|
+
export function _resetEmitterState() {
|
|
112
|
+
consecutiveFailures = 0;
|
|
113
|
+
disabled = false;
|
|
114
|
+
pending.clear();
|
|
115
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-sync.js — COMP-SMARTMEMORY-INGEST S05
|
|
3
|
+
*
|
|
4
|
+
* Idempotent backfill/re-sync walker: `compose smartmemory sync`. Walks the
|
|
5
|
+
* four persisted surfaces (feature-events, gate-log, journal entries,
|
|
6
|
+
* per-feature artifacts), in order, and ingests each as one SmartMemory item.
|
|
7
|
+
* Re-running is safe (Decision 4 dedupe regimes): events dedupe by content
|
|
8
|
+
* hash, files dedupe by source_path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { getSmartmemoryConfig, resolveProjectTag, sourcePathFor } from './smartmemory-config.js';
|
|
15
|
+
import { createSmartmemoryClient, SmartmemoryHttpError } from './smartmemory-client.js';
|
|
16
|
+
import {
|
|
17
|
+
renderFeatureEventContent, renderGateLogContent,
|
|
18
|
+
buildFeatureEventContext, buildGateLogContext,
|
|
19
|
+
} from './smartmemory-ingest.js';
|
|
20
|
+
import { readEvents } from './feature-events.js';
|
|
21
|
+
import { readGateLog } from '../server/gate-log-store.js';
|
|
22
|
+
import { parseJournalEntry } from './journal-writer.js';
|
|
23
|
+
import { listFeatures } from './feature-json.js';
|
|
24
|
+
import { loadFeaturesDir, resolveFeaturesPath, resolveJournalPath, relForDisplay } from './project-paths.js';
|
|
25
|
+
|
|
26
|
+
const ARTIFACT_FILES = [
|
|
27
|
+
{ file: 'design.md', kind: 'design' },
|
|
28
|
+
{ file: 'blueprint.md', kind: 'blueprint' },
|
|
29
|
+
{ file: 'plan.md', kind: 'plan' },
|
|
30
|
+
{ file: 'report.md', kind: 'report' },
|
|
31
|
+
{ file: 'audit.json', kind: 'audit' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function gateLogPathFor(cwd) {
|
|
39
|
+
return process.env.COMPOSE_GATE_LOG || join(cwd, '.compose', 'data', 'gate-log.jsonl');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Surface 1: feature-events.jsonl — malformed lines are already skipped by
|
|
44
|
+
* readEvents. May throw (naked fs read) — caller wraps per-surface.
|
|
45
|
+
*/
|
|
46
|
+
function collectFeatureEventItems(cwd, feature, tag) {
|
|
47
|
+
const items = [];
|
|
48
|
+
const events = readEvents(cwd);
|
|
49
|
+
for (const row of events) {
|
|
50
|
+
// OR semantics (blueprint.md ~line 176): a row carrying BOTH fields must
|
|
51
|
+
// still match on `feature_code` even when `code` differs — nullish
|
|
52
|
+
// coalescing (`code ?? feature_code`) would wrongly ignore `feature_code`
|
|
53
|
+
// whenever `code` is present at all.
|
|
54
|
+
if (feature && row.code !== feature && row.feature_code !== feature) continue;
|
|
55
|
+
items.push({ content: renderFeatureEventContent(row, tag), context: buildFeatureEventContext(tag, row) });
|
|
56
|
+
}
|
|
57
|
+
return items;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Surface 2: gate-log.jsonl. May throw (naked fs read) — caller wraps
|
|
62
|
+
* per-surface.
|
|
63
|
+
*/
|
|
64
|
+
function collectGateLogItems(cwd, feature, tag) {
|
|
65
|
+
const items = [];
|
|
66
|
+
const gateEntries = readGateLog({ featureCode: feature ?? undefined, logPath: gateLogPathFor(cwd) });
|
|
67
|
+
for (const entry of gateEntries) {
|
|
68
|
+
items.push({ content: renderGateLogContent(entry, tag), context: buildGateLogContext(tag, entry) });
|
|
69
|
+
}
|
|
70
|
+
return items;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Surface 3: journal entries — file regime (import:compose, source_path =
|
|
75
|
+
* dedupe key). Direct directory walk, NOT getJournalEntries: that reader
|
|
76
|
+
* hard-caps at MAX_LIMIT=500 (wrong for a backfill walker, which must walk
|
|
77
|
+
* everything) and silently drops unreadable/unparseable files instead of
|
|
78
|
+
* accounting for them, which would break this walker's skipped-count
|
|
79
|
+
* contract.
|
|
80
|
+
*
|
|
81
|
+
* A journal entry is defined by filename shape AND parse success (the same
|
|
82
|
+
* contract the canonical reader enforces) — not just "any *.md file". A
|
|
83
|
+
* stray non-entry .md file is simply not a candidate: it is never ingested
|
|
84
|
+
* and never counted as skipped. Per-file read/parse failures increment
|
|
85
|
+
* `skipped` via the returned counter (not thrown) so one broken entry never
|
|
86
|
+
* takes down the rest of the surface; a failure to even list the directory
|
|
87
|
+
* (e.g. it's not a directory at all) DOES throw — the caller wraps that.
|
|
88
|
+
*/
|
|
89
|
+
function collectJournalItems(cwd, feature, tag) {
|
|
90
|
+
const items = [];
|
|
91
|
+
let skipped = 0;
|
|
92
|
+
|
|
93
|
+
const journalDir = resolveJournalPath(cwd);
|
|
94
|
+
if (!existsSync(journalDir)) return { items, skipped };
|
|
95
|
+
|
|
96
|
+
// Exact filename pattern from lib/journal-writer.js:44 (FILENAME_RE, not
|
|
97
|
+
// exported — replicated here rather than editing that module).
|
|
98
|
+
const JOURNAL_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-session-(\d+)-([a-z0-9][a-z0-9-]*[a-z0-9])\.md$/;
|
|
99
|
+
|
|
100
|
+
const candidates = [];
|
|
101
|
+
for (const name of readdirSync(journalDir)) {
|
|
102
|
+
const m = name.match(JOURNAL_FILENAME_RE);
|
|
103
|
+
if (!m) continue; // not a candidate at all — not counted
|
|
104
|
+
candidates.push({ name, date: m[1], session_number: parseInt(m[2], 10) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Newest-first, mirroring lib/journal-writer.js:920-923 exactly (date desc,
|
|
108
|
+
// then session_number desc) — matters because runSync stops on quota 429,
|
|
109
|
+
// so a partial sync must ingest the newest entries first.
|
|
110
|
+
candidates.sort((a, b) => {
|
|
111
|
+
if (b.date !== a.date) return b.date.localeCompare(a.date);
|
|
112
|
+
return b.session_number - a.session_number;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
for (const { name } of candidates) {
|
|
116
|
+
const absPath = join(journalDir, name);
|
|
117
|
+
let text;
|
|
118
|
+
try {
|
|
119
|
+
text = readFileSync(absPath, 'utf-8');
|
|
120
|
+
} catch {
|
|
121
|
+
skipped++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Always parse — a candidate that fails to parse is skipped, not ingested.
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = parseJournalEntry(text);
|
|
129
|
+
} catch {
|
|
130
|
+
skipped++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (feature && parsed.feature_code !== feature) continue;
|
|
134
|
+
|
|
135
|
+
items.push({
|
|
136
|
+
content: text,
|
|
137
|
+
context: {
|
|
138
|
+
origin: 'import:compose',
|
|
139
|
+
project: tag,
|
|
140
|
+
source_path: sourcePathFor(tag, relForDisplay(cwd, absPath)),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { items, skipped };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Surface 4: per-feature artifacts — file regime. Per-file read failures
|
|
150
|
+
* increment `skipped` (not thrown); a failure to list features or the
|
|
151
|
+
* features directory itself DOES throw — the caller wraps that.
|
|
152
|
+
*/
|
|
153
|
+
function collectArtifactItems(cwd, feature, tag) {
|
|
154
|
+
const items = [];
|
|
155
|
+
let skipped = 0;
|
|
156
|
+
|
|
157
|
+
const allFeatures = listFeatures(cwd, loadFeaturesDir(cwd));
|
|
158
|
+
const targetFeatures = feature ? allFeatures.filter((f) => f.code === feature) : allFeatures;
|
|
159
|
+
const featuresAbsDir = resolveFeaturesPath(cwd);
|
|
160
|
+
for (const f of targetFeatures) {
|
|
161
|
+
const folder = join(featuresAbsDir, f.code);
|
|
162
|
+
for (const { file, kind } of ARTIFACT_FILES) {
|
|
163
|
+
const absPath = join(folder, file);
|
|
164
|
+
if (!existsSync(absPath)) continue;
|
|
165
|
+
let text;
|
|
166
|
+
try {
|
|
167
|
+
text = readFileSync(absPath, 'utf-8');
|
|
168
|
+
} catch {
|
|
169
|
+
skipped++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
items.push({
|
|
173
|
+
content: text,
|
|
174
|
+
context: {
|
|
175
|
+
origin: 'import:compose',
|
|
176
|
+
project: tag,
|
|
177
|
+
source_path: sourcePathFor(tag, relForDisplay(cwd, absPath)),
|
|
178
|
+
artifact: { feature_code: f.code, kind },
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { items, skipped };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Walk the four surfaces and build the flat, ordered list of {content,
|
|
189
|
+
* context} items to ingest, honoring the `--feature CODE` filter (Decision 5
|
|
190
|
+
* semantics): entries with no feature association are excluded when `feature`
|
|
191
|
+
* is set, included in a full sync.
|
|
192
|
+
*
|
|
193
|
+
* Each surface is collected independently, wrapped in its own try/catch: a
|
|
194
|
+
* surface whose top-level read throws (e.g. a corrupted directory in its
|
|
195
|
+
* path) is reported (one `skipped` increment + a console.warn naming the
|
|
196
|
+
* surface and the error) and does NOT abort the other three surfaces.
|
|
197
|
+
*/
|
|
198
|
+
function collectItems(cwd, feature, tag) {
|
|
199
|
+
const items = [];
|
|
200
|
+
let skipped = 0;
|
|
201
|
+
|
|
202
|
+
const surfaces = [
|
|
203
|
+
{ name: 'feature-events', collect: () => ({ items: collectFeatureEventItems(cwd, feature, tag), skipped: 0 }) },
|
|
204
|
+
{ name: 'gate-log', collect: () => ({ items: collectGateLogItems(cwd, feature, tag), skipped: 0 }) },
|
|
205
|
+
{ name: 'journal', collect: () => collectJournalItems(cwd, feature, tag) },
|
|
206
|
+
{ name: 'artifacts', collect: () => collectArtifactItems(cwd, feature, tag) },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
for (const surface of surfaces) {
|
|
210
|
+
try {
|
|
211
|
+
const result = surface.collect();
|
|
212
|
+
items.push(...result.items);
|
|
213
|
+
skipped += result.skipped;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
skipped++;
|
|
216
|
+
console.warn(`[smartmemory-sync] ${surface.name} surface read failed (skipping this surface): ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { items, skipped };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Walk the four persisted surfaces and ingest each item (idempotent).
|
|
225
|
+
* @param {{ cwd: string, dryRun?: boolean, feature?: string|null }} opts
|
|
226
|
+
* @returns {Promise<{ ingested:number, unchanged:number, skipped:number, failed:number, stoppedOnQuota:boolean }>}
|
|
227
|
+
*/
|
|
228
|
+
export async function runSync({ cwd, dryRun = false, feature = null }) {
|
|
229
|
+
const cfg = getSmartmemoryConfig(cwd);
|
|
230
|
+
if (cfg.enabled !== true && !dryRun) {
|
|
231
|
+
throw new Error('smartmemory not enabled');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const tag = resolveProjectTag(cwd);
|
|
235
|
+
const { items, skipped } = collectItems(cwd, feature, tag);
|
|
236
|
+
|
|
237
|
+
let ingested = 0;
|
|
238
|
+
let unchanged = 0;
|
|
239
|
+
let failed = 0;
|
|
240
|
+
let stoppedOnQuota = false;
|
|
241
|
+
|
|
242
|
+
if (dryRun) {
|
|
243
|
+
return { ingested: items.length, unchanged: 0, skipped, failed: 0, stoppedOnQuota: false };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const client = createSmartmemoryClient(cfg);
|
|
247
|
+
|
|
248
|
+
for (const item of items) {
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = await client.ingest(item.content, item.context);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof SmartmemoryHttpError && err.status === 429) {
|
|
254
|
+
await sleep(1000);
|
|
255
|
+
try {
|
|
256
|
+
result = await client.ingest(item.content, item.context);
|
|
257
|
+
} catch (err2) {
|
|
258
|
+
if (err2 instanceof SmartmemoryHttpError && err2.status === 429) {
|
|
259
|
+
stoppedOnQuota = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
failed++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
failed++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (result.unchanged) unchanged++;
|
|
271
|
+
else ingested++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { ingested, unchanged, skipped, failed, stoppedOnQuota };
|
|
275
|
+
}
|
|
@@ -139,6 +139,15 @@ export class StratumMcpClient {
|
|
|
139
139
|
|
|
140
140
|
const transportOpts = { command, args, stderr: 'pipe' };
|
|
141
141
|
if (opts.cwd) transportOpts.cwd = opts.cwd;
|
|
142
|
+
// Inherit the parent environment. Given no `env`, StdioClientTransport
|
|
143
|
+
// supplies only the MCP SDK's 6-var default allowlist
|
|
144
|
+
// (HOME/LOGNAME/PATH/SHELL/TERM/USER), which strips the auth context the
|
|
145
|
+
// spawned stratum-mcp → claude agent needs. Once claude credentials moved
|
|
146
|
+
// to the macOS keychain, that stripped env stopped authenticating and every
|
|
147
|
+
// agent step failed with `403 forbidden / "Request not allowed"`. Pass the
|
|
148
|
+
// full env through; the stratum connector scrubs SENSITIVE_ENV_VARS
|
|
149
|
+
// (API keys, CLAUDECODE) before it spawns the agent.
|
|
150
|
+
transportOpts.env = opts.env ?? { ...process.env };
|
|
142
151
|
this.#transport = new StdioClientTransport(transportOpts);
|
|
143
152
|
|
|
144
153
|
this.#client = new Client(
|
package/lib/triage.js
CHANGED
|
@@ -232,7 +232,9 @@ export async function runTriage(featureCode, opts = {}) {
|
|
|
232
232
|
*
|
|
233
233
|
* Returns true if:
|
|
234
234
|
* - feature.json has no triageTimestamp
|
|
235
|
-
* - any file in the feature folder has an mtime
|
|
235
|
+
* - any file in the feature folder (EXCEPT feature.json itself) has an mtime
|
|
236
|
+
* newer than triageTimestamp. feature.json is excluded because the stamp it
|
|
237
|
+
* is compared against lives inside it, so its own mtime would self-stale.
|
|
236
238
|
*
|
|
237
239
|
* @param {string} cwd - Project root
|
|
238
240
|
* @param {string} featureCode - Feature code
|
|
@@ -263,6 +265,10 @@ export function isTriageStale(cwd, featureCode, featuresDir = resolveFeaturesPat
|
|
|
263
265
|
const entries = readdirSync(featureDir, { withFileTypes: true });
|
|
264
266
|
for (const entry of entries) {
|
|
265
267
|
if (!entry.isFile()) continue;
|
|
268
|
+
// feature.json holds the triageTimestamp we compare against — counting its
|
|
269
|
+
// own mtime is circular: writing the stamp would self-invalidate the cache
|
|
270
|
+
// (COMP-ROADMAP-PLAN C14). Exclude it from the staleness scan.
|
|
271
|
+
if (entry.name === 'feature.json') continue;
|
|
266
272
|
const filePath = join(featureDir, entry.name);
|
|
267
273
|
try {
|
|
268
274
|
const stat = statSync(filePath);
|