claude-mem-lite 6.10.3 → 6.12.0

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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "claude-mem-lite",
12
- "version": "6.10.3",
12
+ "version": "6.12.0",
13
13
  "source": "./",
14
14
  "homepage": "https://github.com/sdsrss/claude-mem-lite",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.10.3",
3
+ "version": "6.12.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/README.md CHANGED
@@ -237,6 +237,25 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
237
237
  repos/ # Shallow-cloned source repos
238
238
  ```
239
239
 
240
+ ## Upgrading to 6.11.0
241
+
242
+ **One default changes: re-enrich stops leaving part of its budget idle.** It reserves half of
243
+ each run's budget for two backfill passes and gives its main scope the rest. When the main
244
+ scope had fewer rows to enrich than its share, the remainder went unspent; it now goes to the
245
+ backfills. The daily unattended pass runs once per machine per day, over all projects
246
+ together, so with an empty main pool it now makes up to 6 of these LLM calls a day where it
247
+ made 3. The ceiling of 6 is unchanged — it was always the declared budget — and a separately
248
+ budgeted scope-classification pass, also unchanged, can add up to 6 more short calls. A manual
249
+ `claude-mem-lite optimize --run` or `mem_optimize` behaves the same way. No schema change and
250
+ no migration: an older build still opens the database, so reverting is pinning
251
+ `claude-mem-lite@6.10.3`.
252
+
253
+ **One security fix does not reach data you already have.** In some combinations of two
254
+ labelled credentials on one line — `token: <v> secret: <v>` is one, when the first value ends
255
+ in a letter — earlier versions redacted the first and stored the second as typed. Values on
256
+ separate lines were never affected. This release fixes the write path; nothing already in
257
+ your database is rewritten.
258
+
240
259
  <!-- normalize-per-project-note:start -->
241
260
  ## Upgrading to 6.8.0
242
261
 
@@ -386,6 +405,7 @@ surface — reach them through the CLI column in the second table.
386
405
  /mem <query> # Shorthand for search
387
406
  /lesson <text> # Save a non-obvious lesson to the events table (v2.31.0)
388
407
  /bug <text> # Log a known bug + repro steps to the events table (v2.31.0)
408
+ /verify # Check memories against the current code; correct stale ones after you approve
389
409
  ```
390
410
 
391
411
  ### Efficient Search Workflow
package/README.zh-CN.md CHANGED
@@ -199,6 +199,20 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
199
199
  repos/ # 浅克隆的源代码仓库
200
200
  ```
201
201
 
202
+ ## 升级到 6.11.0
203
+
204
+ **只有一个默认行为变化:re-enrich 不再让一部分预算空着。** 它为两个回填任务预留每次运行一半的
205
+ 预算,其余给主范围。以前主范围待处理的行数少于它那一份时,剩下的额度就空着不用;现在转给回填
206
+ 任务。每日后台任务是**每台机器每天一次、对所有项目合并运行**,所以主池为空时,它现在每天最多
207
+ 做 6 次这类 LLM 调用,而以前是 3 次。上限 6 没变——这一直是声明的预算;另有一个单独计预算的
208
+ 「范围分类」任务,本版未改动,最多还会再加 6 次简短调用。手动运行 `claude-mem-lite optimize --run`
209
+ 或 `mem_optimize` 行为相同。没有 schema 变更、没有迁移:旧版本仍能打开数据库,回退就是固定到
210
+ `claude-mem-lite@6.10.3`。
211
+
212
+ **有一个安全修复不会作用于你已有的数据。** 同一行里两个带标签的凭据,在某些组合下——例如
213
+ `token: <v> secret: <v>`,且第一个值以字母结尾——以前的版本会脱敏第一个、把第二个按原样存下来。
214
+ 分行写的值从未受影响。本版本修的是写入路径;数据库里已有的内容不会被改写。
215
+
202
216
  <!-- normalize-per-project-note:start -->
203
217
  ## 升级到 6.8.0
204
218
 
@@ -327,6 +341,7 @@ README 和 `docs/ARCHITECTURE.md` 都钉在它上面。)
327
341
  /mem <query> # search 的简写
328
342
  /lesson <text> # 保存非显而易见的经验到 events 表(v2.31.0)
329
343
  /bug <text> # 记录已知 bug + 复现步骤到 events 表(v2.31.0)
344
+ /verify # 对照当前代码核查记忆;你确认后才改正过期的记忆
330
345
  ```
331
346
 
332
347
  ### 高效搜索工作流
package/cli/common.mjs CHANGED
@@ -399,6 +399,13 @@ export const KNOWN_CLI_FLAGS = new Set([
399
399
  // warn-on-every-unknown-flag flip turned the omission into a false warning on a
400
400
  // documented, working command.
401
401
  'prompts-limit',
402
+ // `verify-apply --apply --digest <d>` / `--undo <backup>` — read as flags.apply / .digest / .undo in
403
+ // cli/verify-apply.mjs. Missing here, a working apply printed "Unknown flag --apply — ignored,
404
+ // it had no effect" beside its own read-back. Pinned by tests/verify-apply-cli.test.mjs.
405
+ 'apply',
406
+ 'digest',
407
+ 'print-project',
408
+ 'undo',
402
409
  // Entries here MUST be read by a `claude-mem-lite` subcommand. A flag that no
403
410
  // command reads is worse than an absent one: it converts the "ignored, it had no
404
411
  // effect" warning into silence, so the user's dropped flag reads as accepted.
@@ -0,0 +1,197 @@
1
+ // cli/verify-apply.mjs — `claude-mem-lite verify-apply <proposals.json> [--project P]`
2
+ // (dry run), `... --apply --digest <d>`, and `claude-mem-lite verify-apply --undo <backup.json>`.
3
+ //
4
+ // The write step of /verify (commands/verify.md). The agent proposes; this command is the only
5
+ // thing that writes. It defaults to a dry run that prints the new text itself and a digest, and
6
+ // --apply refuses unless it is handed that digest back: what the user approved is what lands,
7
+ // and a proposals file or a row that changed in between is refused rather than applied. All
8
+ // policy lives in lib/verify-apply-core.mjs; this file is I/O, formatting and exit codes.
9
+
10
+ import { readFileSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { DB_DIR } from '../lib/data-paths.mjs';
13
+ import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
14
+ import { inferProject } from '../utils.mjs';
15
+ import { resolveProject } from '../project-utils.mjs';
16
+ import {
17
+ parseProposals,
18
+ planVerifyApply,
19
+ planDigest,
20
+ priorVerifyApplies,
21
+ runVerifyApply,
22
+ undoVerifyBackup,
23
+ markUndone,
24
+ } from '../lib/verify-apply-core.mjs';
25
+ import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
26
+
27
+ const USAGE =
28
+ '[mem] Usage: claude-mem-lite verify-apply <proposals.json> [--project P] (dry run)\n' +
29
+ ' claude-mem-lite verify-apply <proposals.json> [--project P] --apply --digest <d>\n' +
30
+ ' claude-mem-lite verify-apply --undo <backup.json>';
31
+
32
+ const SNIPPET_CONTEXT = 60;
33
+ const BACKUP_DIR = join(DB_DIR, 'backups');
34
+ // The commands this prints must run as printed. `claude-mem-lite` is on PATH only after an
35
+ // optional global npm install (which may also be a different, stale code home), so name the
36
+ // node binary and THIS cli.mjs — the one that produced the plan.
37
+ /** A path as one shell word: as-is when it is plain, single-quoted otherwise. */
38
+ const shellWord = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
39
+ const SELF = process.argv[1] ? `node ${shellWord(process.argv[1])}` : 'claude-mem-lite';
40
+
41
+ function readJson(path, what) {
42
+ try {
43
+ return { value: JSON.parse(readFileSync(path, 'utf8')) };
44
+ } catch (e) {
45
+ return { error: `[mem] Cannot read ${what} ${path}: ${e.message}` };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * The changed region of a field, with context. The changed part is printed IN FULL, never
51
+ * clipped: the digest covers the whole text, so any cap here would let the user approve text
52
+ * they were never shown (re-review of dcc8f72, P2-2). Only unchanged context is elided.
53
+ */
54
+ function snippet(before, after) {
55
+ const a = before === null || before === undefined ? '' : String(before);
56
+ const b = String(after);
57
+ if (a === b) return [' (unchanged)'];
58
+ let start = 0;
59
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
60
+ let endA = a.length;
61
+ let endB = b.length;
62
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
63
+ endA--;
64
+ endB--;
65
+ }
66
+ const from = Math.max(0, start - SNIPPET_CONTEXT);
67
+ const lead = from > 0 ? '…' : '';
68
+ const tail = (s, end) => (end + SNIPPET_CONTEXT < s.length ? '…' : '');
69
+ return [
70
+ ` - ${lead}${a.slice(from, Math.min(a.length, endA + SNIPPET_CONTEXT))}${tail(a, endA)}`,
71
+ ` + ${lead}${b.slice(from, Math.min(b.length, endB + SNIPPET_CONTEXT))}${tail(b, endB)}`,
72
+ ];
73
+ }
74
+
75
+ function describe(p) {
76
+ const head = ` #${p.id} [${p.verdict}] ${p.action} — evidence: ${p.evidence}`;
77
+ if (p.action === 'retire') return [head, ' retired with no replacement (kept as history)'];
78
+ const fields =
79
+ p.action === 'edit'
80
+ ? Object.entries(p.set)
81
+ : ['title', 'narrative', 'lesson_learned', 'importance', 'facts', 'concepts']
82
+ .filter((k) => p[k] !== undefined)
83
+ .map((k) => [k, p[k]]);
84
+ const lines = [head];
85
+ if (p.action === 'replace') lines.push(' new memory supersedes this one; unlisted fields are copied');
86
+ for (const [k, v] of fields) lines.push(` ${k}:`, ...snippet(p.before[k], v));
87
+ return lines;
88
+ }
89
+
90
+ function undo(db, path) {
91
+ const { value, error } = readJson(path, 'backup');
92
+ if (error) return fail(error);
93
+ const { errors, restored } = undoVerifyBackup(db, value);
94
+ if (restored.length === 0 && errors.length)
95
+ return fail(`[mem] Undo refused, nothing written:\n ${errors.join('\n ')}`);
96
+ for (const r of restored) {
97
+ out(
98
+ ` #${r.id} restored${r.replacementRetired ? ` (replacement #${r.replacementRetired} retired)` : ''}`,
99
+ );
100
+ }
101
+ if (errors.length) return fail(`[mem] Undo read-back found problems:\n ${errors.join('\n ')}`);
102
+ out(`[mem] Undo complete: ${restored.length} row(s) restored.`);
103
+ // The restore is committed; marking the file only stops a second run early. If the mark
104
+ // cannot be written, a second undo is still refused (the rows no longer match the apply's
105
+ // record), so this is a warning, not a failure of the undo that already happened.
106
+ try {
107
+ atomicWriteFileSync(path, JSON.stringify(markUndone(value), null, 1));
108
+ } catch (e) {
109
+ process.stderr.write(
110
+ `[mem] Warning: undo is done, but ${path} could not be marked as undone: ${e.message}\n`,
111
+ );
112
+ }
113
+ }
114
+
115
+ export function cmdVerifyApply(db, args) {
116
+ const { positional, flags } = parseArgs(args);
117
+ if (rejectBareStringFlags(flags, ['project', 'undo', 'digest'])) return;
118
+ // The project verify-apply targets when --project is omitted, printed so /verify can export
119
+ // exactly that project (export's --project matching is fuzzy; this is not).
120
+ if (flags['print-project'] === true) return out(inferProject());
121
+ // Boolean means boolean: `--apply=false` / `--apply no` must not apply.
122
+ if (flags.apply !== undefined && flags.apply !== true)
123
+ return fail(`[mem] --apply takes no value.\n${USAGE}`);
124
+
125
+ if (flags.undo !== undefined) {
126
+ if (positional.length || flags.apply || flags.digest) return fail(USAGE);
127
+ return undo(db, flags.undo);
128
+ }
129
+
130
+ const file = positional[0];
131
+ if (!file || positional.length > 1) return fail(USAGE);
132
+ const { value, error } = readJson(file, 'proposals');
133
+ if (error) return fail(error);
134
+
135
+ const parsed = parseProposals(value);
136
+ if (parsed.errors.length)
137
+ return fail(`[mem] Invalid proposals, nothing written:\n ${parsed.errors.join('\n ')}`);
138
+
139
+ const project = flags.project ? resolveProject(db, flags.project, { mode: 'write' }) : inferProject();
140
+ const { plan, errors } = planVerifyApply(db, parsed.entries, { project });
141
+ if (errors.length) return fail(`[mem] Refused, nothing written:\n ${errors.join('\n ')}`);
142
+ const digest = planDigest(
143
+ plan,
144
+ project,
145
+ priorVerifyApplies(
146
+ BACKUP_DIR,
147
+ plan.map((p) => p.id),
148
+ ),
149
+ );
150
+
151
+ if (!flags.apply) {
152
+ out(`[mem] verify-apply plan — project ${project}, ${plan.length} change(s):`);
153
+ for (const p of plan) for (const line of describe(p)) out(line);
154
+ out(`[mem] Plan digest: ${digest}`);
155
+ out('[mem] Dry run — nothing written. After the user approves exactly this plan, run:');
156
+ out(
157
+ ` ${SELF} verify-apply ${shellWord(file)} --project ${shellWord(project)} --apply --digest ${digest}`,
158
+ );
159
+ return;
160
+ }
161
+
162
+ if (!flags.digest) {
163
+ return fail('[mem] --apply requires --digest <d> from the dry run the user approved. Nothing written.');
164
+ }
165
+ if (flags.digest !== digest) {
166
+ return fail(
167
+ '[mem] Plan digest mismatch: the proposals file or the memories changed since the dry run. ' +
168
+ 'Nothing written — re-run the dry run and get the new plan approved.',
169
+ );
170
+ }
171
+
172
+ let run;
173
+ try {
174
+ run = runVerifyApply(db, plan, { backupDir: BACKUP_DIR });
175
+ } catch (e) {
176
+ // One failure happens AFTER the commit: the undo record could not be written. The changes
177
+ // are in the database then, and saying "nothing written" would be false.
178
+ if (e.message.startsWith('applied, but')) {
179
+ return fail(
180
+ `[mem] APPLIED — the changes are in the database, but ${e.message.slice('applied, but '.length)}`,
181
+ );
182
+ }
183
+ return fail(`[mem] ${e.message}`);
184
+ }
185
+ for (const c of run.checks) {
186
+ out(
187
+ ` #${c.id} ${c.action}${c.newId ? ` → #${c.newId}` : ''}: ${c.ok ? 'ok' : `MISMATCH (${c.problems.join('; ')})`}`,
188
+ );
189
+ }
190
+ out(`[mem] Backup: ${run.backupPath}`);
191
+ out(
192
+ `[mem] To undo (only while these rows are untouched): ${SELF} verify-apply --undo ${shellWord(run.backupPath)}`,
193
+ );
194
+ if (run.checks.some((c) => !c.ok)) {
195
+ fail('[mem] Applied, but read-back found mismatches — show the MISMATCH lines above to the user.');
196
+ }
197
+ }
package/cli.mjs CHANGED
@@ -14,6 +14,7 @@ const CLI_COMMANDS = new Set([
14
14
  'update',
15
15
  'export',
16
16
  'restore',
17
+ 'verify-apply',
17
18
  'compress',
18
19
  'maintain',
19
20
  'optimize',
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: verify
3
+ description: "Use when: the user explicitly asks to verify, audit or correct their stored memories against the current code (e.g. \"check my memories for stale ones\", /verify). You check each memory with read-only repo tools and propose corrections; nothing is written until the user approves the exact plan. Not for routine recall or saving."
4
+ ---
5
+
6
+ # /verify — check memories against the code, then correct the stale ones
7
+
8
+ Memories go stale: a bug recorded as open gets fixed, a measurement gets retracted, a
9
+ mechanism is replaced. Measured on 118 live memories across 7 repos, ~10% were STALE and
10
+ ~14% PARTIAL, and most went stale within a day of being saved. No automatic pass catches
11
+ this — cheap single-shot models misjudged it (precision 0.36) and model-written corrections
12
+ were false in 28 of 72 cases. What works is YOU reading the code: you propose, the user
13
+ approves the exact plan, and `verify-apply` is the only thing that writes.
14
+
15
+ (If another plugin also defines `/verify`, this one is `/claude-mem-lite:verify`.)
16
+
17
+ ## Arguments
18
+
19
+ - `--from <YYYY-MM-DD>`: only memories saved on or after this date (default: all live).
20
+ - `--ids 12,34`: only these memories.
21
+ - `--project <name>`: a project other than the current one.
22
+
23
+ ## Step 1 — Select
24
+
25
+ Get the exact project name first. For the current project (the usual case), run from the
26
+ project's directory:
27
+
28
+ ```bash
29
+ node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" verify-apply --print-project
30
+ ```
31
+
32
+ It prints the canonical name (e.g. `dev--my-app`) that verify-apply itself will target. For
33
+ another project, use the name the user gave only if it already has the `parent--name` shape;
34
+ otherwise ask. Always pass that exact name — `export --project` matches loosely and can pick a
35
+ neighbouring project from a short name.
36
+
37
+ ```bash
38
+ node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" export --project <project> [--from <date>] > <scratch>/memories.json
39
+ ```
40
+
41
+ Every row carries `id`, `project`, `type`, `title`, `narrative`, `facts`, `lesson_learned`,
42
+ `files_modified`, `created_at`. Check that every row's `project` is the name you passed. With
43
+ `--ids`, filter the file to those ids. The repository to check them against is the current
44
+ directory for the current project; for another project, ask the user where it lives.
45
+ `files_modified` holds both absolute and repo-relative paths. Tell the user how many memories
46
+ you are about to check.
47
+
48
+ ## Step 2 — Verify (read-only)
49
+
50
+ For EACH memory, answer: *if this memory were shown to an agent working in this repo today,
51
+ would anything it asserts mislead that agent?*
52
+
53
+ 1. List its concrete, checkable claims: identifiers, file paths, `file:line` references,
54
+ constants, counts, behaviours, "X is still broken", "Y was measured at Z".
55
+ 2. Check each against the current tree (Grep, Read) and, where useful, the history since
56
+ `created_at` (`git log --since=<created_at> -- <files>`, `git log -S<token>`, `git show`).
57
+ 3. Pick one verdict:
58
+ - **VALID** — every checkable claim still holds.
59
+ - **STALE** — at least one specific claim is contradicted by the tree or history: a
60
+ renamed/removed symbol, a changed value or default, a changed behaviour, a bug presented
61
+ as open that has since been fixed, a count that moved, a measurement later retracted.
62
+ - **PARTIAL** — the main point holds; a secondary detail (a line number, a count, a minor
63
+ mechanism) is out of date.
64
+ - **UNVERIFIABLE** / **NO_CODE_CLAIM** — leave these alone.
65
+
66
+ Rules that the measurement showed matter:
67
+ - A file having changed is NOT a contradiction. You need a line, a diff hunk, a commit, or a
68
+ command's output that contradicts a specific claim.
69
+ - Output you did not see is not a contradiction. A grep that printed nothing, or output cut
70
+ off by `head` or a size limit, proves nothing — re-run it untruncated before calling a claim
71
+ stale.
72
+ - A number only a test run could confirm (a test count, a coverage figure) is a dated
73
+ measurement: stale only if a later commit or document reports a different value, not
74
+ because you cannot re-run it here.
75
+ - A record of what happened (what was measured then, what a review found) stays true as
76
+ history. It is stale only if it would mislead about the PRESENT.
77
+ - A note that already records its own fix is not stale because the fix commit landed later.
78
+ - Do not write, stash, check out or run tests while verifying.
79
+
80
+ More than ~25 memories: split them into batches and give each batch to a read-only subagent
81
+ with the rubric above; have each subagent write its results to a file with a bash heredoc and
82
+ reply with only the path. Treat a subagent's verdict as a lead: before proposing anything,
83
+ re-open every cited `file:line`, commit or command yourself.
84
+
85
+ ## Step 3 — Draft proposals (STALE and PARTIAL only)
86
+
87
+ If every memory is VALID (or UNVERIFIABLE / NO_CODE_CLAIM), stop here: report the counts to
88
+ the user and say nothing needs changing. Do not run Step 4 with an empty proposals file.
89
+
90
+ Write `<scratch>/proposals.json` — a JSON array, one entry per memory to change:
91
+
92
+ ```json
93
+ [
94
+ { "id": 52, "action": "replace", "verdict": "STALE",
95
+ "title": "...", "narrative": "...", "lesson_learned": "...",
96
+ "evidence": "7bc8ba9; hook-optimize.mjs:1371-1383" },
97
+ { "id": 64, "action": "edit", "verdict": "PARTIAL",
98
+ "set": { "narrative": "..." }, "evidence": "hook-llm.mjs:1359-1361" },
99
+ { "id": 201, "action": "retire", "verdict": "STALE", "evidence": "600c744" }
100
+ ]
101
+ ```
102
+
103
+ - **edit** — the stale part is one detail in `title`, `narrative`, `lesson_learned`,
104
+ `importance` or `concepts`. Copy the original text into `set` and change only the stale words.
105
+ - **replace** — the memory's claim is wrong but its lesson is still worth keeping, OR the
106
+ stale detail sits in `facts` (edit cannot change `facts`). Write the corrected memory; give
107
+ `facts` (an empty string drops them) or `concepts` when those are what is stale. Omitted
108
+ fields are copied from the original, which stays as history.
109
+ - **retire** — nothing in it is worth keeping (e.g. a mid-debug note about a failure fixed
110
+ minutes later).
111
+ - `evidence` is required: the commit, the `file:line`, or the command and its output (for
112
+ something outside the repo, e.g. a tool version) that shows the memory is out of date.
113
+ - `lesson_learned` is at most 500 characters. Keep the memory's language.
114
+
115
+ ## Step 4 — Show the plan and get approval
116
+
117
+ ```bash
118
+ node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" verify-apply <scratch>/proposals.json --project <project>
119
+ ```
120
+
121
+ This is a dry run: it validates every entry against the database, writes nothing, prints each
122
+ change as `-` old / `+` new text (the changed text in full), and ends with a **plan digest**
123
+ and the exact apply command.
124
+ Show the user that output as it is (do not summarise the changes away), plus your counts of
125
+ VALID / STALE / PARTIAL, and ask for approval. If the dry run refuses an entry, fix the
126
+ proposal — never work around it with `update`, `save` or `delete`. If you change the proposals
127
+ after the user saw them, run the dry run again and show the new plan: the digest changes, and
128
+ the old one will be refused.
129
+
130
+ ## Step 5 — Apply, only after the user approves that plan
131
+
132
+ Run exactly the command the dry run printed:
133
+
134
+ ```bash
135
+ node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" verify-apply <scratch>/proposals.json --project <project> --apply --digest <digest>
136
+ ```
137
+
138
+ It refuses if the proposals or the memories changed since the dry run. Otherwise it backs up
139
+ every target row, applies all entries in one transaction (all or nothing), reads each row back
140
+ and prints `ok` or `MISMATCH`, then prints the backup path and the undo command. Report the
141
+ result and the undo command to the user. Reading the exit status:
142
+ - exit 0 — applied, every row read back `ok`.
143
+ - exit 1 with `MISMATCH` lines — applied, but a row did not read back as planned. Show those
144
+ lines; do not re-run the apply.
145
+ - exit 1 with a message starting `APPLIED` — the changes are in the database, but no undo
146
+ record could be written. Say exactly that.
147
+ - any other exit 1 — nothing was written. Say why.
148
+
149
+ Undo (`verify-apply --undo <backup>`) only works while the changed rows' content and state are
150
+ as the apply left them (usage counters such as access counts do not count): it refuses once any
151
+ of them changes again — an edit, a supersede, or a routine background pass (importance decay,
152
+ alias or concept backfill) — and it runs at most once. It then prints `Undo complete`; a `Warning: … could not be marked as undone` line means
153
+ the undo still happened. After an undo, the earlier apply command is refused: applying the
154
+ same changes again needs a new dry run and the user's approval of its new digest. That refusal
155
+ rests on the backup file: once it is deleted, the earlier command would match again, so never
156
+ re-run an apply command from an earlier approval.