@lh8ppl/claude-memory-kit 0.2.4 → 0.3.1
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 +16 -10
- package/bin/cmk-capture-prompt.mjs +21 -1
- package/package.json +2 -1
- package/src/audit-log.mjs +1 -0
- package/src/auto-drain.mjs +17 -1
- package/src/auto-extract.mjs +72 -16
- package/src/auto-persona.mjs +86 -1
- package/src/capture-prompt.mjs +34 -1
- package/src/capture-turn.mjs +64 -6
- package/src/config-core.mjs +161 -0
- package/src/conflict-queue.mjs +20 -3
- package/src/content-hash.mjs +30 -0
- package/src/doctor.mjs +62 -3
- package/src/forget.mjs +13 -0
- package/src/frontmatter.mjs +4 -1
- package/src/import-anthropic-memory.mjs +25 -1
- package/src/import-claude-md.mjs +333 -0
- package/src/index-db.mjs +39 -0
- package/src/index-rebuild.mjs +48 -4
- package/src/index.mjs +10 -0
- package/src/inject-context.mjs +179 -7
- package/src/install.mjs +180 -1
- package/src/mcp-server.mjs +63 -8
- package/src/memory-health.mjs +229 -0
- package/src/memory-write.mjs +32 -10
- package/src/merge-facts.mjs +12 -0
- package/src/native-binding.mjs +142 -0
- package/src/poison-guard.mjs +55 -0
- package/src/provenance.mjs +4 -0
- package/src/remember-core.mjs +53 -8
- package/src/repair.mjs +20 -3
- package/src/result-shapes.mjs +1 -1
- package/src/scratchpad.mjs +5 -3
- package/src/search.mjs +96 -9
- package/src/semantic-backend.mjs +599 -0
- package/src/settings-hooks.mjs +4 -1
- package/src/subcommands.mjs +359 -42
- package/src/transcript-index.mjs +165 -0
- package/src/turn-tools.mjs +179 -0
- package/src/write-fact.mjs +34 -3
- package/template/.claude/skills/memory-search/SKILL.md +86 -0
- package/template/.gitattributes.fragment +16 -0
- package/template/CLAUDE.md.template +3 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// `cmk config get/set/--show-origin` core (Task 129, D-121).
|
|
2
|
+
//
|
|
3
|
+
// The v0.1.0 stub became real the day `--with-semantic` shipped:
|
|
4
|
+
// context/settings.json now carries a user-facing setting
|
|
5
|
+
// (search.default_mode) and hand-editing JSON was the only path. This is
|
|
6
|
+
// the read-merge-write surface over the kit's settings files.
|
|
7
|
+
//
|
|
8
|
+
// Settings live in `<tier-root>/settings.json` for each of the three tiers
|
|
9
|
+
// (resolveTierRoot — the shared module, not re-derived). Resolution
|
|
10
|
+
// precedence mirrors the kit's memory model + git config semantics:
|
|
11
|
+
// local (context.local/) > project (context/) > user (~/.claude-memory-kit/)
|
|
12
|
+
// A `get` returns the highest-precedence tier that defines the dotted key;
|
|
13
|
+
// `--show-origin` lists every tier that defines it (winner + shadowed), the
|
|
14
|
+
// direnv lesson (design §7.2: "without --show-origin, users rage-quit when
|
|
15
|
+
// settings appear from nowhere"). `set` writes one tier (project default),
|
|
16
|
+
// preserving every sibling key (the mergeProjectSettings discipline,
|
|
17
|
+
// generalized per tier).
|
|
18
|
+
//
|
|
19
|
+
// Scope (D-121): the kit's own JSON settings files. NOT the richer
|
|
20
|
+
// settings-or-observation `--show-origin` sketch in design §7.2's example
|
|
21
|
+
// (observations have their own provenance/shadowed_by surface, §6); this is
|
|
22
|
+
// the concrete settings half the semantic default forced into existence.
|
|
23
|
+
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { resolveTierRoot } from './tier-paths.mjs';
|
|
27
|
+
|
|
28
|
+
// Highest-precedence first.
|
|
29
|
+
const TIERS = Object.freeze([
|
|
30
|
+
{ name: 'local', tier: 'L' },
|
|
31
|
+
{ name: 'project', tier: 'P' },
|
|
32
|
+
{ name: 'user', tier: 'U' },
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Keys that would pollute the prototype chain — rejected on both read and
|
|
36
|
+
// write. `cmk config set __proto__.x y` must never reach Object.prototype
|
|
37
|
+
// (skill-review blocking finding); a key path containing any of these is
|
|
38
|
+
// invalid, not a silent no-op.
|
|
39
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
40
|
+
function hasForbiddenSegment(dottedKey) {
|
|
41
|
+
return dottedKey.split('.').some((p) => FORBIDDEN_KEYS.has(p));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function settingsPathFor(tierName, { projectRoot, userDir }) {
|
|
45
|
+
const tier = TIERS.find((t) => t.name === tierName)?.tier;
|
|
46
|
+
return join(resolveTierRoot({ tier, projectRoot, userDir }), 'settings.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readSettings(path) {
|
|
50
|
+
if (!existsSync(path)) return null;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
53
|
+
} catch {
|
|
54
|
+
// A malformed settings file is treated as absent for resolution — never
|
|
55
|
+
// throw on a read (a hand-broken JSON shouldn't crash `cmk config get`).
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Walk a dotted path; returns {found, value}. `found` distinguishes a key
|
|
61
|
+
// set to `undefined`-ish from a key that isn't there (the honesty contract).
|
|
62
|
+
function dig(obj, dottedKey) {
|
|
63
|
+
if (obj == null || typeof obj !== 'object') return { found: false };
|
|
64
|
+
const parts = dottedKey.split('.');
|
|
65
|
+
let cur = obj;
|
|
66
|
+
for (const p of parts) {
|
|
67
|
+
if (cur == null || typeof cur !== 'object' || !(p in cur)) return { found: false };
|
|
68
|
+
cur = cur[p];
|
|
69
|
+
}
|
|
70
|
+
return { found: true, value: cur };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a dotted setting key across tiers (local > project > user).
|
|
75
|
+
*
|
|
76
|
+
* @returns {{found: boolean, value?: *, tier?: 'local'|'project'|'user'}}
|
|
77
|
+
*/
|
|
78
|
+
export function configGet(key, { projectRoot, userDir } = {}) {
|
|
79
|
+
if (!key || !String(key).trim()) return { found: false };
|
|
80
|
+
if (hasForbiddenSegment(key)) return { found: false };
|
|
81
|
+
for (const { name } of TIERS) {
|
|
82
|
+
const settings = readSettings(settingsPathFor(name, { projectRoot, userDir }));
|
|
83
|
+
const hit = dig(settings, key);
|
|
84
|
+
if (hit.found) return { found: true, value: hit.value, tier: name };
|
|
85
|
+
}
|
|
86
|
+
return { found: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Scalar coercion: true/false/null → primitives, integer/float strings →
|
|
90
|
+
* numbers, everything else stays a string. JSON settings are typed, and a
|
|
91
|
+
* CLI arg is always a string — `cmk config set x true` should write a bool. */
|
|
92
|
+
function coerce(raw) {
|
|
93
|
+
if (raw === 'true') return true;
|
|
94
|
+
if (raw === 'false') return false;
|
|
95
|
+
if (raw === 'null') return null;
|
|
96
|
+
if (/^-?\d+$/.test(raw)) return Number.parseInt(raw, 10);
|
|
97
|
+
if (/^-?\d*\.\d+$/.test(raw)) return Number.parseFloat(raw);
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setDeep(obj, dottedKey, value) {
|
|
102
|
+
const parts = dottedKey.split('.');
|
|
103
|
+
let cur = obj;
|
|
104
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
105
|
+
const p = parts[i];
|
|
106
|
+
if (cur[p] == null || typeof cur[p] !== 'object' || Array.isArray(cur[p])) cur[p] = {};
|
|
107
|
+
cur = cur[p];
|
|
108
|
+
}
|
|
109
|
+
cur[parts[parts.length - 1]] = value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set a dotted key in one tier's settings.json (project default), preserving
|
|
114
|
+
* every sibling key (read-merge-write).
|
|
115
|
+
*
|
|
116
|
+
* @returns {{ok: boolean, tier?: string, path?: string, error?: string}}
|
|
117
|
+
*/
|
|
118
|
+
export function configSet(key, rawValue, { projectRoot, userDir, tier = 'project' } = {}) {
|
|
119
|
+
if (!key || !String(key).trim()) return { ok: false, error: 'key is required (dotted path)' };
|
|
120
|
+
if (hasForbiddenSegment(key)) {
|
|
121
|
+
return { ok: false, error: `key contains a forbidden segment (${[...FORBIDDEN_KEYS].join('/')}) — prototype-pollution guard` };
|
|
122
|
+
}
|
|
123
|
+
if (!TIERS.some((t) => t.name === tier)) {
|
|
124
|
+
return { ok: false, error: `tier must be one of local/project/user (got ${tier})` };
|
|
125
|
+
}
|
|
126
|
+
const path = settingsPathFor(tier, { projectRoot, userDir });
|
|
127
|
+
try {
|
|
128
|
+
const current = readSettings(path) ?? {};
|
|
129
|
+
setDeep(current, key, coerce(String(rawValue)));
|
|
130
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
131
|
+
writeFileSync(path, JSON.stringify(current, null, 2) + '\n', 'utf8');
|
|
132
|
+
return { ok: true, tier, path };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every tier that defines the key, highest-precedence first. The winner is
|
|
140
|
+
* the first; the rest carry `shadowedBy` = the winning tier (the direnv
|
|
141
|
+
* "where did this come from?" surface).
|
|
142
|
+
*
|
|
143
|
+
* @returns {{found: boolean, entries: Array<{tier, value, path, winner, shadowedBy?}>}}
|
|
144
|
+
*/
|
|
145
|
+
export function configShowOrigin(key, { projectRoot, userDir } = {}) {
|
|
146
|
+
const entries = [];
|
|
147
|
+
if (!key || !String(key).trim()) return { found: false, entries };
|
|
148
|
+
if (hasForbiddenSegment(key)) return { found: false, entries };
|
|
149
|
+
for (const { name } of TIERS) {
|
|
150
|
+
const path = settingsPathFor(name, { projectRoot, userDir });
|
|
151
|
+
const hit = dig(readSettings(path), key);
|
|
152
|
+
if (hit.found) entries.push({ tier: name, value: hit.value, path });
|
|
153
|
+
}
|
|
154
|
+
if (entries.length === 0) return { found: false, entries: [] };
|
|
155
|
+
const winnerTier = entries[0].tier;
|
|
156
|
+
for (let i = 0; i < entries.length; i++) {
|
|
157
|
+
entries[i].winner = i === 0;
|
|
158
|
+
if (i > 0) entries[i].shadowedBy = winnerTier;
|
|
159
|
+
}
|
|
160
|
+
return { found: true, entries };
|
|
161
|
+
}
|
package/src/conflict-queue.mjs
CHANGED
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
} from 'node:fs';
|
|
50
50
|
import { join } from 'node:path';
|
|
51
51
|
import { resolveTierRoot, VALID_TIERS } from './tier-paths.mjs';
|
|
52
|
+
import { writeBullet } from './provenance.mjs';
|
|
53
|
+
import { hashContent } from './content-hash.mjs';
|
|
52
54
|
import { nowIso, appendAuditEntry, REASON_CODES } from './audit-log.mjs';
|
|
53
55
|
import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
|
|
54
56
|
import { generateId } from '@lh8ppl/cmk-canonicalize';
|
|
@@ -786,9 +788,24 @@ export function mergeScratchpadBullets({
|
|
|
786
788
|
const effectiveSection = section ?? discoverSectionAt(lines, matchA.bulletIdx);
|
|
787
789
|
const range = effectiveSection ? findSectionRange(updatedLines, effectiveSection) : null;
|
|
788
790
|
const insertAt = range ? range.endIdx : updatedLines.length;
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
791
|
+
// D-125 class (Task 138 review finding): the old hand-rolled comment had
|
|
792
|
+
// no `write:` key, so the first reindex after a merge-both resolution hit
|
|
793
|
+
// the NOT-NULL observations.write_source constraint. Canonical shape via
|
|
794
|
+
// the shared builder; the merged_from trail lives in the audit entry below.
|
|
795
|
+
const sha1 = hashContent(combinedText);
|
|
796
|
+
const formatted = writeBullet({
|
|
797
|
+
id: newId,
|
|
798
|
+
text: combinedText,
|
|
799
|
+
provenance: {
|
|
800
|
+
source: 'merge-both',
|
|
801
|
+
source_line: 1,
|
|
802
|
+
sha1,
|
|
803
|
+
write: 'merged',
|
|
804
|
+
trust: mergedTrust,
|
|
805
|
+
at: ts,
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
updatedLines.splice(insertAt, 0, ...formatted.lines.split('\n'), '');
|
|
792
809
|
|
|
793
810
|
writeFileSync(scratchpadPath, updatedLines.join('\n'), 'utf8');
|
|
794
811
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Content-fingerprint helper — the single home for the kit's content hash.
|
|
2
|
+
//
|
|
3
|
+
// Every "fingerprint this text/file content" site (provenance source_sha1,
|
|
4
|
+
// the `files` checkpoint diff key, transcript dedup, conflict-merge keys)
|
|
5
|
+
// MUST route through hashContent so the algorithm is defined in exactly one
|
|
6
|
+
// place. Eight modules previously rolled their own `createHash('sha1')`,
|
|
7
|
+
// which (a) let the algorithm drift per-site and (b) tripped CodeQL's
|
|
8
|
+
// js/weak-cryptographic-algorithm on each one independently.
|
|
9
|
+
//
|
|
10
|
+
// SHA-256, not SHA-1: the digests are non-cryptographic content fingerprints
|
|
11
|
+
// (dedup + change-detection), so SHA-1 was never a security flaw here — but a
|
|
12
|
+
// weak-hash sink on every site is noise that hides real findings, and the
|
|
13
|
+
// whole-convention move to SHA-256 (the user's call, D-149) removes the sink
|
|
14
|
+
// kit-wide while keeping the digest consistent across writers. The on-disk
|
|
15
|
+
// FIELD name stays `source_sha1` / `sha1` for back-compat (renaming the YAML
|
|
16
|
+
// key + db column would break existing fact files + checkpoints); only the
|
|
17
|
+
// algorithm changes. Existing `files`-table checkpoints mismatch once on the
|
|
18
|
+
// first boot after upgrade and self-heal via the normal reindex.
|
|
19
|
+
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hash text/file content to a hex digest used as a non-cryptographic
|
|
24
|
+
* fingerprint (dedup, drift-detection, provenance). UTF-8 input.
|
|
25
|
+
* @param {string} content
|
|
26
|
+
* @returns {string} 64-char lowercase hex SHA-256 digest
|
|
27
|
+
*/
|
|
28
|
+
export function hashContent(content) {
|
|
29
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
30
|
+
}
|
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
|
@@ -29,6 +29,7 @@ import { ERROR_CATEGORIES, errorResult, notFoundResult } from './result-shapes.m
|
|
|
29
29
|
import { findBulletScratchpad } from './bullet-lookup.mjs';
|
|
30
30
|
import { openIndexDb } from './index-db.mjs';
|
|
31
31
|
import { reindexBoot } from './index-rebuild.mjs';
|
|
32
|
+
import { reindex } from './reindex.mjs';
|
|
32
33
|
|
|
33
34
|
// Layer-2 review: PR-1 rejected \n / \r / : in the `reason` field as a
|
|
34
35
|
// minimum fix for the naive serializer (finding B2). PR-2's frontmatter.mjs
|
|
@@ -292,6 +293,18 @@ export function forget(opts = {}) {
|
|
|
292
293
|
},
|
|
293
294
|
});
|
|
294
295
|
|
|
296
|
+
// Task 124 (D-112): the writer owns the derived view on the DELETE path
|
|
297
|
+
// too — writeFact refreshes INDEX.md on every create (the Task-85 lesson);
|
|
298
|
+
// without this, the tombstoned fact stayed listed in INDEX.md and doctor
|
|
299
|
+
// HC-4 failed until a manual `cmk reindex` (dogfood-found 2026-06-10).
|
|
300
|
+
// Best-effort, same contract as writeFact's: the tombstone is already
|
|
301
|
+
// durable on disk, so an index hiccup must not fail the forget.
|
|
302
|
+
try {
|
|
303
|
+
reindex({ tier: match.tier, projectRoot, userDir, warn: () => {} });
|
|
304
|
+
} catch {
|
|
305
|
+
// index rebuild is best-effort; the tombstone already succeeded
|
|
306
|
+
}
|
|
307
|
+
|
|
295
308
|
// Task 110 (F-7 / D-84): reindex the project tier IN-BAND so the just-
|
|
296
309
|
// tombstoned fact stops surfacing in `cmk search` immediately — no manual
|
|
297
310
|
// `cmk reindex`, no forgotten fact resurfacing (D-85: the action completes
|
package/src/frontmatter.mjs
CHANGED
|
@@ -43,7 +43,10 @@ const LOAD_OPTIONS = Object.freeze({
|
|
|
43
43
|
|
|
44
44
|
export function parse(text) {
|
|
45
45
|
if (typeof text !== 'string') return { frontmatter: null, body: '' };
|
|
46
|
-
|
|
46
|
+
// Task 139 (D-126): \r? tolerance — a Windows clone with autocrlf=true
|
|
47
|
+
// rewrites committed memory files to CRLF, and a strict-\n boundary made
|
|
48
|
+
// every fact file invisible (cut-gate9 H1: clone reindex found 0 facts).
|
|
49
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
47
50
|
if (!m) return { frontmatter: null, body: text };
|
|
48
51
|
let frontmatter;
|
|
49
52
|
try {
|
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
REASON_CODES,
|
|
40
40
|
} from './audit-log.mjs';
|
|
41
41
|
import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
|
|
42
|
+
import { writeBullet } from './provenance.mjs';
|
|
43
|
+
import { hashContent } from './content-hash.mjs';
|
|
42
44
|
|
|
43
45
|
const MEMORY_REL = ['context', 'MEMORY.md'];
|
|
44
46
|
|
|
@@ -227,7 +229,29 @@ export async function importAnthropicMemory({
|
|
|
227
229
|
// deduplication of section headers is a v0.1.x candidate per design §16.
|
|
228
230
|
const today = ts.slice(0, 10);
|
|
229
231
|
const sectionHeader = `\n## Imported (Anthropic auto-memory, ${today})\n`;
|
|
230
|
-
|
|
232
|
+
// Task 138 (D-125): emit the CANONICAL provenance comment via the shared
|
|
233
|
+
// writeBullet builder — the hand-rolled `write_source:`-keyed comment was
|
|
234
|
+
// invisible to the reindex parser (it maps the `write:` key to the
|
|
235
|
+
// NOT-NULL observations.write_source column), so the first reindex after
|
|
236
|
+
// an import failed and search degraded to the stale index (cut-gate9 F-13).
|
|
237
|
+
const bulletLines = proposals
|
|
238
|
+
.map((p) => {
|
|
239
|
+
const sha1 = hashContent(p.text);
|
|
240
|
+
const formatted = writeBullet({
|
|
241
|
+
id: p.id,
|
|
242
|
+
text: p.text,
|
|
243
|
+
provenance: {
|
|
244
|
+
source: 'anthropic-auto-memory',
|
|
245
|
+
source_line: 1,
|
|
246
|
+
sha1,
|
|
247
|
+
write: 'imported',
|
|
248
|
+
trust: 'medium',
|
|
249
|
+
at: ts,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
return formatted.lines;
|
|
253
|
+
})
|
|
254
|
+
.join('\n');
|
|
231
255
|
mkdirSync(join(projectRoot, 'context'), { recursive: true });
|
|
232
256
|
appendFileSync(targetPath, sectionHeader + '\n' + bulletLines + '\n', 'utf8');
|
|
233
257
|
|