@lh8ppl/claude-memory-kit 0.2.2 → 0.2.4
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 +5 -4
- package/package.json +1 -1
- package/src/audit-log.mjs +1 -0
- package/src/auto-extract.mjs +1 -1
- package/src/auto-persona.mjs +40 -8
- package/src/capture-turn.mjs +48 -1
- package/src/compressor.mjs +1 -1
- package/src/conflict-queue.mjs +14 -0
- package/src/doctor.mjs +53 -126
- package/src/forget.mjs +29 -0
- package/src/index-rebuild.mjs +42 -0
- package/src/install.mjs +17 -2
- package/src/mcp-server.mjs +354 -125
- package/src/merge-facts.mjs +4 -0
- package/src/persona-portability.mjs +24 -1
- package/src/read-core.mjs +87 -0
- package/src/register-crons.mjs +64 -33
- package/src/remember-core.mjs +91 -0
- package/src/result-shapes.mjs +2 -2
- package/src/review-queue.mjs +13 -0
- package/src/search.mjs +6 -5
- package/src/settings-hooks.mjs +56 -2
- package/src/spawn-bin.mjs +7 -2
- package/src/subcommands.mjs +425 -169
- package/src/weekly-curate.mjs +5 -0
- package/src/write-fact.mjs +25 -1
- package/template/.claude/skills/memory-write/SKILL.md +52 -35
- package/template/.gitignore.fragment +6 -0
- package/template/support/cron-jobs/nightly-memsearch-index.md +0 -17
- package/template/support/milvus-deploy/README.md +0 -57
- package/template/support/milvus-deploy/docker-compose.yml +0 -66
- package/template/support/scripts/memsearch-index-with-flush.sh +0 -59
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
- **Cross-project persona — the wedge (v0.2)** — when you state how you work *everywhere* ("always use uv, never pip", "from now on run the linter before committing"), the per-turn auto-extract promotes it into your **user tier** (`~/.claude-memory-kit/`) **that turn**. So a brand-new project **cold-opens already knowing your style** — layered structure, your tooling, your testing discipline — with no hand-curation and no waiting. Carry it between your own machines with `cmk persona export`/`import`, or pin a single fact across projects with `cmk lessons promote`.
|
|
8
8
|
- **Frozen snapshot at session start** — MEMORY.md + USER.md + SOUL.md + INDEX.md + today's session log inject once at the first tool call, so Claude sees your context every session without you re-telling it.
|
|
9
9
|
- **Auto-extract on every assistant turn** — a background `claude --print` subagent reads each turn and saves durable facts to memory. Durable project knowledge (setup/config, conventions, workflows, tool quirks) becomes a **rich Why/How fact file** (structured + searchable); lighter signals stay terse `MEMORY.md` bullets. Runs automatically, so the rich tier survives even when the model uses Claude Code's built-in memory instead. No manual writes needed.
|
|
10
|
-
- **Explicit capture when you want it** — say "remember this" / "from now on" / "we decided" / "forget X" (the `memory-write` skill), or run `cmk remember "<fact>"`. Both dedup, screen for secrets, abstract machine paths to `~`, and write silently.
|
|
11
|
-
- **Search + MCP** — `cmk search "<term>"` (keyword/hybrid over facts + scratchpads)
|
|
10
|
+
- **Explicit capture when you want it** — say "remember this" / "from now on" / "we decided" / "forget X" (the `memory-write` skill), or run `cmk remember "<fact>"`. Both dedup, screen for secrets, abstract machine paths to `~`, and write silently. For backtick/quote-heavy rich facts, capture them shell-safe as JSON: `cmk remember --from-file fact.json` (or `--json` from stdin) — content never touches the shell.
|
|
11
|
+
- **Search + MCP — Claude runs every memory op for you, in conversation** — `cmk search "<term>"` (keyword/hybrid over facts + scratchpads). `cmk install` registers the kit's **MCP server**, so Claude can do the whole memory surface as tools without you ever typing `cmk`: capture (`mk_remember`, rich Why/How too), recall (`mk_search` / `mk_get` / `mk_timeline` / `mk_cite`), adjust trust (`mk_trust`), promote a fact across projects (`mk_lessons_promote`), forget (`mk_forget` — previews first, then deletes on confirm), and clear the review/conflict queues (`mk_queue_list` / `mk_queue_resolve`). The tools are allow-listed on install, so they run prompt-free.
|
|
12
12
|
- **Bounded by compression** — session → daily → weekly Haiku rollups (cron or lazy-on-read) keep the snapshot small as history grows. The session-buffer rollup self-heals at session start too, so memory stays bounded even if you never cleanly close the window.
|
|
13
13
|
- **Per-project, in-repo** — `context/` lives inside your project and travels with `git clone`. Each project keeps its own memory.
|
|
14
14
|
- **9 health checks** — `cmk doctor` validates install, hook wiring, distill freshness, INDEX consistency, cron registration, and stale locks.
|
|
@@ -26,7 +26,7 @@ cmk install # scaffolds context/ + the memory-write skill AND wires the l
|
|
|
26
26
|
cmk doctor # verify, then restart Claude Code
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`cmk install` is a complete entry point: it scaffolds `context/`, drops the `memory-write` skill into `.claude/skills/` (committed — travels with `git clone`), and writes the 5 lifecycle hooks (PATH-resolved, cross-OS) into the project's `.claude/settings.json`. No separate `/plugin` step needed. Use `cmk install --no-hooks`
|
|
29
|
+
`cmk install` is a complete entry point: it scaffolds `context/`, drops the `memory-write` skill into `.claude/skills/` (committed — travels with `git clone`), and writes the 5 lifecycle hooks (PATH-resolved, cross-OS) into the project's `.claude/settings.json`. It also **registers the kit's MCP server** in `.mcp.json` and allow-lists its tools (`mcp__cmk__*`) in `.claude/settings.json`, so Claude can drive memory as tools with no per-call prompt. No separate `/plugin` step needed. Use `cmk install --no-hooks` to skip the hooks + MCP wiring (scaffold-only).
|
|
30
30
|
|
|
31
31
|
> Installing the package globally adds the `cmk` CLI **and** the installer. It's the `cmk install` *subcommand* that wires the hooks — not the bare `npm install`.
|
|
32
32
|
|
|
@@ -51,9 +51,10 @@ Most-used commands (full list via `cmk --help`):
|
|
|
51
51
|
| `cmk doctor` | Run HC-1..HC-9 health checks, surface repair commands |
|
|
52
52
|
| `cmk repair --hooks` / `--locks` / `--index` / `--all` | Idempotent self-repair |
|
|
53
53
|
| `cmk search "<query>" [--mode keyword\|semantic\|hybrid]` | Search accumulated memory (keyword default) |
|
|
54
|
+
| `cmk get <id…>` / `cmk timeline <id>` / `cmk cite <id>` / `cmk recent-activity` | Read the index back — full fact bodies + provenance, sequential context around an observation, a canonical citation link, recent changes (the CLI side of the `mk_*` MCP read tools) |
|
|
54
55
|
| `cmk roll --scope now\|today\|recent` | Manually trigger a compression pipeline |
|
|
55
56
|
| `cmk register-crons [--dry-run] [--unregister]` | Register daily + weekly jobs with cron / launchd / Task Scheduler |
|
|
56
|
-
| `cmk forget <id>` | Tombstone a fact (
|
|
57
|
+
| `cmk forget <id>` | Tombstone a fact — disappears from `cmk search` immediately, no manual reindex (audit trail preserved) |
|
|
57
58
|
| `cmk lessons promote <id> [--to USER.md\|HABITS.md]` | Promote one captured fact to your cross-project **user tier** (the safe path — sanitized, secret-screened, audited) so it applies in **every** project |
|
|
58
59
|
| `cmk disable-native-memory` / `enable-native-memory` | Opt out of Claude Code's built-in Auto Memory so the kit is your single, lean memory layer (committable — travels with `git clone`) |
|
|
59
60
|
| `cmk persona generate` | Run cross-project persona synthesis on demand (instead of waiting for the weekly pass) |
|
package/package.json
CHANGED
package/src/audit-log.mjs
CHANGED
|
@@ -30,6 +30,7 @@ export const AUDIT_LOG_SCHEMA_VERSION = 1;
|
|
|
30
30
|
// they come online; the rule is one canonical machine-parseable token per
|
|
31
31
|
// kind of audit event (not a free-text reason field).
|
|
32
32
|
export const REASON_CODES = Object.freeze({
|
|
33
|
+
FACT_CREATED: 'fact-created', // writeFact: a new fact file was written (Task 123.A — the default create audit; callers emitting a richer code opt out via audit:false)
|
|
33
34
|
DUPLICATE: 'duplicate', // writeFact: same path + same id
|
|
34
35
|
DUPLICATE_ELSEWHERE: 'duplicate-elsewhere', // writeFact: different path + same id
|
|
35
36
|
USER_REQUESTED: 'user-requested', // forget: user-initiated tombstone
|
package/src/auto-extract.mjs
CHANGED
|
@@ -796,7 +796,7 @@ export async function runAutoExtract({
|
|
|
796
796
|
// duration ≈ 25000ms = hitting the cap, not finishing) → automatic
|
|
797
797
|
// capture + persona promotion (F2) silently never ran. This call is
|
|
798
798
|
// DETACHED (fire-and-forget, never blocks the session), so a generous
|
|
799
|
-
// ceiling is free. Live-test finding (2026-06-01,
|
|
799
|
+
// ceiling is free. Live-test finding (2026-06-01, live-test-4 baseline).
|
|
800
800
|
timeoutMs: 90_000,
|
|
801
801
|
});
|
|
802
802
|
// Touch the cooldown marker IMMEDIATELY after the Haiku call
|
package/src/auto-persona.mjs
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// reproduced design §16.16's predicted failure: cross-project doctrine
|
|
5
5
|
// ("how I work everywhere" — venv-3.13, layered-backend) was captured
|
|
6
6
|
// but filed PROJECT-tier; the USER tier stayed empty, collapsing the
|
|
7
|
-
// 3-tier value prop to project+local.
|
|
7
|
+
// 3-tier value prop to project+local. The user won't hand-curate the user
|
|
8
8
|
// tier ("too much of a hassle"), so the user tier must fill itself.
|
|
9
9
|
//
|
|
10
10
|
// Posture (tasks.md 45.6 — supersedes 45.2/45.3's manual gate):
|
|
11
|
-
// OPTIMISTIC AUTO-PROMOTE.
|
|
11
|
+
// OPTIMISTIC AUTO-PROMOTE. The user (2026-05-30): "i dont want to do
|
|
12
12
|
// anything, i want it to be automatic." A synthesized doctrine that
|
|
13
13
|
// applies beyond the current project is auto-promoted to the user tier
|
|
14
14
|
// at trust:medium — no manual `cmk persona accept` step. A confidence
|
|
@@ -75,6 +75,11 @@ export const PERSONA_CANDIDATE_RE =
|
|
|
75
75
|
// userDir is passed through to listObservationSources purely to keep the
|
|
76
76
|
// U-tier resolution sandbox-scoped (never walk the real home dir —
|
|
77
77
|
// design §16.36); we then filter to tier P, the synthesis SOURCE.
|
|
78
|
+
// Byte budget for the `facts` persona corpus (Task 111 / F-2). Bounds the Haiku
|
|
79
|
+
// classifier input so a large project's whole-memory sweep can't blow the timeout.
|
|
80
|
+
// Generous (facts are high-signal) but bounded; whole facts only (see below).
|
|
81
|
+
export const PERSONA_CORPUS_BYTES = 60_000;
|
|
82
|
+
|
|
78
83
|
function assembleProjectCorpus({ projectRoot, userDir }) {
|
|
79
84
|
const sources = listObservationSources({ projectRoot, userDir });
|
|
80
85
|
const parts = [];
|
|
@@ -94,7 +99,30 @@ function assembleProjectCorpus({ projectRoot, userDir }) {
|
|
|
94
99
|
parts.push((content ?? '').trim());
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
|
-
|
|
102
|
+
// Task 111 (F-2): BOUND the corpus. Previously this joined EVERY tier-P fact
|
|
103
|
+
// + scratchpad with no cap, so on a real project with substantial memory the
|
|
104
|
+
// classifier prompt grew unbounded and the Haiku `claude --print` call blew the
|
|
105
|
+
// timeout (the reported "did not return within 50000ms"). Accumulate WHOLE
|
|
106
|
+
// facts up to a byte budget (never split a fact mid-body) and mark truncation.
|
|
107
|
+
// KNOWN LIMITATION (mirrors TRANSCRIPT_WINDOW_BYTES): facts past the budget are
|
|
108
|
+
// dropped in file-iteration order — a doctrine fact in the tail can be missed
|
|
109
|
+
// on one pass, but the weekly janitor re-runs, and some doctrine beats a
|
|
110
|
+
// timed-out zero. A value-ordered (trust/recency-first) accumulation is the
|
|
111
|
+
// follow-up if a large corpus drops doctrine.
|
|
112
|
+
const out = [];
|
|
113
|
+
let used = 0;
|
|
114
|
+
let truncated = false;
|
|
115
|
+
for (const part of parts.filter(Boolean)) {
|
|
116
|
+
const cost = Buffer.byteLength(part, 'utf8') + 2; // +2 for the '\n\n' join
|
|
117
|
+
if (used + cost > PERSONA_CORPUS_BYTES) {
|
|
118
|
+
truncated = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
out.push(part);
|
|
122
|
+
used += cost;
|
|
123
|
+
}
|
|
124
|
+
if (truncated) out.push('### …\n(corpus truncated — additional project facts omitted for this pass)');
|
|
125
|
+
return out.join('\n\n');
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
// Default size of the recent-transcript window handed to the SessionEnd persona
|
|
@@ -111,7 +139,7 @@ function assembleProjectCorpus({ projectRoot, userDir }) {
|
|
|
111
139
|
// 40k chars ≈ a long session's worth of turns ≈ ~10k tokens — trivial cost for a
|
|
112
140
|
// once-per-session call, and the classifier prompt's "IGNORE anything specific to
|
|
113
141
|
// this ONE project" instruction guards precision at the larger size (live test:
|
|
114
|
-
// clean 2/2, no false promotes). The exact bound is a
|
|
142
|
+
// clean 2/2, no false promotes). The exact bound is a live-test-9 tuning item.
|
|
115
143
|
// KNOWN LIMITATION (documented, not yet fixed): only the most-recent date-named
|
|
116
144
|
// file is read, so a session spanning midnight loses the pre-midnight turns. Rare;
|
|
117
145
|
// a multi-file read is the follow-up if it bites.
|
|
@@ -250,7 +278,7 @@ export function parsePersonaCandidates(outputText) {
|
|
|
250
278
|
*/
|
|
251
279
|
export async function autoPersona(opts = {}) {
|
|
252
280
|
const t0 = Date.now();
|
|
253
|
-
const { projectRoot, userDir, backend, now, settings, cooldownMs = DEFAULT_COOLDOWN_MS, source = 'facts' } = opts;
|
|
281
|
+
const { projectRoot, userDir, backend, now, settings, cooldownMs = DEFAULT_COOLDOWN_MS, source = 'facts', timeoutMs = 50_000 } = opts;
|
|
254
282
|
|
|
255
283
|
if (!projectRoot) {
|
|
256
284
|
return errorResult({
|
|
@@ -302,7 +330,11 @@ export async function autoPersona(opts = {}) {
|
|
|
302
330
|
instructions: buildClassifierInstructions(source),
|
|
303
331
|
preserveCitationIds: false,
|
|
304
332
|
maxOutputBytes: 4096,
|
|
305
|
-
|
|
333
|
+
// Task 111 (F-2): the timeout is caller-supplied. The SessionEnd hook path
|
|
334
|
+
// keeps the 50_000 default (it composes with the 60s SessionEnd ceiling per
|
|
335
|
+
// design §8.5 / D-42). The CLI `cmk persona generate` has NO outer hook
|
|
336
|
+
// ceiling, so it passes a generous value — the explicit command can wait.
|
|
337
|
+
timeoutMs,
|
|
306
338
|
});
|
|
307
339
|
// Spent a Haiku call — refresh the shared cooldown marker so the next
|
|
308
340
|
// gated caller backs off. (touch even on cooldownMs:0 cycles: the call
|
|
@@ -349,7 +381,7 @@ export async function autoPersona(opts = {}) {
|
|
|
349
381
|
* inferred noise. This still holds for every medium/inferred write.
|
|
350
382
|
* - trust:'high' (explicit path — Task 76 `cmk lessons promote` + Task 78
|
|
351
383
|
* inline grading of an EXPLICITLY-STATED rule). **45.4 REFINEMENT
|
|
352
|
-
* (2026-06-02, D-32 —
|
|
384
|
+
* (2026-06-02, D-32 — the user chose "latest explicit wins"):** an explicit,
|
|
353
385
|
* user-attested rule at trust:high MAY supersede an equal-trust same-topic
|
|
354
386
|
* entry (high >= high → supersede). The newest explicit statement wins,
|
|
355
387
|
* even over a hand-curated high. The original protection is unchanged for
|
|
@@ -359,7 +391,7 @@ export async function autoPersona(opts = {}) {
|
|
|
359
391
|
*/
|
|
360
392
|
// Persist low/medium-confidence (and otherwise-not-promoted) candidates to a
|
|
361
393
|
// durable review-queue FILE at <userDir>/queues/persona-review.md, so they are
|
|
362
|
-
// not lost when only returned in the response (
|
|
394
|
+
// not lost when only returned in the response (the user, 2026-05-31: "response
|
|
363
395
|
// object can get lost — i dont like it"). Dedup by canonical id against what's
|
|
364
396
|
// already in the file so repeated synthesis passes don't pile up duplicates.
|
|
365
397
|
// Returns the queue path (or null when there's nothing to write).
|
package/src/capture-turn.mjs
CHANGED
|
@@ -48,6 +48,9 @@ import {
|
|
|
48
48
|
appendFileSync,
|
|
49
49
|
readFileSync,
|
|
50
50
|
writeFileSync,
|
|
51
|
+
readdirSync,
|
|
52
|
+
statSync,
|
|
53
|
+
unlinkSync,
|
|
51
54
|
} from 'node:fs';
|
|
52
55
|
import { join } from 'node:path';
|
|
53
56
|
import { spawn } from 'node:child_process';
|
|
@@ -57,6 +60,41 @@ function dateFromIso(iso) {
|
|
|
57
60
|
return String(iso).slice(0, 10);
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
// A `.extract-<ts>.tmp` turn-file lives only for the duration of one
|
|
64
|
+
// auto-extract run (bounded by the Stop-hook ceiling, design §8.5). The owning
|
|
65
|
+
// child unlinks it in its `finally`; capture-turn unlinks it here when the spawn
|
|
66
|
+
// fails. But a child KILLED before its finally (hook ceiling), or a Windows
|
|
67
|
+
// unlink refused by a scanner, leaks the temp (cut-gate7 found 2 lingering —
|
|
68
|
+
// D-103 finding E). This janitor sweeps any `.extract-*.tmp` older than the
|
|
69
|
+
// threshold — far longer than any live run, so it can't race an in-flight child.
|
|
70
|
+
// Best-effort: a sweep hiccup must never block the capture.
|
|
71
|
+
const STALE_TURN_FILE_MS = 10 * 60 * 1000; // 10 min — well beyond the hook ceiling
|
|
72
|
+
|
|
73
|
+
export function sweepStaleTurnFiles(transcriptsDir, maxAgeMs = STALE_TURN_FILE_MS, now = Date.now()) {
|
|
74
|
+
let swept = 0;
|
|
75
|
+
if (!existsSync(transcriptsDir)) return swept;
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(transcriptsDir);
|
|
79
|
+
} catch {
|
|
80
|
+
return swept;
|
|
81
|
+
}
|
|
82
|
+
for (const name of entries) {
|
|
83
|
+
if (!name.startsWith('.extract-') || !name.endsWith('.tmp')) continue;
|
|
84
|
+
const p = join(transcriptsDir, name);
|
|
85
|
+
try {
|
|
86
|
+
if (now - statSync(p).mtimeMs > maxAgeMs) {
|
|
87
|
+
unlinkSync(p);
|
|
88
|
+
swept += 1;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// best-effort: a stat/unlink failure (already gone, or briefly locked)
|
|
92
|
+
// must not abort the sweep or the capture.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return swept;
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
// Write a `phase: 'spawn'` NDJSON entry to `<projectRoot>/context/sessions/{date}.extract.log`
|
|
61
99
|
// when the auto-extract spawn fails. This closes PR-A's class-1 audit
|
|
62
100
|
// deferral (capture-turn Door 5 observability gap). Auto-extract's own
|
|
@@ -143,7 +181,7 @@ function readLastUserTurnFromTranscript(transcriptPath) {
|
|
|
143
181
|
// (context/sessions/now.md). Before this, now.md was fed ONLY by observe-edit's
|
|
144
182
|
// file-write lines ("[ts] Write file=X lines=N"), so the SessionEnd compressor
|
|
145
183
|
// summarized a list of filenames and hallucinated content the dialogue never
|
|
146
|
-
// contained (
|
|
184
|
+
// contained (live-test-6: "Flask app: app.py" — inferred a framework from a
|
|
147
185
|
// filename). Buffering the actual user+assistant turns here means the summary
|
|
148
186
|
// reflects what was DISCUSSED. Same `## <ts> — speaker` shape as the transcript
|
|
149
187
|
// so the compressor reads it as dialogue; now.md is truncated after each compress
|
|
@@ -281,6 +319,10 @@ export function captureTurn({
|
|
|
281
319
|
// summarizes the DIALOGUE, not observe-edit's filename log. Best-effort.
|
|
282
320
|
appendConversationToNowMd({ projectRoot, ts, userTurn, assistantTurn: sanitized });
|
|
283
321
|
|
|
322
|
+
// Janitor: clear any orphaned turn-files from a prior killed/crashed child
|
|
323
|
+
// before writing this turn's (D-103 finding E). Best-effort.
|
|
324
|
+
sweepStaleTurnFiles(transcriptsDir);
|
|
325
|
+
|
|
284
326
|
const turnFile = join(transcriptsDir, `.extract-${Date.now()}.tmp`);
|
|
285
327
|
try {
|
|
286
328
|
writeFileSync(
|
|
@@ -316,6 +358,11 @@ export function captureTurn({
|
|
|
316
358
|
reason: spawnResult.reason,
|
|
317
359
|
error: spawnResult.error,
|
|
318
360
|
});
|
|
361
|
+
// NB: we do NOT unlink the turn-file here. Ownership is clean — auto-extract
|
|
362
|
+
// owns deletion (its `finally`); when the spawn fails (or a child is killed
|
|
363
|
+
// before its finally), the file becomes an orphan that the entry-sweep above
|
|
364
|
+
// reaps once it's stale (D-103 finding E). capture-turn never deletes a file
|
|
365
|
+
// it handed off, so tests can still inspect the IPC shape on the no-spawn path.
|
|
319
366
|
}
|
|
320
367
|
|
|
321
368
|
return {
|
package/src/compressor.mjs
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// Note on the allowedTools split: design.md §6.1 documents
|
|
25
25
|
// `--allowed-tools "Read"`; the code-dive note recommended tightening
|
|
26
26
|
// to fully empty per claude-remember's actual pattern. This PR
|
|
27
|
-
// implements empty per
|
|
27
|
+
// implements empty per the user's instruction (the auto-extract sub-Claude
|
|
28
28
|
// never needs Read either — the turn content arrives in the prompt).
|
|
29
29
|
|
|
30
30
|
import { spawn as defaultSpawn } from 'node:child_process';
|
package/src/conflict-queue.mjs
CHANGED
|
@@ -427,6 +427,20 @@ function parseQueue(queueText) {
|
|
|
427
427
|
*
|
|
428
428
|
* Returns { resolved: N, kept_old: N, kept_new: N, merged: N, skipped: N }.
|
|
429
429
|
*/
|
|
430
|
+
/**
|
|
431
|
+
* Pure-read list of PENDING conflict-queue entries (no mutation). Used by the MCP
|
|
432
|
+
* `mk_queue_list` tool so a "list" never rewrites the queue file — unlike
|
|
433
|
+
* resolveConflictQueue, which reserializes on every call. Returns `[]` when the
|
|
434
|
+
* queue file doesn't exist.
|
|
435
|
+
*/
|
|
436
|
+
export function listConflictQueue({ tier = 'P', projectRoot, userDir } = {}) {
|
|
437
|
+
const tierRoot = resolveTierRoot({ tier, projectRoot, userDir });
|
|
438
|
+
const queuePath = join(tierRoot, ...QUEUE_RELATIVE);
|
|
439
|
+
if (!existsSync(queuePath)) return [];
|
|
440
|
+
const { entries } = parseQueue(readFileSync(queuePath, 'utf8'));
|
|
441
|
+
return entries.filter((e) => e.fields.resolution === 'pending');
|
|
442
|
+
}
|
|
443
|
+
|
|
430
444
|
export async function resolveConflictQueue({
|
|
431
445
|
tier,
|
|
432
446
|
projectRoot,
|