@lh8ppl/claude-memory-kit 0.3.1 → 0.3.3
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 -4
- package/package.json +2 -2
- package/src/auto-extract.mjs +16 -5
- package/src/decisions-journal.mjs +223 -0
- package/src/digest.mjs +89 -0
- package/src/forget.mjs +6 -0
- package/src/import-claude-md.mjs +7 -2
- package/src/index-rebuild.mjs +91 -3
- package/src/inject-context.mjs +16 -6
- package/src/lazy-compress.mjs +81 -0
- package/src/mcp-server.mjs +9 -1
- package/src/read-core.mjs +65 -3
- package/src/remember-core.mjs +15 -15
- package/src/sanitize.mjs +30 -0
- package/src/search.mjs +224 -6
- package/src/session-end-tasks.mjs +40 -3
- package/src/subcommands.mjs +71 -5
- package/template/.claude/skills/memory-search/SKILL.md +20 -0
- package/template/CLAUDE.md.template +1 -0
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { compressSession } from './compress-session.mjs';
|
|
36
36
|
import { autoPersona } from './auto-persona.mjs';
|
|
37
37
|
import { graduateAllScratchpads } from './graduate-session.mjs';
|
|
38
|
+
import { syncDecisionsJournal } from './decisions-journal.mjs';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Run the two independent SessionEnd Haiku passes concurrently.
|
|
@@ -45,7 +46,7 @@ import { graduateAllScratchpads } from './graduate-session.mjs';
|
|
|
45
46
|
* @param {() => object} opts.makeBackend - factory returning a fresh CompressorBackend
|
|
46
47
|
* per call (each concurrent pass gets its own instance — no shared state).
|
|
47
48
|
* @param {string} [opts.now] - ISO timestamp override (tests).
|
|
48
|
-
* @returns {Promise<{compressOutcome: PromiseSettledResult, personaOutcome: PromiseSettledResult, graduationOutcome: PromiseSettledResult}>}
|
|
49
|
+
* @returns {Promise<{compressOutcome: PromiseSettledResult, personaOutcome: PromiseSettledResult, graduationOutcome: PromiseSettledResult, journalOutcome: PromiseSettledResult}>}
|
|
49
50
|
*/
|
|
50
51
|
export async function runSessionEndTasks({ projectRoot, userDir, makeBackend, now }) {
|
|
51
52
|
const [compressOutcome, personaOutcome] = await Promise.allSettled([
|
|
@@ -75,7 +76,30 @@ export async function runSessionEndTasks({ projectRoot, userDir, makeBackend, no
|
|
|
75
76
|
graduationOutcome = { status: 'rejected', reason: err };
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// Task 159 (D-169): auto-sync the decision journal. This is what makes
|
|
80
|
+
// DECISIONS.md "automatic" (D-164) — Task 147 BUILT the append logic but wired
|
|
81
|
+
// it to ONLY the manual `cmk digest`, so the journal never populated on its own.
|
|
82
|
+
// Same shape as the graduation sweep: SEQUENTIAL, pure local file I/O (reads the
|
|
83
|
+
// type:project fact files auto-extract wrote per-turn → rewrites DECISIONS.md),
|
|
84
|
+
// no Haiku/network (~175ms), no hook-ceiling risk, wrapped so a throw can't reject
|
|
85
|
+
// the hook. DISJOINT from compress (sessions/ tree) + persona (user-tier) +
|
|
86
|
+
// graduation (persona scratchpads) — nothing else in the block touches DECISIONS.md,
|
|
87
|
+
// so no lock contention. Session-end is the natural "this session's decisions
|
|
88
|
+
// landed → render them" boundary (squad's session-end Scribe instinct, made
|
|
89
|
+
// deterministic — the kit's typed-fact substrate needs no LLM to merge).
|
|
90
|
+
// syncDecisionsJournal is already best-effort (its own try/catch returns
|
|
91
|
+
// {written:false,error}); the wrapper here guards the unexpected synchronous throw.
|
|
92
|
+
let journalOutcome;
|
|
93
|
+
try {
|
|
94
|
+
journalOutcome = {
|
|
95
|
+
status: 'fulfilled',
|
|
96
|
+
value: syncDecisionsJournal({ projectRoot, now }),
|
|
97
|
+
};
|
|
98
|
+
} catch (err) {
|
|
99
|
+
journalOutcome = { status: 'rejected', reason: err };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { compressOutcome, personaOutcome, graduationOutcome, journalOutcome };
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
/**
|
|
@@ -86,7 +110,7 @@ export async function runSessionEndTasks({ projectRoot, userDir, makeBackend, no
|
|
|
86
110
|
* @param {{compressOutcome: PromiseSettledResult, personaOutcome: PromiseSettledResult}} outcomes
|
|
87
111
|
* @returns {string[]}
|
|
88
112
|
*/
|
|
89
|
-
export function summarizeSessionEnd({ compressOutcome, personaOutcome, graduationOutcome }) {
|
|
113
|
+
export function summarizeSessionEnd({ compressOutcome, personaOutcome, graduationOutcome, journalOutcome }) {
|
|
90
114
|
const lines = [];
|
|
91
115
|
|
|
92
116
|
if (compressOutcome.status === 'fulfilled') {
|
|
@@ -123,5 +147,18 @@ export function summarizeSessionEnd({ compressOutcome, personaOutcome, graduatio
|
|
|
123
147
|
}
|
|
124
148
|
}
|
|
125
149
|
|
|
150
|
+
// journalOutcome is optional (Task 159) — pre-159 callers render no journal line.
|
|
151
|
+
if (journalOutcome) {
|
|
152
|
+
if (journalOutcome.status === 'fulfilled') {
|
|
153
|
+
const j = journalOutcome.value ?? {};
|
|
154
|
+
lines.push(
|
|
155
|
+
`cmk-compress-session: journal (written: ${j.written ?? false}, appended: ${j.appended ?? 0})\n`,
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
const e = journalOutcome.reason;
|
|
159
|
+
lines.push(`cmk-compress-session: journal sync failed: ${e?.message ?? e}\n`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
126
163
|
return lines;
|
|
127
164
|
}
|
package/src/subcommands.mjs
CHANGED
|
@@ -416,7 +416,19 @@ async function runSearch(queryParts, options) {
|
|
|
416
416
|
let mode = explicitMode ?? resolveDefaultSearchMode({ projectRoot });
|
|
417
417
|
// Task 104.2 — the L3 raw tier: `--scope transcripts` searches the
|
|
418
418
|
// separate transcript-chunk index (synthetic T: ids; no tier/trust).
|
|
419
|
+
// Task 156 — `--scope decisions` scans context/DECISIONS.md (the decision
|
|
420
|
+
// journal) for decision-history / "what did we reject" recall.
|
|
419
421
|
const scope = options?.scope ?? 'facts';
|
|
422
|
+
// Task 156 / v0.3.3 cut-gate-16: the `decisions` scope is keyword-only BY
|
|
423
|
+
// DESIGN — it scans the flat `context/DECISIONS.md` journal, which is NOT
|
|
424
|
+
// embedded (no vec table). So it can never go through the semantic backend.
|
|
425
|
+
// Coerce to keyword BEFORE the semantic block, silently — a user who has the
|
|
426
|
+
// hybrid default (from `--with-semantic`) must not see a scary
|
|
427
|
+
// "unknown-scope:decisions" warning (configured default) or hard exit-2
|
|
428
|
+
// (explicit --mode) for using a real, shipped scope. The recall just works.
|
|
429
|
+
if (scope === 'decisions') {
|
|
430
|
+
mode = SEARCH_MODES.KEYWORD;
|
|
431
|
+
}
|
|
420
432
|
let semanticBackend;
|
|
421
433
|
if (mode === SEARCH_MODES.SEMANTIC || mode === SEARCH_MODES.HYBRID) {
|
|
422
434
|
const { prepareSemanticBackend } = await import('./semantic-backend.mjs');
|
|
@@ -443,6 +455,7 @@ async function runSearch(queryParts, options) {
|
|
|
443
455
|
query,
|
|
444
456
|
mode,
|
|
445
457
|
scope,
|
|
458
|
+
projectRoot, // Task 156: the decisions scope reads context/DECISIONS.md
|
|
446
459
|
minTrust: options?.minTrust,
|
|
447
460
|
tier: options?.tier,
|
|
448
461
|
since: options?.since,
|
|
@@ -464,9 +477,15 @@ async function runSearch(queryParts, options) {
|
|
|
464
477
|
for (const hit of r.results) {
|
|
465
478
|
// Plain-text output suitable for terminal piping. Snippet uses
|
|
466
479
|
// FTS5's <b>...</b> markers; preserved as-is so callers can pipe
|
|
467
|
-
// to a TUI that renders them OR strip via sed.
|
|
468
|
-
//
|
|
469
|
-
|
|
480
|
+
// to a TUI that renders them OR strip via sed. Hits with no tier/trust
|
|
481
|
+
// (raw transcript chunks; decision-journal entries) show the scope's
|
|
482
|
+
// label instead — 'transcript' for the L3 raw tier, 'decision' for the
|
|
483
|
+
// journal (Task 156), plus a `(retracted)` marker so the "what did we
|
|
484
|
+
// reject" trail is visible at a glance.
|
|
485
|
+
let provenance;
|
|
486
|
+
if (hit.tier) provenance = `${hit.tier}/${hit.trust}`;
|
|
487
|
+
else if (r.scope === 'decisions') provenance = hit.retracted ? 'decision (retracted)' : 'decision';
|
|
488
|
+
else provenance = 'transcript';
|
|
470
489
|
console.log(
|
|
471
490
|
`${hit.id}\t${provenance}\t${hit.source_file}:${hit.source_line}\t${hit.snippet}`,
|
|
472
491
|
);
|
|
@@ -512,10 +531,19 @@ export function withReadDb(fn, deps = {}) {
|
|
|
512
531
|
}
|
|
513
532
|
}
|
|
514
533
|
|
|
515
|
-
export function runGet(ids,
|
|
534
|
+
export function runGet(ids, options = {}, _command, deps = {}) {
|
|
516
535
|
const log = deps.log ?? console.log;
|
|
517
536
|
const list = Array.isArray(ids) ? ids : [ids];
|
|
518
|
-
|
|
537
|
+
// Task 155 (D-163): `--include-tombstoned` is the HUMAN-only recovery opt-in.
|
|
538
|
+
// It's a CLI flag ONLY — the MCP mk_get tool never exposes it, so automatic
|
|
539
|
+
// recall stays tombstone-blind. projectRoot is resolved the same way
|
|
540
|
+
// withReadDb does, so the tombstone-file fallback can find the archive.
|
|
541
|
+
const includeTombstoned = options.includeTombstoned === true;
|
|
542
|
+
const projectRoot = deps.projectRoot ?? resolvePath(process.cwd());
|
|
543
|
+
const rows = withReadDb(
|
|
544
|
+
(db) => getObservations(db, list, { includeTombstoned, projectRoot }),
|
|
545
|
+
deps,
|
|
546
|
+
);
|
|
519
547
|
log(JSON.stringify(rows, null, 2));
|
|
520
548
|
// All-missing/invalid → exit 2 (lets a script tell "nothing matched" from a hit).
|
|
521
549
|
if (rows.length > 0 && rows.every((r) => r.error)) process.exitCode = 2;
|
|
@@ -869,6 +897,32 @@ function runReindex(options /* , command */) {
|
|
|
869
897
|
}
|
|
870
898
|
}
|
|
871
899
|
|
|
900
|
+
/**
|
|
901
|
+
* `cmk digest` (Task 147) — print a regenerated, readable render of everything
|
|
902
|
+
* the kit currently knows, AND sync the append-only context/DECISIONS.md
|
|
903
|
+
* journal (the permanent decision ledger; D-161). The digest goes to stdout;
|
|
904
|
+
* the journal is a committed file the sync maintains in place.
|
|
905
|
+
*/
|
|
906
|
+
async function runDigestCli(options) {
|
|
907
|
+
const projectRoot = resolvePath(process.cwd());
|
|
908
|
+
const { digest } = await import('./digest.mjs');
|
|
909
|
+
const { syncDecisionsJournal } = await import('./decisions-journal.mjs');
|
|
910
|
+
|
|
911
|
+
// Keep the permanent decision journal current (append-only; best-effort —
|
|
912
|
+
// a journal hiccup must not break the digest render).
|
|
913
|
+
const sync = syncDecisionsJournal({ projectRoot });
|
|
914
|
+
|
|
915
|
+
console.log(digest({ projectRoot }));
|
|
916
|
+
|
|
917
|
+
if (sync.written) {
|
|
918
|
+
console.log(`\ncontext/DECISIONS.md updated (+${sync.appended} bytes) — the append-only decision journal.`);
|
|
919
|
+
} else if (sync.error) {
|
|
920
|
+
console.error(`\n(decision journal not updated: ${sync.error})`);
|
|
921
|
+
} else {
|
|
922
|
+
console.log('\ncontext/DECISIONS.md is up to date.');
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
872
926
|
/**
|
|
873
927
|
* `cmk forget <id-or-query>` — wired in Task 9. Tombstones the matching
|
|
874
928
|
* fact (moves it to <tier>/<memory|fragments>/archive/tombstones/<id>.md
|
|
@@ -1973,6 +2027,12 @@ export const subcommands = [
|
|
|
1973
2027
|
description: 'fetch full observation bodies + provenance by ID (parity with the mk_get MCP tool)',
|
|
1974
2028
|
milestone: 108,
|
|
1975
2029
|
argSpec: [{ flags: '<ids...>', description: 'one or more citation IDs (e.g. P-S79MJHFN)' }],
|
|
2030
|
+
optionSpec: [
|
|
2031
|
+
{
|
|
2032
|
+
flags: '--include-tombstoned',
|
|
2033
|
+
description: 'also recover forgotten (tombstoned) facts from the archive — human-only; the AI never reads tombstones',
|
|
2034
|
+
},
|
|
2035
|
+
],
|
|
1976
2036
|
action: runGet,
|
|
1977
2037
|
},
|
|
1978
2038
|
{
|
|
@@ -2019,6 +2079,12 @@ export const subcommands = [
|
|
|
2019
2079
|
milestone: 37,
|
|
2020
2080
|
action: runDoctorCli,
|
|
2021
2081
|
},
|
|
2082
|
+
{
|
|
2083
|
+
name: 'digest',
|
|
2084
|
+
description: 'print a readable digest of everything in memory + sync the append-only DECISIONS.md decision journal',
|
|
2085
|
+
milestone: 147,
|
|
2086
|
+
action: runDigestCli,
|
|
2087
|
+
},
|
|
2022
2088
|
{
|
|
2023
2089
|
name: 'config',
|
|
2024
2090
|
description: 'read/write kit settings (context/settings.json) without hand-editing JSON',
|
|
@@ -63,6 +63,26 @@ Hits are raw turn excerpts (dialogue + the tools the agent ran), keyed
|
|
|
63
63
|
whole turns. If something found here is durably useful, say so in the
|
|
64
64
|
summary so the caller can capture it as a proper fact.
|
|
65
65
|
|
|
66
|
+
## Decision HISTORY — the `decisions` scope
|
|
67
|
+
|
|
68
|
+
For "what did we DECIDE about X" a normal fact search (steps 1-3) is enough —
|
|
69
|
+
the decision fact carries its own **Why**. But when the question is about how a
|
|
70
|
+
decision **evolved**, what we **reject**ed or moved away from, or **why X
|
|
71
|
+
changed** ("did we ever consider Y?", "weren't we using Postgres?", "what did
|
|
72
|
+
we decide and did it change?"), search the **decision journal** — the
|
|
73
|
+
append-only `context/DECISIONS.md`, which keeps superseded + retracted entries
|
|
74
|
+
the live fact store no longer carries:
|
|
75
|
+
|
|
76
|
+
- MCP: `mk_search` with `scope: "decisions"`.
|
|
77
|
+
- CLI: `cmk search "<topic>" --scope decisions`
|
|
78
|
+
|
|
79
|
+
Hits are decision entries keyed by their fact id, labelled `decision` (or
|
|
80
|
+
`decision (retracted)` for a reversed one). The retracted/superseded entries
|
|
81
|
+
ARE the answer to "what did we reject" — surface them explicitly, with the
|
|
82
|
+
date, so the caller sees the trail. Use this scope IN ADDITION to the fact
|
|
83
|
+
ladder when the question has a history/evolution axis; the fact search answers
|
|
84
|
+
the "current decision", the journal answers "how it got there".
|
|
85
|
+
|
|
66
86
|
## When the query is vague
|
|
67
87
|
|
|
68
88
|
If you cannot form a concrete query, look at recent activity first, then
|
|
@@ -29,6 +29,7 @@ The `cmk doctor` health checks verify each layer is wired correctly: install int
|
|
|
29
29
|
The snapshot injected at session start is a **bounded hot index, not everything** — there is a deeper, queryable archive. When a question is "what did we decide / what's our X / how does the user work / what's the setup / **how is this project structured or built / where does X live / what's the architecture**," **query your memory instead of re-deriving the answer from scratch** — the structure is a recorded decision, recall it before re-reading the files to reconstruct it:
|
|
30
30
|
|
|
31
31
|
- **`cmk search "<topic>"`** — find any captured fact (decisions, preferences, config, lessons) across the project + user tiers.
|
|
32
|
+
- **`cmk search "<topic>" --scope decisions`** — the append-only **decision journal** (`context/DECISIONS.md`). Use it for decision **history / evolution** — "what did we reject", "did X change", "why did we move away from Y" — it keeps superseded + retracted decisions the live fact store drops. (A plain `cmk search` answers "what's the current decision"; this answers "how it got there".)
|
|
32
33
|
- **`context/memory/<type>_<slug>.md`** — the granular fact archive with full **Why / How** rationale (`context/memory/INDEX.md` lists them).
|
|
33
34
|
- **`~/.claude-memory-kit/` (`USER.md` / `HABITS.md` / `LESSONS.md`)** — how this user works across *all* their projects.
|
|
34
35
|
|