@lh8ppl/claude-memory-kit 0.3.0 → 0.3.2
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/README.md +13 -3
- package/package.json +2 -2
- package/src/audit-log.mjs +1 -0
- package/src/auto-drain.mjs +17 -1
- package/src/auto-extract.mjs +4 -5
- package/src/auto-persona.mjs +86 -1
- package/src/capture-prompt.mjs +2 -1
- package/src/config-core.mjs +161 -0
- package/src/conflict-queue.mjs +2 -2
- package/src/content-hash.mjs +30 -0
- package/src/decisions-journal.mjs +223 -0
- package/src/digest.mjs +89 -0
- package/src/doctor.mjs +62 -3
- package/src/forget.mjs +6 -0
- package/src/import-anthropic-memory.mjs +2 -2
- package/src/import-claude-md.mjs +333 -0
- package/src/index-rebuild.mjs +6 -2
- package/src/index.mjs +10 -0
- package/src/inject-context.mjs +130 -1
- package/src/install.mjs +75 -2
- package/src/mcp-server.mjs +6 -1
- package/src/memory-health.mjs +229 -0
- package/src/memory-write.mjs +32 -10
- package/src/native-binding.mjs +142 -0
- package/src/poison-guard.mjs +55 -0
- package/src/remember-core.mjs +53 -8
- package/src/repair.mjs +20 -3
- package/src/search.mjs +105 -2
- package/src/semantic-backend.mjs +114 -0
- package/src/subcommands.mjs +300 -27
- package/src/transcript-index.mjs +5 -2
- package/src/write-fact.mjs +34 -3
- package/template/.claude/skills/memory-search/SKILL.md +1 -1
- package/template/.gitattributes.fragment +16 -0
- package/template/CLAUDE.md.template +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// The append-only decision journal — context/DECISIONS.md (Task 147, D-161).
|
|
2
|
+
//
|
|
3
|
+
// A chronological, human-readable page of every decision + its why. The VIEW
|
|
4
|
+
// the kit was missing: decisions are captured as facts but scattered across N
|
|
5
|
+
// per-fact files with no chronological decision page (cmk search is pull, not
|
|
6
|
+
// browse; MEMORY.md is bounded + rolls). This is the squad `decisions.md` /
|
|
7
|
+
// our own DECISION-LOG.md equivalent, made automatic.
|
|
8
|
+
//
|
|
9
|
+
// LIFECYCLE — APPEND-ONLY, never regenerated, never parked (D-161):
|
|
10
|
+
// - This is NOT a derived view like INDEX.md. Regenerating from live facts
|
|
11
|
+
// would silently ERASE superseded/forgotten decisions — rewriting history
|
|
12
|
+
// to look like the current state was always obvious (the exact failure the
|
|
13
|
+
// decision-trail-preservation rule exists to prevent).
|
|
14
|
+
// - A decision journal is unbounded by design: old decisions are the MOST
|
|
15
|
+
// valuable part (they explain why the codebase is shaped as it is), so the
|
|
16
|
+
// MEMORY.md rolling-window must NOT apply.
|
|
17
|
+
// - Mechanics: new decision → appended; tombstoned → its entry MARKED
|
|
18
|
+
// retracted IN PLACE (never removed); every pre-existing entry survives
|
|
19
|
+
// every update.
|
|
20
|
+
//
|
|
21
|
+
// The update is triggered like a derived view (runs where reindex runs, so the
|
|
22
|
+
// journal stays current) but its WRITE LOGIC is append-only — the file is the
|
|
23
|
+
// accumulator, the facts are only the trigger. Each entry carries a stable
|
|
24
|
+
// machine marker `<!-- decision:P-XXXXXXXX -->` so the updater knows which ids
|
|
25
|
+
// are already journaled + which entries to annotate, without parsing prose
|
|
26
|
+
// (and so a human can freely add their own prose between entries — preserved).
|
|
27
|
+
//
|
|
28
|
+
// v0.3.2 scope: explicit signals only (capture appends; forget marks retracted;
|
|
29
|
+
// explicit supersession annotates). AUTOMATIC semantic contradiction-detection
|
|
30
|
+
// is deferred to F-D / Task 95.
|
|
31
|
+
|
|
32
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { parse as parseFrontmatter } from './frontmatter.mjs';
|
|
35
|
+
import { ID_PATTERN } from './tier-paths.mjs';
|
|
36
|
+
|
|
37
|
+
export const DECISIONS_HEADER =
|
|
38
|
+
'# Decisions\n\n' +
|
|
39
|
+
'> Append-only decision journal — every decision the kit captured, in order, with its why.\n' +
|
|
40
|
+
'> Maintained by claude-memory-kit (`cmk digest`). Superseded/retracted entries stay (the trail is the point).';
|
|
41
|
+
|
|
42
|
+
// Only this fact type is a "decision" in the kit taxonomy (the project/state
|
|
43
|
+
// category — what project-memory's decisions.md and our DECISION-LOG track).
|
|
44
|
+
const DECISION_TYPE = 'project';
|
|
45
|
+
|
|
46
|
+
const markerFor = (id) => `<!-- decision:${id} -->`;
|
|
47
|
+
const RETRACT_TAG = '_(retracted';
|
|
48
|
+
|
|
49
|
+
/** The yyyy-mm-dd slice of an ISO timestamp, or the raw value if unparseable. */
|
|
50
|
+
function dateOnly(iso) {
|
|
51
|
+
if (typeof iso !== 'string') return 'unknown-date';
|
|
52
|
+
const m = iso.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
53
|
+
return m ? m[1] : iso;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render ONE journal entry for a decision fact. The machine marker lets the
|
|
58
|
+
* updater dedup + annotate; the human-readable lines are Title / date / Why / id.
|
|
59
|
+
*
|
|
60
|
+
* @param {{id:string,title:string,createdAt?:string,why?:string|null}} f
|
|
61
|
+
* @returns {string} the entry block (no trailing newline)
|
|
62
|
+
*/
|
|
63
|
+
export function buildDecisionEntry(f) {
|
|
64
|
+
const date = dateOnly(f.createdAt);
|
|
65
|
+
const lines = [
|
|
66
|
+
markerFor(f.id),
|
|
67
|
+
`### ${f.title}`,
|
|
68
|
+
`**When:** ${date} · **Fact:** \`${f.id}\``,
|
|
69
|
+
];
|
|
70
|
+
if (f.why && String(f.why).trim()) {
|
|
71
|
+
lines.push(`**Why:** ${String(f.why).trim()}`);
|
|
72
|
+
}
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The kit's id matcher, DERIVED from the canonical ID_PATTERN (tier-paths.mjs)
|
|
77
|
+
// so the base32 alphabet lives in exactly ONE place and can't drift. The
|
|
78
|
+
// original bug: this module hardcoded `[A-Z2-9]` (uppercase only), but the real
|
|
79
|
+
// alphabet includes a lowercase `a` — so any id containing `a` never matched
|
|
80
|
+
// "already journaled" → re-appended on EVERY digest run (the cut-gate find).
|
|
81
|
+
// Strip the `^…$` anchors to embed the pattern inside larger regexes.
|
|
82
|
+
const ID_CHARS = ID_PATTERN.source.replace(/^\^/, '').replace(/\$$/, '');
|
|
83
|
+
|
|
84
|
+
/** ids already present in the journal body (by their machine marker). */
|
|
85
|
+
function journaledIds(content) {
|
|
86
|
+
const ids = new Set();
|
|
87
|
+
const re = new RegExp(`<!-- decision:(${ID_CHARS}) -->`, 'g');
|
|
88
|
+
let m;
|
|
89
|
+
while ((m = re.exec(content)) !== null) ids.add(m[1]);
|
|
90
|
+
return ids;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Append-only journal update (D-161). Pure: content in → content out.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} a
|
|
97
|
+
* @param {string} a.existingContent current DECISIONS.md (‘’ if absent)
|
|
98
|
+
* @param {Array} a.facts live decision-class facts ({id,type,title,createdAt,why})
|
|
99
|
+
* @param {Set<string>} a.tombstonedIds ids whose fact has been forgotten
|
|
100
|
+
* @param {string} a.now ISO timestamp for retraction stamps
|
|
101
|
+
* @returns {string} the new DECISIONS.md content
|
|
102
|
+
*/
|
|
103
|
+
export function updateDecisionsJournal({ existingContent = '', facts = [], tombstonedIds = new Set(), now }) {
|
|
104
|
+
let content = existingContent.trim() === '' ? DECISIONS_HEADER + '\n' : existingContent;
|
|
105
|
+
const already = journaledIds(content);
|
|
106
|
+
|
|
107
|
+
// 1) Append entries for decision-class facts not yet journaled.
|
|
108
|
+
const newEntries = [];
|
|
109
|
+
for (const f of facts) {
|
|
110
|
+
if (f.type !== DECISION_TYPE) continue; // only decisions
|
|
111
|
+
if (already.has(f.id)) continue; // already journaled — never duplicate
|
|
112
|
+
newEntries.push(buildDecisionEntry(f));
|
|
113
|
+
already.add(f.id);
|
|
114
|
+
}
|
|
115
|
+
if (newEntries.length > 0) {
|
|
116
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
117
|
+
content += '\n' + newEntries.join('\n\n') + '\n';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2) Mark retracted (in place) any journaled entry whose fact is now
|
|
121
|
+
// tombstoned and not already marked. Never removes the entry.
|
|
122
|
+
const stamp = dateOnly(now);
|
|
123
|
+
for (const id of tombstonedIds) {
|
|
124
|
+
const marker = markerFor(id);
|
|
125
|
+
const idx = content.indexOf(marker);
|
|
126
|
+
if (idx === -1) continue; // not journaled — nothing to retract
|
|
127
|
+
// Bound the search to THIS entry's span — up to the next decision marker
|
|
128
|
+
// (or end-of-file). Prevents a malformed/hand-edited entry with no heading
|
|
129
|
+
// from attaching the retraction note to the NEXT entry's heading.
|
|
130
|
+
const nextMarker = content.indexOf('<!-- decision:', idx + marker.length);
|
|
131
|
+
const spanEnd = nextMarker === -1 ? content.length : nextMarker;
|
|
132
|
+
// Find this entry's heading line (the `### …` after the marker, within span).
|
|
133
|
+
const headingStart = content.indexOf('### ', idx);
|
|
134
|
+
if (headingStart === -1 || headingStart >= spanEnd) continue;
|
|
135
|
+
const headingEnd = content.indexOf('\n', headingStart);
|
|
136
|
+
if (headingEnd === -1) continue;
|
|
137
|
+
// Already retracted? (the note sits right after the heading)
|
|
138
|
+
const afterHeading = content.slice(headingEnd + 1, headingEnd + 1 + RETRACT_TAG.length);
|
|
139
|
+
if (afterHeading === RETRACT_TAG) continue;
|
|
140
|
+
const note = `${RETRACT_TAG} ${stamp})_`;
|
|
141
|
+
content = content.slice(0, headingEnd + 1) + note + '\n' + content.slice(headingEnd + 1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
145
|
+
return content;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- File-IO orchestration (the impure shell over the pure core) ----------
|
|
149
|
+
|
|
150
|
+
// Leading indent is [ \t]* (NOT \s*) so it can't match the newline the
|
|
151
|
+
// (?:^|\n) anchor already consumed — that overlap is the backtracking
|
|
152
|
+
// ambiguity SonarCloud flags as ReDoS. Disjoint character classes = linear.
|
|
153
|
+
const RICH_WHY_RE = /(?:^|\n)[ \t]*\*\*Why:\*\*[ \t]*([^\n]+)/;
|
|
154
|
+
|
|
155
|
+
/** Read decision-class facts (type:project) from the project tier. */
|
|
156
|
+
function readProjectDecisionFacts(projectRoot) {
|
|
157
|
+
const dir = join(projectRoot, 'context', 'memory');
|
|
158
|
+
const out = [];
|
|
159
|
+
if (!existsSync(dir)) return out;
|
|
160
|
+
for (const name of readdirSync(dir)) {
|
|
161
|
+
if (!name.endsWith('.md') || name === 'INDEX.md') continue;
|
|
162
|
+
try {
|
|
163
|
+
const { frontmatter, body } = parseFrontmatter(readFileSync(join(dir, name), 'utf8'));
|
|
164
|
+
if (!frontmatter?.id || frontmatter.type !== DECISION_TYPE) continue;
|
|
165
|
+
if (frontmatter.deleted_at) continue;
|
|
166
|
+
const whyMatch = String(body ?? '').match(RICH_WHY_RE);
|
|
167
|
+
out.push({
|
|
168
|
+
id: frontmatter.id,
|
|
169
|
+
type: frontmatter.type,
|
|
170
|
+
title: frontmatter.title ?? frontmatter.id,
|
|
171
|
+
createdAt: frontmatter.created_at ?? null,
|
|
172
|
+
why: whyMatch ? whyMatch[1].trim() : null,
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
// unparseable file — reindex/HC-4 own that class; the journal skips it
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Stable chronological order (oldest first) so appends read like a timeline.
|
|
179
|
+
out.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** ids of forgotten facts (tombstone archive). */
|
|
184
|
+
function readTombstonedIds(projectRoot) {
|
|
185
|
+
const ids = new Set();
|
|
186
|
+
const dir = join(projectRoot, 'context', 'memory', 'archive', 'tombstones');
|
|
187
|
+
if (!existsSync(dir)) return ids;
|
|
188
|
+
for (const name of readdirSync(dir)) {
|
|
189
|
+
const m = name.match(new RegExp(`^(${ID_CHARS})\\.md$`));
|
|
190
|
+
if (m) ids.add(m[1]);
|
|
191
|
+
}
|
|
192
|
+
return ids;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read → append-only update → write context/DECISIONS.md. Idempotent: a run
|
|
197
|
+
* with nothing new is a no-op write (same bytes). Best-effort: never throws
|
|
198
|
+
* into the caller (a journal failure must not break a capture/read path).
|
|
199
|
+
*
|
|
200
|
+
* @returns {{written:boolean, path:string, appended:number}|{written:false,error:string}}
|
|
201
|
+
*/
|
|
202
|
+
export function syncDecisionsJournal({ projectRoot, now } = {}) {
|
|
203
|
+
try {
|
|
204
|
+
const path = join(projectRoot, 'context', 'DECISIONS.md');
|
|
205
|
+
const existingContent = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
206
|
+
const facts = readProjectDecisionFacts(projectRoot);
|
|
207
|
+
const tombstonedIds = readTombstonedIds(projectRoot);
|
|
208
|
+
const before = existingContent;
|
|
209
|
+
const next = updateDecisionsJournal({
|
|
210
|
+
existingContent,
|
|
211
|
+
facts,
|
|
212
|
+
tombstonedIds,
|
|
213
|
+
now: now ?? new Date().toISOString(),
|
|
214
|
+
});
|
|
215
|
+
if (next !== before) {
|
|
216
|
+
writeFileSync(path, next, 'utf8');
|
|
217
|
+
return { written: true, path, appended: next.length - before.length };
|
|
218
|
+
}
|
|
219
|
+
return { written: false, path, appended: 0 };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return { written: false, error: err?.message ?? String(err) };
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/digest.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// `cmk digest` — a regenerated, readable render of everything the kit currently
|
|
2
|
+
// knows (Task 147, D-132). Facts by type + the persona + active threads, as one
|
|
3
|
+
// markdown page. The README-demo artifact.
|
|
4
|
+
//
|
|
5
|
+
// REGENERATED (not append-only): unlike DECISIONS.md (the permanent journal),
|
|
6
|
+
// the digest is a CURRENT-KNOWLEDGE snapshot — it should reflect only what
|
|
7
|
+
// exists now, so it is rebuilt on every invocation (the INDEX.md lifecycle,
|
|
8
|
+
// correct here). The two surfaces differ on purpose: digest = "what do we know
|
|
9
|
+
// now", DECISIONS.md = "what did we decide over time" (D-161).
|
|
10
|
+
//
|
|
11
|
+
// Read-only by contract: pure reads over the fact archive + scratchpads. The
|
|
12
|
+
// `--decisions` flag also triggers the DECISIONS.md journal sync (the one
|
|
13
|
+
// mutation, delegated to the append-only writer).
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { parse as parseFrontmatter } from './frontmatter.mjs';
|
|
18
|
+
|
|
19
|
+
const TYPE_ORDER = ['project', 'feedback', 'reference', 'user'];
|
|
20
|
+
const TYPE_LABEL = {
|
|
21
|
+
project: 'Decisions & project state',
|
|
22
|
+
feedback: 'Working-style & preferences',
|
|
23
|
+
reference: 'References',
|
|
24
|
+
user: 'About the user',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function readFacts(projectRoot) {
|
|
28
|
+
const dir = join(projectRoot, 'context', 'memory');
|
|
29
|
+
const facts = [];
|
|
30
|
+
if (!existsSync(dir)) return facts;
|
|
31
|
+
for (const name of readdirSync(dir)) {
|
|
32
|
+
if (!name.endsWith('.md') || name === 'INDEX.md') continue;
|
|
33
|
+
try {
|
|
34
|
+
const { frontmatter } = parseFrontmatter(readFileSync(join(dir, name), 'utf8'));
|
|
35
|
+
if (!frontmatter?.id || frontmatter.deleted_at) continue;
|
|
36
|
+
facts.push({
|
|
37
|
+
id: frontmatter.id,
|
|
38
|
+
type: frontmatter.type ?? 'unknown',
|
|
39
|
+
title: frontmatter.title ?? frontmatter.id,
|
|
40
|
+
trust: frontmatter.trust ?? 'unknown',
|
|
41
|
+
createdAt: frontmatter.created_at ?? null,
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
// unparseable — reindex/HC-4 own that class
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return facts;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the digest markdown from facts (pure — exported for testing).
|
|
52
|
+
* @param {Array} facts
|
|
53
|
+
* @param {{now?:string}} [opts]
|
|
54
|
+
*/
|
|
55
|
+
export function buildDigest(facts, { now } = {}) {
|
|
56
|
+
const stamp = (now ?? new Date().toISOString()).slice(0, 10);
|
|
57
|
+
const lines = [`# Memory digest — ${stamp}`, ''];
|
|
58
|
+
if (facts.length === 0) {
|
|
59
|
+
lines.push('_Memory is empty — capture starts as you work._', '');
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
lines.push(`${facts.length} fact(s) in project memory.`, '');
|
|
63
|
+
|
|
64
|
+
const byType = new Map();
|
|
65
|
+
for (const f of facts) {
|
|
66
|
+
if (!byType.has(f.type)) byType.set(f.type, []);
|
|
67
|
+
byType.get(f.type).push(f);
|
|
68
|
+
}
|
|
69
|
+
const orderedTypes = [
|
|
70
|
+
...TYPE_ORDER.filter((t) => byType.has(t)),
|
|
71
|
+
...[...byType.keys()].filter((t) => !TYPE_ORDER.includes(t)),
|
|
72
|
+
];
|
|
73
|
+
for (const type of orderedTypes) {
|
|
74
|
+
const group = byType.get(type).slice().sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
75
|
+
lines.push(`## ${TYPE_LABEL[type] ?? type} (${group.length})`, '');
|
|
76
|
+
for (const f of group) {
|
|
77
|
+
const date = String(f.createdAt ?? '').slice(0, 10) || '—';
|
|
78
|
+
lines.push(`- **${f.title}** · \`${f.id}\` · ${f.trust} · ${date}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Read facts + render the digest for a project (read-only). */
|
|
86
|
+
export function digest({ projectRoot, now } = {}) {
|
|
87
|
+
const facts = readFacts(projectRoot);
|
|
88
|
+
return buildDigest(facts, { now });
|
|
89
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `cmk doctor` — health checks HC-1..HC-
|
|
1
|
+
// `cmk doctor` — health checks HC-1..HC-8 (Task 37, T-031; memsearch HC-1/HC-7 removed in Task 120; HC-8 native bindings added in Task 141a).
|
|
2
2
|
//
|
|
3
3
|
// Public boundary:
|
|
4
4
|
// async runDoctor({projectRoot, userDir, now, promptUser?, ...overrides})
|
|
@@ -44,6 +44,8 @@ import { nowIso } from './audit-log.mjs';
|
|
|
44
44
|
import { detectStaleLocks } from './lock-discipline.mjs';
|
|
45
45
|
import { cronSentinelPath } from './lazy-compress.mjs';
|
|
46
46
|
import { getNativeAutoMemoryState } from './native-memory.mjs';
|
|
47
|
+
import { checkKitBinding, checkEmbedderBinding } from './native-binding.mjs';
|
|
48
|
+
import { resolveDefaultSearchMode } from './semantic-backend.mjs';
|
|
47
49
|
|
|
48
50
|
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000;
|
|
49
51
|
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
|
|
@@ -470,13 +472,67 @@ function hc7StaleLocks({ projectRoot, userDir }) {
|
|
|
470
472
|
};
|
|
471
473
|
}
|
|
472
474
|
|
|
475
|
+
// --- HC-8: native bindings present (npm 12 readiness, Task 141a) -------
|
|
476
|
+
// The BACKSTOP, not the primary UX: `cmk install` probes + asks inline
|
|
477
|
+
// (the user's 2026-06-12 steer); HC-8 catches the after-the-fact states
|
|
478
|
+
// (npm upgraded later, package reinstalled without the allow flag).
|
|
479
|
+
// The repair is an `npm install -g` → requiresInstall per the design §14
|
|
480
|
+
// ask-before-install rule.
|
|
481
|
+
async function hc8NativeBindings({ projectRoot, kitBindingProbe, embedderBindingProbe }) {
|
|
482
|
+
const kitProbe = kitBindingProbe ?? checkKitBinding;
|
|
483
|
+
const kit = kitProbe();
|
|
484
|
+
if (!kit.ok) {
|
|
485
|
+
return {
|
|
486
|
+
id: 'HC-8',
|
|
487
|
+
name: 'Native bindings present (npm 12 readiness)',
|
|
488
|
+
status: 'fail',
|
|
489
|
+
message: `better-sqlite3 native binding unavailable (${kit.reason}) — most common cause: npm 12 blocks dependency install scripts by default, so a fresh install skips the binding build (a Node major upgrade is the other); search/reindex will crash until it is rebuilt`,
|
|
490
|
+
recoveryCommand: kit.remedy,
|
|
491
|
+
requiresInstall: true,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// The embedder matters only when this project actually defaults to it.
|
|
495
|
+
const mode = resolveDefaultSearchMode({ projectRoot });
|
|
496
|
+
if (mode === 'keyword') {
|
|
497
|
+
return {
|
|
498
|
+
id: 'HC-8',
|
|
499
|
+
name: 'Native bindings present (npm 12 readiness)',
|
|
500
|
+
status: 'pass',
|
|
501
|
+
message: 'better-sqlite3 binding healthy (semantic not configured — embedder not checked)',
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const embedderProbe = embedderBindingProbe ?? checkEmbedderBinding;
|
|
505
|
+
const embedder = await embedderProbe();
|
|
506
|
+
if (!embedder.ok) {
|
|
507
|
+
const state = embedder.installed
|
|
508
|
+
? `installed but its native binding failed (${embedder.reason}) — npm 12 blocks onnxruntime-node's install script by default`
|
|
509
|
+
: `not installed, but search.default_mode is '${mode}'`;
|
|
510
|
+
return {
|
|
511
|
+
id: 'HC-8',
|
|
512
|
+
name: 'Native bindings present (npm 12 readiness)',
|
|
513
|
+
status: 'fail',
|
|
514
|
+
message: `semantic embedder ${state}; searches degrade to keyword until fixed`,
|
|
515
|
+
recoveryCommand: embedder.remedy,
|
|
516
|
+
requiresInstall: true,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
id: 'HC-8',
|
|
521
|
+
name: 'Native bindings present (npm 12 readiness)',
|
|
522
|
+
status: 'pass',
|
|
523
|
+
message: `better-sqlite3 binding healthy; embedder import OK (default mode: ${mode}; the deep pipeline check runs at --with-semantic warm)`,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
473
527
|
/**
|
|
474
|
-
* Run the full
|
|
528
|
+
* Run the full 8-check health audit.
|
|
475
529
|
*
|
|
476
530
|
* @param {object} opts
|
|
477
531
|
* @param {string} opts.projectRoot
|
|
478
532
|
* @param {string} [opts.userDir]
|
|
479
533
|
* @param {string} [opts.now]
|
|
534
|
+
* @param {Function} [opts.kitBindingProbe] - HC-8 test seam.
|
|
535
|
+
* @param {Function} [opts.embedderBindingProbe] - HC-8 test seam.
|
|
480
536
|
* @returns {Promise<{action, checks, duration_ms}>}
|
|
481
537
|
*
|
|
482
538
|
* Note: M3 fix (skill-review 2026-05-28) dropped the v0.1.0 `promptUser`
|
|
@@ -489,6 +545,8 @@ export async function runDoctor({
|
|
|
489
545
|
projectRoot,
|
|
490
546
|
userDir,
|
|
491
547
|
now,
|
|
548
|
+
kitBindingProbe,
|
|
549
|
+
embedderBindingProbe,
|
|
492
550
|
} = {}) {
|
|
493
551
|
const t0 = Date.now();
|
|
494
552
|
if (!projectRoot) {
|
|
@@ -510,10 +568,11 @@ export async function runDoctor({
|
|
|
510
568
|
const c5 = hc5CronRegistered({ projectRoot });
|
|
511
569
|
const c6 = hc6NativeAutoMemory({ projectRoot, now: ts });
|
|
512
570
|
const c7 = hc7StaleLocks({ projectRoot, userDir: resolvedUserDir });
|
|
571
|
+
const c8 = await hc8NativeBindings({ projectRoot, kitBindingProbe, embedderBindingProbe });
|
|
513
572
|
|
|
514
573
|
return {
|
|
515
574
|
action: 'completed',
|
|
516
|
-
checks: [c1, c2, c3, c4, c5, c6, c7],
|
|
575
|
+
checks: [c1, c2, c3, c4, c5, c6, c7, c8],
|
|
517
576
|
duration_ms: Date.now() - t0,
|
|
518
577
|
};
|
|
519
578
|
}
|
package/src/forget.mjs
CHANGED
|
@@ -172,6 +172,12 @@ function scrubAllScratchpads(tierRoot, id) {
|
|
|
172
172
|
if (!entry.isFile()) continue;
|
|
173
173
|
if (!entry.name.endsWith('.md')) continue;
|
|
174
174
|
if (entry.name === 'INDEX.md') continue;
|
|
175
|
+
// DECISIONS.md is the APPEND-ONLY decision journal (Task 147 / D-161), NOT
|
|
176
|
+
// a scratchpad — forget must NOT strip its id-bearing lines (the marker +
|
|
177
|
+
// **Fact:** line). The journal sync marks the entry RETRACTED in place
|
|
178
|
+
// instead (preserving the trail). Scrubbing it here would delete the
|
|
179
|
+
// entry's marker and break the retract-in-place path (composition bug).
|
|
180
|
+
if (entry.name === 'DECISIONS.md') continue;
|
|
175
181
|
const p = join(tierRoot, entry.name);
|
|
176
182
|
const r = scrubScratchpadFile(p, id);
|
|
177
183
|
if (r.changed) edits.push({ path: p, removed: r.removed });
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from './audit-log.mjs';
|
|
41
41
|
import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
|
|
42
42
|
import { writeBullet } from './provenance.mjs';
|
|
43
|
-
import {
|
|
43
|
+
import { hashContent } from './content-hash.mjs';
|
|
44
44
|
|
|
45
45
|
const MEMORY_REL = ['context', 'MEMORY.md'];
|
|
46
46
|
|
|
@@ -236,7 +236,7 @@ export async function importAnthropicMemory({
|
|
|
236
236
|
// an import failed and search degraded to the stale index (cut-gate9 F-13).
|
|
237
237
|
const bulletLines = proposals
|
|
238
238
|
.map((p) => {
|
|
239
|
-
const sha1 =
|
|
239
|
+
const sha1 = hashContent(p.text);
|
|
240
240
|
const formatted = writeBullet({
|
|
241
241
|
id: p.id,
|
|
242
242
|
text: p.text,
|