claude-mem-lite 6.19.4 → 6.21.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.
Files changed (66) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +57 -21
  4. package/README.zh-CN.md +45 -20
  5. package/adopt-cli.mjs +156 -31
  6. package/adopt-content.mjs +20 -7
  7. package/bash-utils.mjs +45 -7
  8. package/claudemd.mjs +132 -55
  9. package/cli/common.mjs +11 -1
  10. package/commands/adopt.md +16 -6
  11. package/commands/unadopt.md +21 -11
  12. package/haiku-client.mjs +115 -27
  13. package/hook-context.mjs +14 -8
  14. package/hook-episode.mjs +75 -7
  15. package/hook-handoff.mjs +98 -7
  16. package/hook-llm.mjs +22 -30
  17. package/hook-memory.mjs +4 -8
  18. package/hook-optimize.mjs +27 -5
  19. package/hook-shared.mjs +89 -1
  20. package/hook-update.mjs +5 -2
  21. package/hook.mjs +345 -45
  22. package/install.mjs +187 -30
  23. package/lib/bash-file-targets.mjs +16 -1
  24. package/lib/citation-tracker.mjs +71 -2
  25. package/lib/cite-back-hint.mjs +52 -4
  26. package/lib/cooldown-path.mjs +13 -0
  27. package/lib/data-paths.mjs +23 -1
  28. package/lib/deferred-work.mjs +1 -1
  29. package/lib/delete-core.mjs +34 -5
  30. package/lib/export-columns.mjs +1 -0
  31. package/lib/git-state.mjs +10 -1
  32. package/lib/handoff-constants.mjs +4 -0
  33. package/lib/hook-prune.mjs +53 -17
  34. package/lib/hook-stdin.mjs +7 -1
  35. package/lib/llm-provider-probe.mjs +50 -1
  36. package/lib/local-steering.mjs +386 -0
  37. package/lib/maintain-core.mjs +206 -38
  38. package/lib/mcp-ownership.mjs +23 -0
  39. package/lib/observation-write.mjs +6 -1
  40. package/lib/plan-reader.mjs +2 -4
  41. package/lib/private-strip.mjs +31 -19
  42. package/lib/project-rekey.mjs +178 -0
  43. package/lib/prompt-admission.mjs +68 -0
  44. package/lib/quiet-scope.mjs +45 -3
  45. package/lib/recall-core.mjs +36 -7
  46. package/lib/save-nudge.mjs +3 -2
  47. package/lib/search-core.mjs +4 -0
  48. package/lib/task-reader.mjs +3 -5
  49. package/lib/tmp-fixture-sweep.mjs +2 -1
  50. package/lib/verify-apply-core.mjs +9 -2
  51. package/mem-cli.mjs +52 -22
  52. package/memdir.mjs +45 -1
  53. package/npm-shrinkwrap.json +2 -2
  54. package/package.json +5 -1
  55. package/project-utils.mjs +24 -4
  56. package/schema.mjs +32 -1
  57. package/scripts/post-tool-recall.js +5 -3
  58. package/scripts/post-tool-use.sh +46 -20
  59. package/scripts/pre-tool-recall.js +13 -6
  60. package/scripts/prompt-search-utils.mjs +5 -27
  61. package/scripts/setup.sh +7 -4
  62. package/search-scoring.mjs +13 -6
  63. package/server.mjs +58 -9
  64. package/source-files.mjs +6 -0
  65. package/tool-schemas.mjs +14 -1
  66. package/utils.mjs +43 -1
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "claude-mem-lite",
12
- "version": "6.19.4",
12
+ "version": "6.21.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. FTS5 BM25 keyword 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.19.4",
3
+ "version": "6.21.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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
@@ -88,7 +88,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
88
88
  - **Observation relations** -- Bidirectional links between related observations based on file overlap
89
89
  - **User prompt capture** -- Records user prompts via UserPromptSubmit hook for intent tracking
90
90
  - **Read file tracking** -- Tracks files read during sessions for richer episode context
91
- - **Zero data loss** -- If LLM fails, observations are saved with degraded (inferred) metadata instead of being discarded
91
+ - **Degraded fallback when the LLM fails** -- An episode the deterministic rules already rate notable (an error fixed, a config or schema file changed) is saved with inferred metadata instead of being discarded. A routine edit the rules rate as noise (`Modified a.js, b.js` with no lesson) is dropped — as it is when the model answers without a lesson — so a failing provider loses those episodes; `claude-mem-lite doctor` warns when the `claude` CLI it would call does not resolve
92
92
  - **Two-tier dedup** -- Jaccard similarity (5-minute window) + MinHash signatures (7-day cross-session window) prevent duplicates
93
93
  - **Synonym expansion** -- Abbreviations like `K8s`, `DB`, `auth` automatically expand to full forms in FTS5 search (100+ pairs including CJK↔EN cross-language mappings)
94
94
  - **CJK synonym extraction** -- Unsegmented Chinese text is scanned for known vocabulary words (数据库→database, 搜索→search, etc.) enabling cross-language memory recall
@@ -167,9 +167,9 @@ Plugin mode manages its own hooks/runtime. On session start it only **checks and
167
167
 
168
168
  > **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/bug`, `/adopt`) all run from the plugin with no second step. The slash commands invoke the bundled CLI by an absolute path resolved from the plugin directory (`node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <cmd>`), so they work without anything on your `PATH`. A global `claude-mem-lite` **shell** command (for running queries yourself in a terminal) is **optional** — `npm i -g claude-mem-lite` — and is a *separate* npm install: the plugin's auto-update does **not** refresh it, so re-run `npm i -g claude-mem-lite@latest` if you want that shell command kept in sync. You do **not** need it for the plugin to be fully functional.
169
169
 
170
- > **Auto-adopt writes into your project, on every SessionStart (v3.13+).** The plugin adds a slug-scoped **managed block** to your project's own **`<cwd>/CLAUDE.md`** — a file that is normally committed to git — plus a `<cwd>/.claude/plugin_claude_mem_lite.md` detail file. The block is a system-authority pointer that boosts Claude's proactive use of `mem_recall` / `mem_save`. Everything outside the block is preserved verbatim, and it coexists with other plugins' blocks in the same file ([details](#invited-memory-v232)). This happens on **every** SessionStart, not just the first: the sync is idempotent and re-applies the block if it is edited away, and refreshes it when the shipped template changes. It applies regardless of install path (npm, npx, `/plugin`, manual), so **no manual `/adopt` is needed**.
170
+ > **Auto-adopt no longer adds its block to your project's `CLAUDE.md` (next release after 6.19.4).** On every SessionStart the plugin delivers its steering text — the trigger table that boosts Claude's proactive use of `mem_recall` / `mem_save`. In a git repository that does not already carry the block it keeps a managed block in `CLAUDE.local.md` at the repository root (Claude Code loads that file like `CLAUDE.md`, subagents included) and adds the file to the repository's `.git/info/exclude` unless your ignore rules already cover it, so git does not list or commit it; you get a **one-time notice** the first time it is written. Claude Code reads instruction files before the plugin's startup hook runs, so the session that creates the file gets the text added to its context instead, once (its subagents do not see it). Outside git, in a repository rooted at `$HOME`, where `CLAUDE.local.md` is tracked or a symbolic link, or where the repository root is an npm package that `npm publish` would ship it with (no `"private": true`, no `files` list leaving it out, and without one no `.npmignore` naming it), nothing is written and the text is **injected** into each session's context, with a one-time notice suggesting `/adopt` (not at `$HOME`). The detail file the block points at lives in the plugin's data directory, named with a `~/` path. `.git/info/exclude` is read by git only: docker build contexts, archives and other packagers can pick `CLAUDE.local.md` up (a publishable npm package root does not keep it: a block written before the directory became a package is taken out at the next session start). Earlier versions added the block to your project's own `<cwd>/CLAUDE.md` (a file that is normally committed) plus a `<cwd>/.claude/plugin_claude_mem_lite.md` detail file, unasked, in every project you opened; those projects keep the block, and the first session of this version refreshes it because the text changed — to move one to the local file, run `claude-mem-lite unadopt` there and commit the removal. Why not inject everywhere: in our sandbox evaluation (one project, Claude Opus 5.5) the agent made 1.5 proactive memory saves per 8-session run with injected steering against 5.25 with the block in `CLAUDE.local.md` or `CLAUDE.md`, and subagents never saw injected text. It applies regardless of install path (npm, npx, `/plugin`, manual); silence both notices with `MEM_NO_ADOPT_HINT=1`.
171
171
  >
172
- > Opt out per project with `claude-mem-lite adopt --disable` (`--enable` to re-arm), globally with `export MEM_NO_AUTO_ADOPT=1`, or freeze an already-adopted block against template refreshes with `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`. `claude-mem-lite unadopt` removes the block and the detail file. Manual `/adopt` remains available for re-applying after edits and for the `--all` batch path.
172
+ > **Want the block in `CLAUDE.md` instead** (for example to share it with your team)? Run `claude-mem-lite adopt`: it writes the slug-scoped managed block into `<cwd>/CLAUDE.md` plus the detail file, preserving everything outside the block, and removes the `CLAUDE.local.md` copy. A project that carries the block is kept in sync on **every SessionStart** — refreshed when the shipped template changes — and gets no local or injected copy on top of it, also in sessions started from a subdirectory. A `CLAUDE.local.md` block the plugin created and you deleted (or `claude-mem-lite unadopt` removed) is not written back — the text is injected instead — until `claude-mem-lite adopt --enable`. Turn steering off per project with `claude-mem-lite adopt --disable` (it also removes the `CLAUDE.local.md` block; a `CLAUDE.md` block stays until `unadopt`; `--enable` re-arms) or globally with `export MEM_NO_AUTO_ADOPT=1` (blocks already written stay and keep loading until `unadopt`); freeze the blocks against template refreshes with `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`. `claude-mem-lite unadopt` removes the `CLAUDE.md` block, the detail file, and the `CLAUDE.local.md` block with its exclude entry.
173
173
 
174
174
  ### Method 2: npx (one-liner)
175
175
 
@@ -237,6 +237,35 @@ 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.21.0
241
+
242
+ **Projects whose names are not plain ASCII get a new id, and what they stored moves once.** No
243
+ schema-version change: 6.20.0 still opens the database after this release has. Pin
244
+ `claude-mem-lite@6.20.0` before upgrading to avoid the move; after it, 6.20.0 names these
245
+ directories by their old ids again, so their moved rows are listed only with
246
+ `--project <new id>`.
247
+
248
+ - **Which projects.** Every character outside ASCII letters, digits and `_.-` used to become `-`,
249
+ so `~/projects/博客` and `~/projects/商城` were both `projects----` and shared one memory.
250
+ Letters, marks and digits of every script are now kept (`projects--博客`). An id whose parent
251
+ and directory names are both plain ASCII keeps its id byte for byte; a name with letters,
252
+ marks or digits of another script, or with a character outside the Basic Multilingual Plane
253
+ such as `🚀`, changes id.
254
+ - **At a project's first session start its data moves.** When no stored file path shows another
255
+ directory using the old id, the project takes everything under it, deferred items and session
256
+ history included. Otherwise only the memories whose file paths lie inside the directory move;
257
+ the rest stay under the old id. A one-time notice says what moved and how to list what stayed
258
+ (`claude-mem-lite recent 50 --project <old id>`).
259
+ - **Not in this release:** directories with the same parent and name in different repositories
260
+ (`~/a/packages/api` and `~/b/packages/api`) still share one id.
261
+ - **Also changed:** two sessions open in one project keep separate memory sessions (handoffs,
262
+ summaries and unsaved tool activity no longer mix); a session's follow-up prompts no longer
263
+ resume another session; maintenance hides idle memories for 7 days before queuing them for
264
+ deletion, in every project; an importance you set is no longer changed by reads, the access
265
+ boost or re-enrich; `recall` / `mem_recall` rank the current project and the exact path first
266
+ (`--project` / `project` keep one project); `<private>` fails closed on an unclosed, nested or
267
+ attributed tag. Full list: CHANGELOG.md.
268
+
240
269
  ## Upgrading to 6.19.0
241
270
 
242
271
  **Search output changes; no switch.** No schema change and no migration, so reverting is
@@ -543,12 +572,12 @@ automatically on the next SessionStart.
543
572
 
544
573
  ```bash
545
574
  claude-mem-lite adopt # install for current project
546
- claude-mem-lite adopt --all # install for every project under ~/.claude/projects/
575
+ claude-mem-lite adopt --all # legacy clean-up: strip the old memory-dir sentinel in every project
547
576
  claude-mem-lite adopt --status # list adopted/disabled projects + current gating snapshot
548
577
  claude-mem-lite adopt --dry-run # preview without writing
549
578
  claude-mem-lite adopt --disable # opt out of auto-adopt for current project (writes .mem-no-auto-adopt sentinel)
550
579
  claude-mem-lite adopt --enable # re-arm auto-adopt for current project (deletes the sentinel)
551
- claude-mem-lite unadopt # remove sentinel + doc (runtime marker stays to honor the explicit removal)
580
+ claude-mem-lite unadopt # remove the CLAUDE.md block + doc, and the CLAUDE.local.md block (not written again)
552
581
  ```
553
582
 
554
583
  Slash commands `/adopt` and `/unadopt` wrap the same CLI.
@@ -580,24 +609,31 @@ Slash commands `/adopt` and `/unadopt` wrap the same CLI.
580
609
  `/exit` + fresh session is enough. Same caveat applies to `unadopt`.
581
610
 
582
611
  **Safety:**
583
- - Hash-guarded: editing the managed-block body yourself blocks automatic
584
- rewrites unless you pass `--force`.
612
+ - Regenerated, not hand-edit-safe: `adopt` rewrites the managed block to the shipped
613
+ template, and so does every SessionStart whenever the block differs from it — hand edits
614
+ included. Keep your own notes outside the `claude-mem-lite:begin…end` markers, or set
615
+ `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1` to freeze the block.
585
616
  - Slug-scoped & dedup-guarded: only the `claude-mem-lite:begin…end` region is
586
617
  ever rewritten, and duplicate / CRLF-orphaned copies are collapsed to one.
587
618
  Unlike the legacy `MEMORY.md` scheme there is no line-budget gate — `CLAUDE.md`
588
619
  has no truncation cap.
589
- - **Auto-adopt runs on EVERY SessionStart, for any install path (v2.82.1+;
590
- target moved from the memdir to `<cwd>/CLAUDE.md` in v3.13).** The sync is
591
- idempotent — it re-applies the managed block if it was edited away and
592
- refreshes it when the shipped template changes (freeze with
593
- `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`). Per-project opt-out: `claude-mem-lite adopt --disable`
594
- (writes a durable `<memdir>/.mem-no-auto-adopt` sentinel that survives marker
595
- deletion / plugin reinstalls). Global opt-out: `MEM_NO_AUTO_ADOPT=1`.
596
- Pre-v2.82.1 the `CLAUDE_PLUGIN_ROOT` gate left auto-adopt unreachable for
597
- every `install.mjs`-written hook (the common path) — see CHANGELOG v2.82.1.
620
+ - **Auto-adopt runs on EVERY SessionStart, for any install path, and no longer adds
621
+ the block to `CLAUDE.md` (next release after 6.19.4).** A git project without the
622
+ block gets it in `CLAUDE.local.md` at the git root (kept out of git via
623
+ `.git/info/exclude`), or in SessionStart context outside git; a project that
624
+ carries the `CLAUDE.md` block (explicit `adopt`, or an older version's auto-adopt)
625
+ has it kept in sync — refreshed when the shipped template changes (freeze with
626
+ `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`) — and gets no local or injected copy, also in
627
+ sessions started from a subdirectory. A `CLAUDE.local.md` block the plugin created
628
+ and you (or `unadopt`) removed is not written back; `adopt --enable` re-arms it.
629
+ Per-project opt-out: `claude-mem-lite adopt --disable` (writes a durable
630
+ `<memdir>/.mem-no-auto-adopt` sentinel that survives marker deletion / plugin
631
+ reinstalls; run at the repository root it also covers sessions started in its
632
+ subdirectories; it removes the local block).
633
+ Global opt-out: `MEM_NO_AUTO_ADOPT=1` (blocks already written stay until `unadopt`).
598
634
  - The fallback hook layer is never removed from source — conditional trim is
599
- runtime-gated on sentinel presence, so projects without adoption get the
600
- full verbose output.
635
+ runtime-gated on steering being delivered (block present, or injection on), so
636
+ projects that opted out get the full verbose output.
601
637
 
602
638
  See [the invited-memory design][invited-memory] for the full design (including the
603
639
  reusable template other plugins can follow). It is a development-time document and
@@ -703,7 +739,7 @@ Episodes are batched related operations (edits to the same file group) that get
703
739
  Episode buffer -> Flush to JSON -> claude -p --model haiku -> Structured observation -> SQLite
704
740
  ```
705
741
 
706
- Each observation includes type, title, narrative, concepts, facts, importance (1-3), and is automatically deduplicated via two tiers: Jaccard similarity (>70% within 5 minutes) and MinHash signatures (>80% within 7 days across sessions). If the LLM call fails, a degraded observation is saved with inferred metadata (zero data loss). Related observations are linked via `related_ids` based on FTS5 title similarity and file overlap.
742
+ Each observation includes type, title, narrative, concepts, facts, importance (1-3), and is automatically deduplicated via two tiers: Jaccard similarity (>70% within 5 minutes) and MinHash signatures (>80% within 7 days across sessions). If the LLM call fails, an episode the rules rate notable is saved with inferred metadata; a routine edit the rules rate as noise is dropped. Related observations are linked via `related_ids` based on FTS5 title similarity and file overlap.
707
743
 
708
744
  ## Management Commands
709
745
 
@@ -1020,8 +1056,8 @@ claude-mem-lite.
1020
1056
  | `CLAUDE_MEM_BASH_RECALL` | File recall before a Bash command that views (`cat`, `sed -n`, `head`…) or writes (`sed -i`, `cat > f`, a python patch…) a file, like the Read / Edit recall. A bash prefilter keeps Node from starting for other commands. `off` disables this leg only. | _(on)_ |
1021
1057
  | `CLAUDE_MEM_LESSON_GROUNDING` | An auto-captured event keeps its lesson only when the lesson quotes the window's own diagnosis (a failing output line, a comment the edit added, or the commit message); otherwise the row is kept without it at importance 1, below every injection face. `off` keeps unquoted lessons. | _(on)_ |
1022
1058
  | `CLAUDE_MEM_LESSON_OUTPUT_CAP` | An auto-captured lesson that shares four consecutive words with TOOL OUTPUT (a command's printed text or a tool's response, which whoever controls that output can write) is kept off the injection faces: an event keeps its row and lesson, searchable, at importance 1; a `change` observation, whose importance later reads can raise, loses the lesson, and the lesson-less row is then dropped like any other (kept only with `CLAUDE_MEM_KEEP_LOW_SIGNAL=1`). Four shared words of filler ("is not in the") count too, so a lesson quoting your own comment or commit message is demoted when it also happens to share such a run with output in the same window. The row's title is not checked. `off` restores the model's importance and the lesson. | _(on)_ |
1023
- | `MEM_NO_AUTO_ADOPT` | Global opt-out for auto-adopt (v2.82.0+). `1` prevents the per-SessionStart auto-write of the `CLAUDE.md` managed block across **all** projects. For per-project opt-out use `claude-mem-lite adopt --disable` instead (writes a durable `<memdir>/.mem-no-auto-adopt` sentinel that survives marker deletion). | _(disabled)_ |
1024
- | `MEM_NO_ADOPT_HINT` | Silences the one-line "Invited-memory 未启用:`claude-mem-lite adopt`…" hint that SessionStart appends when the current project hasn't been adopted. Since v2.82.1 auto-adopt runs on every SessionStart for any install path, so this hint typically surfaces only when you've explicitly opted out (`MEM_NO_AUTO_ADOPT=1` or `claude-mem-lite adopt --disable`). | _(disabled)_ |
1059
+ | `MEM_NO_AUTO_ADOPT` | Global opt-out for auto-adopt (v2.82.0+). `1` stops auto-adopt across **all** projects — no injected steering, no new `CLAUDE.local.md`, and no sync of an existing `CLAUDE.md` or `CLAUDE.local.md` block (those stay, and keep loading, until `claude-mem-lite unadopt`). For per-project opt-out use `claude-mem-lite adopt --disable` instead (writes a durable `<memdir>/.mem-no-auto-adopt` sentinel that survives marker deletion). | _(disabled)_ |
1060
+ | `MEM_NO_ADOPT_HINT` | Silences the one-time notices shown to you the first time a project gets `CLAUDE.local.md` or is steered by injection (the latter suggests `/adopt`, except at `$HOME`), and the one-line "Invited-memory 未启用:`claude-mem-lite adopt`…" hint that SessionStart appends when the current project hasn't been adopted. Since v2.82.1 auto-adopt runs on every SessionStart for any install path, so this hint typically surfaces only when you've explicitly opted out (`MEM_NO_AUTO_ADOPT=1` or `claude-mem-lite adopt --disable`). | _(disabled)_ |
1025
1061
 
1026
1062
  ### What gets injected into your context
1027
1063
 
package/README.zh-CN.md CHANGED
@@ -67,7 +67,7 @@
67
67
  - **观察关联** -- 基于文件重叠自动建立观察之间的双向链接
68
68
  - **用户提示捕获** -- 通过 UserPromptSubmit 钩子记录用户提示,追踪用户意图
69
69
  - **Read 文件追踪** -- 追踪会话中读取的文件,丰富 episode 上下文
70
- - **零数据丢失** -- LLM 失败时,使用推断的元数据保存降级记录,而非丢弃
70
+ - **LLM 失败时的降级保存** -- 规则已判定为有价值的 episode(修掉了报错、改了配置或 schema 文件)用推断的元数据保存,而非丢弃;规则判为噪声的日常编辑(`Modified a.js, b.js` 且没有 lesson)会被丢掉——模型成功但没给 lesson 时也一样——所以 LLM 不可用期间这类 episode 会丢失。`claude-mem-lite doctor` 会在要调用的 `claude` CLI 不存在时告警
71
71
  - **两级去重** -- Jaccard 相似度(5 分钟窗口)+ MinHash 签名(7 天跨会话窗口)双重防重
72
72
  - **同义词扩展** -- 缩写如 `K8s`、`DB`、`auth` 在 FTS5 搜索时自动扩展为全称(48+ 对)
73
73
  - **伪相关反馈(PRF)** -- 首轮结果作为种子扩展查询,提升召回率
@@ -154,9 +154,9 @@ node install.mjs install
154
154
  1. **安装依赖** -- `npm install --omit=dev`(编译原生 `better-sqlite3`)
155
155
  2. **注册 MCP 服务器** -- `mem-lite` 服务器,包含 18 个工具(9 个核心通过 `tools/list` 暴露 + 9 个隐藏但可调;完整表见 Usage 段)。v2.78 前服务器名为通用的 `mem`,现已改名为 `mem-lite` 避免与用户其它 `.mcp.json` 冲突;工具名(`mem_search`/`mem_recall` 等)保持不变。
156
156
 
157
- > **自动 adopt 会写进你的项目,且每次 SessionStart 都跑(v3.13+)。** 插件向**项目自己的 `<cwd>/CLAUDE.md`**(通常是会进 git 的文件)写入一个 slug 限定的**托管块**,外加 `<cwd>/.claude/plugin_claude_mem_lite.md` 详情文件。该块是一条提升 Claude 主动调用 `mem_recall` / `mem_save` 的 system-authority 指针;块以外的内容逐字保留,也能与其它插件的块共存于同一文件。这是**每次** SessionStart 都做的幂等同步,不只是第一次——块被删掉会重新写回,出货模板变了会刷新。**任何安装路径都生效**(npm、npx、`/plugin`、手动),**无需再手动跑 `/adopt`**。
157
+ > **自动 adopt 不再把托管块加进你项目的 `CLAUDE.md`(6.19.4 之后的下一个版本起)。** 每次 SessionStart,插件都会送达引导文本(提升 Claude 主动调用 `mem_recall` / `mem_save` 的触发表)。在还没有托管块的 git 仓库里,它把托管块写进仓库根目录的 `CLAUDE.local.md`(Claude Code 像加载 `CLAUDE.md` 一样加载它,子代理也能看到),并把这个文件加进仓库的 `.git/info/exclude`(你的 ignore 规则已覆盖时不加),所以 git 不列出、也不提交它;第一次写入时你会看到一条**一次性提示**。Claude Code 在插件的启动钩子运行之前就读取了指令文件,所以创建这个文件的那个会话改为在上下文里注入一次引导(该会话的子代理看不到)。不在 git 仓库里、仓库根是 `$HOME`、`CLAUDE.local.md` 已被 git 跟踪或是符号链接,或者仓库根是一个 `npm publish` 会把它带上的 npm 包(没有 `"private": true`,没有把它排除在外的 `files` 列表,没有 `files` 列表时也没有列出它的 `.npmignore`)时,什么都不写,改为每个会话把引导**注入**上下文,并一次性提示可以运行 `/adopt`(在 `$HOME` 下不提示)。托管块指向的详情文件放在插件自己的数据目录,以 `~/` 路径书写。`.git/info/exclude` 只对 git 生效:docker 构建上下文、归档和其他打包工具都可能把 `CLAUDE.local.md` 打进去(可发布的 npm 包根目录不会保留这个文件:目录变成 npm 包之前写入的托管块,会在下一次会话启动时移除)。旧版本会在你打开的每个项目里,未经询问就向项目自己的 `<cwd>/CLAUDE.md`(通常会进 git)写入托管块,外加 `<cwd>/.claude/plugin_claude_mem_lite.md` 详情文件;这些项目保留托管块,本版本的第一个会话会因为文本变化刷新它——想把某个项目迁到本地文件,在那里运行 `claude-mem-lite unadopt` 并提交这次删除。为什么不全部改成注入:沙箱实测中(一个项目,Claude Opus 5.5),8 个会话里模型主动记录的次数,注入时平均 1.5 次,写在 `CLAUDE.local.md` 或 `CLAUDE.md` 时 5.25 次;而且子代理完全看不到注入的文本。**任何安装路径都生效**(npm、npx、`/plugin`、手动);两条提示都可以用 `MEM_NO_ADOPT_HINT=1` 关闭。
158
158
  >
159
- > 关闭方式:项目级 `claude-mem-lite adopt --disable`(重新启用用 `--enable`);全局 `export MEM_NO_AUTO_ADOPT=1`;只冻结模板刷新用 `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`。`claude-mem-lite unadopt` 可移除托管块与详情文件。手动 `/adopt` 仍保留用于编辑后重写或 `--all` 批量场景。
159
+ > **想把托管块改写进 `CLAUDE.md`**(例如与团队共享)?运行 `claude-mem-lite adopt`:它向 `<cwd>/CLAUDE.md` 写入 slug 限定的托管块和详情文件,块以外的内容逐字保留,并删除 `CLAUDE.local.md` 里的那份。带托管块的项目在**每次 SessionStart** 都会同步,出货模板变了会刷新,且不会再叠加本地副本或注入,从子目录启动的会话也一样。插件创建过、又被你删掉(或被 `claude-mem-lite unadopt` 删掉)的 `CLAUDE.local.md` 托管块不会被写回,改为注入,直到运行 `claude-mem-lite adopt --enable`。关闭方式:项目级 `claude-mem-lite adopt --disable`(同时删除 `CLAUDE.local.md` 托管块;`CLAUDE.md` 托管块保留到 `unadopt`;重新启用用 `--enable`);全局 `export MEM_NO_AUTO_ADOPT=1`(已写入的托管块保留并继续加载,直到 `unadopt`);只冻结模板刷新用 `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`。`claude-mem-lite unadopt` 会移除 `CLAUDE.md` 托管块、详情文件,以及 `CLAUDE.local.md` 托管块和它的 exclude 条目。
160
160
  3. **配置钩子** -- 全部七个生命周期事件:`SessionStart`、`PreCompact`、`PreToolUse`、`PostToolUse`、`PostToolUseFailure`、`Stop`、`UserPromptSubmit`
161
161
  4. **创建数据目录** -- `~/.claude-mem-lite/`(隐藏目录),存放数据库与运行时文件
162
162
  5. **自动迁移** -- 自动检测 `~/.claude-mem/`(原版 claude-mem)或 `~/claude-mem-lite/`(v0.5 前的非隐藏目录),将数据库和运行时文件迁移到 `~/.claude-mem-lite/`,原目录保持不变
@@ -199,6 +199,27 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
199
199
  repos/ # 浅克隆的源代码仓库
200
200
  ```
201
201
 
202
+ ## 升级到 6.21.0
203
+
204
+ **名字不是纯 ASCII 的项目会换一个新标识,它们存下的内容会搬一次。** 没有 schema 版本变更:
205
+ 本版本打开过的数据库,6.20.0 仍能打开。想避免搬迁,请在**升级之前**固定 `claude-mem-lite@6.20.0`;
206
+ 升级之后再回退,6.20.0 会按旧标识称呼这些目录,搬走的行只能用 `--project <新标识>` 列出。
207
+
208
+ - **哪些项目。** 以前 ASCII 字母、数字和 `_.-` 以外的字符都变成 `-`,所以 `~/projects/博客` 和
209
+ `~/projects/商城` 都是 `projects----`,共用一份记忆。现在任何文字的字母、组合符和数字都会保留
210
+ (`projects--博客`)。父目录名和目录名都是纯 ASCII 的,标识逐字节不变;名字里有其他文字的字母、
211
+ 组合符或数字,或有基本多文种平面以外的字符(如 `🚀`)时,标识会变。
212
+ - **项目第一次启动会话时搬迁数据。** 如果旧标识下没有任何文件路径显示另一个同样使用该旧标识的目录,
213
+ 就把旧标识下的全部内容搬过来,包括待办和会话历史。否则只搬文件路径落在本目录里的记忆,其余留在
214
+ 旧标识下。会提示一次搬了什么,以及怎么列出留下的内容(`claude-mem-lite recent 50 --project <旧标识>`)。
215
+ - **本版本不包含:** 不同仓库里父目录和目录名都相同的目录(`~/a/packages/api` 与 `~/b/packages/api`)
216
+ 仍共用一个标识。
217
+ - **其他变化:** 同一项目同时开两个会话,各自的交接、摘要和未保存的工具记录不再混在一起;会话里的
218
+ 跟进提示不再续上别的会话;维护会先把闲置记忆隐藏 7 天,再排队删除,所有项目一致;你设置的 importance
219
+ 不再被读取、访问提升或 re-enrich 改动;`recall` / `mem_recall` 当前项目和完全匹配的路径排在前面
220
+ (`--project` / `project` 只看一个项目);`<private>` 在未闭合、嵌套或带属性时一律按私密处理。
221
+ 完整列表见 CHANGELOG.md。
222
+
202
223
  ## 升级到 6.19.0
203
224
 
204
225
  **搜索输出有变化,没有开关。** 没有 schema 变更、不需要迁移,回退只需固定 `claude-mem-lite@6.18.0`。
@@ -451,12 +472,12 @@ instruction-following 权威。
451
472
 
452
473
  ```bash
453
474
  claude-mem-lite adopt # 当前项目注入
454
- claude-mem-lite adopt --all # 扫描 ~/.claude/projects/* 全部注入
475
+ claude-mem-lite adopt --all # 旧方案清理:在所有项目里删除旧的 memory 目录哨兵
455
476
  claude-mem-lite adopt --status # 列出已 adopt / 已禁用项目 + 当前 gate 快照
456
477
  claude-mem-lite adopt --dry-run # 只打印不写入
457
478
  claude-mem-lite adopt --disable # 当前项目关闭 auto-adopt(写 .mem-no-auto-adopt 哨兵)
458
479
  claude-mem-lite adopt --enable # 当前项目重新启用 auto-adopt(删哨兵)
459
- claude-mem-lite unadopt # 精确移除 sentinel + 详情文档(runtime marker 保留以尊重显式撤销)
480
+ claude-mem-lite unadopt # 移除 CLAUDE.md 托管块和详情文档,以及 CLAUDE.local.md 托管块(之后不再写回)
460
481
  ```
461
482
 
462
483
  Slash 命令 `/adopt` 和 `/unadopt` 是上述 CLI 的包装。
@@ -479,18 +500,22 @@ Slash 命令 `/adopt` 和 `/unadopt` 是上述 CLI 的包装。
479
500
  同理。
480
501
 
481
502
  **安全性:**
482
- - Hash 守护:你手动改了 sentinel 段 → 下一次 adopt 报 `UserEditedError`,
483
- 除非显式 `--force`。
484
- - 预算门:MEMORY.md 已 >180 行时拒绝新增(避开 Claude Code 200 行截断)。
485
- - **任何安装路径每次 SessionStart 都自动 adopt(v2.82.1+;v3.13 起写入目标由
486
- memdir 改为 `<cwd>/CLAUDE.md`)。** 同步是幂等的——托管块被删会写回,出货模板
487
- 变化会刷新(用 `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1` 冻结)。项目级关闭:
488
- `claude-mem-lite adopt --disable`(写 `<memdir>/.mem-no-auto-adopt` 哨兵,
489
- 存活于 marker 删除 / 插件重装)。全局关闭:`export MEM_NO_AUTO_ADOPT=1`。
490
- v2.82.1 前因 `CLAUDE_PLUGIN_ROOT` gate 与 `install.mjs` 写出的 hook 命令
491
- 不匹配,auto-adopt 实质 5 周零触发——见 CHANGELOG v2.82.1。
492
- - 保守 hook 层源码永不删——条件瘦身仅基于 sentinel 存在性做 runtime 判断,
493
- 未 adopt 的项目仍看完整 verbose 输出。
503
+ - 托管块会被重新生成,不保留手改:`adopt` 会把它改写成出货模板,每次 SessionStart
504
+ 只要它和模板不同也会改写——手动编辑的内容一样被覆盖。自己的笔记写在
505
+ `claude-mem-lite:begin…end` 标记之外,或设 `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1` 冻结。
506
+ - 按 slug 限定、去重:只改写 `claude-mem-lite:begin…end` 这一段,重复或 CRLF 残留的
507
+ 副本会合并成一份。旧的 `MEMORY.md` 方案才有行数预算,`CLAUDE.md` 没有截断上限。
508
+ - **任何安装路径每次 SessionStart 都自动 adopt,且不再把托管块加进 `CLAUDE.md`(6.19.4
509
+ 之后的下一个版本起)。** 没有托管块的 git 项目,引导写进 git 根目录的 `CLAUDE.local.md`
510
+ (经 `.git/info/exclude` 排除在 git 之外),不在 git 里时注入 SessionStart 上下文;带
511
+ `CLAUDE.md` 托管块的项目(显式 adopt 或旧版本写入)保持同步,出货模板变化会刷新(用
512
+ `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1` 冻结),且不会再叠加本地副本或注入,从子目录启动的会话
513
+ 也一样。插件创建过、又被你(或 `unadopt`)删掉的 `CLAUDE.local.md` 托管块不会被写回;
514
+ `adopt --enable` 可重新启用。项目级关闭:`claude-mem-lite adopt --disable`(写
515
+ `<memdir>/.mem-no-auto-adopt` 哨兵,存活于 marker 删除 / 插件重装;在仓库根目录运行时,对从子目录启动的
516
+ 会话同样生效;并删除本地托管块)。全局关闭:`export MEM_NO_AUTO_ADOPT=1`(已写入的托管块保留,直到 `unadopt`)。
517
+ - 保守 hook 层源码永不删——条件瘦身按"引导已送达"(有托管块或注入开启)做 runtime
518
+ 判断,关闭了引导的项目仍看完整 verbose 输出。
494
519
 
495
520
  完整设计见 `docs/plans/2026-04-16-invited-memory-pattern.md`(含其它插件
496
521
  可复用的模板)。
@@ -577,7 +602,7 @@ Episode 是一批相关操作(对同一组文件的编辑),由后台 LLM w
577
602
  Episode 缓冲区 -> 刷新为 JSON -> claude -p --model haiku -> 结构化观察 -> SQLite
578
603
  ```
579
604
 
580
- 每条观察包含类型、标题、叙述、概念、事实和重要度(1-3),并通过两级机制自动去重:Jaccard 相似度(5 分钟内 >70%)和 MinHash 签名(7 天跨会话 >80%)。LLM 调用失败时,使用推断的元数据保存降级记录(零数据丢失)。相关观察通过 FTS5 标题相似度和文件重叠自动建立 `related_ids` 链接。
605
+ 每条观察包含类型、标题、叙述、概念、事实和重要度(1-3),并通过两级机制自动去重:Jaccard 相似度(5 分钟内 >70%)和 MinHash 签名(7 天跨会话 >80%)。LLM 调用失败时,规则判定有价值的 episode 用推断的元数据保存降级记录,规则判为噪声的日常编辑会被丢掉。相关观察通过 FTS5 标题相似度和文件重叠自动建立 `related_ids` 链接。
581
606
 
582
607
  ## 管理命令
583
608
 
@@ -786,8 +811,8 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
786
811
  | `CLAUDE_MEM_BASH_RECALL` | 在查看(`cat`、`sed -n`、`head`…)或写入(`sed -i`、`cat > f`、python 补丁…)文件的 Bash 命令执行前做文件召回,与 Read / Edit 召回相同。bash 预过滤让其他命令不启动 Node。设为 `off` 只关闭这一路。 | _(开启)_ |
787
812
  | `CLAUDE_MEM_LESSON_GROUNDING` | 自动捕获的 event 只有在教训引用了本窗口自己的诊断文字(失败输出行、编辑新增的注释或提交信息)时才保留教训;否则保留这一行但去掉教训,importance 降为 1,低于所有注入面的门槛。设为 `off` 保留未引用原文的教训。 | _(开启)_ |
788
813
  | `CLAUDE_MEM_LESSON_OUTPUT_CAP` | 自动捕获的教训如果和**工具输出**(命令打印的文字或工具返回的内容,能控制这段输出的人就能写它)有连续 4 个词相同,就不会进入注入面:event 保留这一行和教训、仍可搜索,importance 降为 1;`change` 类 observation 的 importance 之后会被读取次数抬高,所以改为去掉教训,没有教训的这一行随后会像其他同类行一样被丢弃(只有设了 `CLAUDE_MEM_KEEP_LOW_SIGNAL=1` 才保留)。连续 4 个虚词(例如 "is not in the")也算,所以引用你自己的注释或提交信息的教训,只要碰巧和同一窗口的输出共有这样一串词,也会被降级。这一行的标题不在检查范围内。设为 `off` 恢复模型给出的 importance 和教训。 | _(开启)_ |
789
- | `MEM_NO_AUTO_ADOPT` | auto-adopt 全局关闭开关(v2.82.0+)。设为 `1` 阻止每次 SessionStart 在**所有**项目自动写入 `CLAUDE.md` 托管块。项目级关闭走 `claude-mem-lite adopt --disable`(写 `<memdir>/.mem-no-auto-adopt` 哨兵,存活于 marker 删除)。 | _(禁用)_ |
790
- | `MEM_NO_ADOPT_HINT` | 静音当前项目未 adopt 时 SessionStart 追加的那一行 "Invited-memory 未启用…" 提示。v2.82.1 起任何安装路径每次 SessionStart 都自动 adopt,所以该提示一般只在你显式 opt out(`MEM_NO_AUTO_ADOPT=1` 或 `claude-mem-lite adopt --disable`)的项目才会出现。 | _(禁用)_ |
814
+ | `MEM_NO_AUTO_ADOPT` | auto-adopt 全局关闭开关(v2.82.0+)。设为 `1` 在**所有**项目停止自动 adopt——不注入引导文本、不新建 `CLAUDE.local.md`,也不同步已有的 `CLAUDE.md` 或 `CLAUDE.local.md` 托管块(这些托管块会保留并继续加载,直到 `claude-mem-lite unadopt`)。项目级关闭走 `claude-mem-lite adopt --disable`(写 `<memdir>/.mem-no-auto-adopt` 哨兵,存活于 marker 删除)。 | _(禁用)_ |
815
+ | `MEM_NO_ADOPT_HINT` | 静音项目第一次得到 `CLAUDE.local.md`、或第一次以注入方式被引导时给你的一次性提示(后者建议运行 `/adopt`,在 `$HOME` 下除外),以及当前项目未 adopt 时 SessionStart 追加的那一行 "Invited-memory 未启用…" 提示。v2.82.1 起任何安装路径每次 SessionStart 都自动 adopt,所以该提示一般只在你显式 opt out(`MEM_NO_AUTO_ADOPT=1` 或 `claude-mem-lite adopt --disable`)的项目才会出现。 | _(禁用)_ |
791
816
 
792
817
  ## 许可证
793
818
 
package/adopt-cli.mjs CHANGED
@@ -14,10 +14,14 @@
14
14
  // every memdir). New-scheme adoption happens per-project on SessionStart (cwd known).
15
15
 
16
16
  import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
17
- import { homedir } from 'os';
18
- import { join, isAbsolute } from 'path';
17
+ import { claudeConfigDir, claudeStatePath } from './lib/data-paths.mjs';
18
+ import { join, isAbsolute, resolve } from 'path';
19
19
  import {
20
20
  memdirPath,
21
+ disableSentinelPath,
22
+ isAutoAdoptDisabled,
23
+ isAutoAdoptDisabledFor,
24
+ legacyMemdirPath,
21
25
  removePluginSection,
22
26
  removePluginDoc,
23
27
  isAdopted as memdirIsAdopted,
@@ -29,12 +33,36 @@ import {
29
33
  isAdopted as claudeMdIsAdopted,
30
34
  hasResidue as claudeMdHasResidue,
31
35
  needsRefresh,
36
+ readBlock,
32
37
  migrateLegacyMemoryDir,
33
38
  hasLegacyMemdirSentinel,
34
39
  claudeMdPath,
35
40
  detailDocPath,
36
41
  } from './claudemd.mjs';
37
42
  import { PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, buildClaudeMdBlock, getDetailDoc } from './adopt-content.mjs';
43
+ import {
44
+ localSteeringRoot,
45
+ readLocalSteering,
46
+ writeLocalSteering,
47
+ removeLocalSteering,
48
+ ensureSteeringDetailDoc,
49
+ localMdPath,
50
+ forgetLocalSteering,
51
+ tildePath,
52
+ } from './lib/local-steering.mjs';
53
+
54
+ /**
55
+ * Remove the auto-written CLAUDE.local.md block for the project at `cwd`, if there is one.
56
+ * @returns {{action: 'removed'|'partial'|'absent', residue?: string, path?: string}}
57
+ */
58
+ function dropLocalSteering(cwd) {
59
+ const root = localSteeringRoot(cwd);
60
+ if (!root) return { action: 'absent' };
61
+ // Runs even when the block is already gone (deleted by hand): removeLocalSteering then drops
62
+ // the exclude lines it added (pre-tag defect review, mutation M7).
63
+ const r = removeLocalSteering(root, PLUGIN_SLUG);
64
+ return r.action === 'absent' ? { action: 'absent' } : { ...r, path: localMdPath(root) };
65
+ }
38
66
 
39
67
  function log(msg) {
40
68
  console.log(msg);
@@ -45,7 +73,7 @@ function detectCwd() {
45
73
  }
46
74
 
47
75
  function projectsRoot() {
48
- return join(homedir(), '.claude', 'projects');
76
+ return join(claudeConfigDir(), 'projects');
49
77
  }
50
78
 
51
79
  function listAllMemdirs() {
@@ -66,7 +94,7 @@ function listAllMemdirs() {
66
94
  }
67
95
 
68
96
  function claudeConfigPath() {
69
- return join(homedir(), '.claude.json');
97
+ return claudeStatePath();
70
98
  }
71
99
 
72
100
  // Real adopted-project paths come from Claude Code's own ~/.claude.json `projects`
@@ -93,23 +121,9 @@ function hasFlag(args, flag) {
93
121
  }
94
122
 
95
123
  // ─── Per-project auto-adopt opt-out sentinel ─────────────────────────────────
96
- // `<memdir>/.mem-no-auto-adopt` is the durable, project-scoped escape hatch.
97
- // Survives marker deletion, sentinel removal, and plugin reinstalls — that's
98
- // the point: "user said no for this project" should not be reversible by
99
- // `rm ~/.claude-mem-lite/runtime/.auto-adopt-*`. Managed via
100
- // `claude-mem-lite adopt --disable` / `--enable`. silentAutoAdopt checks it
101
- // at entry and skips WITHOUT writing the runtime marker, so toggling
102
- // `--enable` re-arms auto-adopt on the next SessionStart. Kept in the memdir
103
- // (not the project tree) so it survives `unadopt` cleaning out .claude/.
104
- const DISABLE_SENTINEL_BASENAME = '.mem-no-auto-adopt';
105
-
106
- export function disableSentinelPath(memdir) {
107
- return join(memdir, DISABLE_SENTINEL_BASENAME);
108
- }
109
-
110
- export function isAutoAdoptDisabled(memdir) {
111
- return existsSync(disableSentinelPath(memdir));
112
- }
124
+ // The `.mem-no-auto-adopt` escape hatch lives in memdir.mjs since report §9-A: lib/quiet-scope.mjs
125
+ // has to ask it too (injected steering counts as adopted), and lib/ may not import this face.
126
+ export { disableSentinelPath, isAutoAdoptDisabled };
113
127
 
114
128
  /**
115
129
  * cmdAdopt — write the CLAUDE.md managed block + detail doc for the current
@@ -151,7 +165,15 @@ function adoptOne(cwd, { force, dryRun }) {
151
165
  const mig = migrateLegacyMemoryDir(cwd, PLUGIN_SLUG, { force });
152
166
  const r = writeManaged(cwd, { slug: PLUGIN_SLUG, version, block, doc });
153
167
  const migNote = mig.action === 'removed' ? ' (+migrated legacy memdir)' : '';
154
- log(`[adopt] ${cwd} → ${r.action}${migNote}`);
168
+ // CLAUDE.md now carries the block; a CLAUDE.local.md copy would load it twice.
169
+ const local = dropLocalSteering(cwd);
170
+ const localNote =
171
+ local.action === 'absent'
172
+ ? ''
173
+ : local.action === 'skipped-symlink'
174
+ ? ` (left ${local.path} alone: it is a symlink)`
175
+ : ` (+removed the block from ${local.path})`;
176
+ log(`[adopt] ${cwd} → ${r.action}${migNote}${localNote}`);
155
177
  return r;
156
178
  } catch (e) {
157
179
  log(`[adopt] ${cwd} → error: ${e.message}`);
@@ -215,14 +237,19 @@ function migrateAll(args) {
215
237
  * existing users whose marker predates v3.13 still migrate). Order:
216
238
  * 1. respect per-project `.mem-no-auto-adopt` opt-out → skip.
217
239
  * 2. migrate legacy memory-dir sentinel away (idempotent; no-op once gone).
218
- * 3. adopt the CLAUDE.md scheme if absent; else refresh if shipped content
219
- * drifted (unless CLAUDE_MEM_NO_TEMPLATE_REFRESH=1).
240
+ * 3. a managed block in CLAUDE.md → keep it in sync, refreshing if shipped content drifted
241
+ * (unless CLAUDE_MEM_NO_TEMPLATE_REFRESH=1), and drop a local copy (no double steering).
242
+ * 4. otherwise, inside a git work tree → the block in <top-level>/CLAUDE.local.md, kept
243
+ * out of commits via info/exclude; return 'local' (`written` says what changed). In a
244
+ * subdirectory, a root CLAUDE.md block → 'already-adopted', a root opt-out → 'disabled'.
245
+ * 5. otherwise (no git, $HOME, a tracked or symlinked CLAUDE.local.md, an npm-publishable
246
+ * root, any git failure) → write nothing, return 'inject' (the caller puts the steering
247
+ * into SessionStart context) — or 'already-adopted' when that file carries the block.
220
248
  * Silent: never logs, never throws. Returns { ok, action, reason } for debugLog.
221
249
  */
222
250
  export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
223
- const memdir = memdirPath(cwd);
224
251
  try {
225
- if (isAutoAdoptDisabled(memdir)) {
252
+ if (isAutoAdoptDisabledFor(cwd)) {
226
253
  return { ok: true, action: 'disabled', reason: 'disabled-by-sentinel' };
227
254
  }
228
255
  migrateLegacyMemoryDir(cwd, PLUGIN_SLUG);
@@ -231,6 +258,50 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
231
258
  const doc = getDetailDoc();
232
259
  const version = CURRENT_SENTINEL_VERSION;
233
260
 
261
+ // Report §9-A (docs/audits/20260929-sandbox-usage-eval.md): a project with NO managed
262
+ // block is no longer written into. The first SessionStart used to add CLAUDE.md and
263
+ // .claude/plugin_claude_mem_lite.md to every repository the user opened — 4 of 4 sandbox
264
+ // repos, swept into the next `git add -A` — and the startup dashboard then reported them
265
+ // as the user's uncommitted work. The same text now rides SessionStart context
266
+ // ('inject'); only an explicit `adopt` writes files. A project that already carries the
267
+ // block (adopted explicitly, or by an older version) is kept in sync exactly as before,
268
+ // including a half state whose detail doc went missing.
269
+ //
270
+ // r3 (tasks/specs/sandbox-eval-l3.md, report §8.5): injection kept the repository clean but
271
+ // cost most of the proactive memory writes (1.5 vs 5.25 per trajectory) and never reached
272
+ // subagents (0/12). Inside a git work tree the block now goes to CLAUDE.local.md, which the
273
+ // host loads like CLAUDE.md and info/exclude keeps out of commits (5.25 writes, 12/12).
274
+ const hasBlock = readBlock(cwd, PLUGIN_SLUG).body !== null;
275
+ if (!hasBlock) {
276
+ if (markerDir && markerKey) writeMarker(markerDir, markerKey);
277
+ const root = localSteeringRoot(cwd);
278
+ if (root) {
279
+ // A session started below the top-level (pre-tag claims review P2-3, P1-4): the host
280
+ // loads the root's CLAUDE.md as an ancestor, so a block there already steers this
281
+ // session; and an opt-out recorded for the root covers the file that lives there.
282
+ if (resolve(root) !== resolve(cwd)) {
283
+ if (readBlock(root, PLUGIN_SLUG).body !== null)
284
+ return { ok: true, action: 'already-adopted', reason: 'root-claude-md' };
285
+ // Off for the project means off here too: no file, no injected copy, no /adopt offer
286
+ // (delta review P2-2; lib/quiet-scope.mjs mirrors it).
287
+ if (isAutoAdoptDisabledFor(root)) return { ok: true, action: 'disabled', reason: 'root-disabled' };
288
+ }
289
+ const localBlock = buildClaudeMdBlock({ detailDocRef: tildePath(ensureSteeringDetailDoc()) });
290
+ const r = writeLocalSteering(root, {
291
+ slug: PLUGIN_SLUG,
292
+ version,
293
+ block: localBlock,
294
+ frozen: process.env.CLAUDE_MEM_NO_TEMPLATE_REFRESH === '1',
295
+ });
296
+ if (r.action !== 'refused') return { ok: true, action: 'local', written: r.action };
297
+ // A refused file that carries the block anyway (tracked, or behind a link) is loaded by
298
+ // the host: injecting it too would load it twice.
299
+ if (r.present) return { ok: true, action: 'already-adopted', reason: `local-${r.reason}` };
300
+ return { ok: true, action: 'inject', reason: `local-${r.reason}` };
301
+ }
302
+ return { ok: true, action: 'inject' };
303
+ }
304
+ dropLocalSteering(cwd);
234
305
  let action = 'already-adopted';
235
306
  if (!claudeMdIsAdopted(cwd, PLUGIN_SLUG)) {
236
307
  writeManaged(cwd, { slug: PLUGIN_SLUG, version, block, doc });
@@ -267,11 +338,29 @@ export function hasAutoAdoptMarker(markerDir, markerKey) {
267
338
  /**
268
339
  * cmdDisable — `claude-mem-lite adopt --disable [--all]`.
269
340
  * Writes `<memdir>/.mem-no-auto-adopt` so SessionStart auto-adopt skips this
270
- * project permanently. Does NOT remove an existing block — pair with `unadopt`.
341
+ * project permanently. Does NOT remove a CLAUDE.md block (the user asked for that one, by
342
+ * running adopt) — pair with `unadopt`. DOES remove the CLAUDE.local.md block, which
343
+ * auto-adopt wrote on its own: "turn the guidance off here" has to mean it stops loading.
271
344
  */
272
345
  function cmdDisable(args) {
273
346
  const all = hasFlag(args, '--all');
274
- const targets = all ? listAllMemdirs().map((m) => m.memdir) : [memdirPath(detectCwd())];
347
+ const localTargets = all ? listKnownProjectDirs() : [detectCwd()];
348
+ for (const dir of localTargets) {
349
+ const r = dropLocalSteering(dir);
350
+ if (r.action !== 'absent') log(`[adopt --disable] ${r.path} → ${r.action}`);
351
+ if (r.residue) log(` ⚠ ${r.residue}`);
352
+ }
353
+ // Known projects too, not only memdirs that already exist: Claude Code creates `memory/`
354
+ // only when its auto-memory is used, and a project without one was left armed (pre-tag
355
+ // defect review P2-4).
356
+ const targets = all
357
+ ? [
358
+ ...new Set([
359
+ ...listAllMemdirs().map((m) => m.memdir),
360
+ ...listKnownProjectDirs().map((d) => memdirPath(d)),
361
+ ]),
362
+ ]
363
+ : [memdirPath(detectCwd())];
275
364
 
276
365
  if (targets.length === 0) {
277
366
  log('[adopt --disable] no memdirs found');
@@ -304,7 +393,19 @@ function cmdDisable(args) {
304
393
  */
305
394
  function cmdEnable(args) {
306
395
  const all = hasFlag(args, '--all');
307
- const targets = all ? listAllMemdirs().map((m) => m.memdir) : [memdirPath(detectCwd())];
396
+ // Re-arm the CLAUDE.local.md block too: a block the user or `unadopt` removed is not written
397
+ // back until this forgets that it was (lib/local-steering.mjs).
398
+ for (const dir of all ? listKnownProjectDirs() : [detectCwd()]) {
399
+ const root = localSteeringRoot(dir);
400
+ if (root && forgetLocalSteering(root))
401
+ log(`[adopt --enable] ${localMdPath(root)} → will be written again`);
402
+ }
403
+ // The legacy ~/.claude memdir too: isAutoAdoptDisabledFor still honours a sentinel an earlier
404
+ // version left there, so --enable must be able to remove it.
405
+ const cwdNow = detectCwd();
406
+ const targets = all
407
+ ? listAllMemdirs().map((m) => m.memdir)
408
+ : [memdirPath(cwdNow), legacyMemdirPath(cwdNow)].filter(Boolean);
308
409
 
309
410
  if (targets.length === 0) {
310
411
  log('[adopt --enable] no memdirs found');
@@ -342,6 +443,11 @@ function statusAll() {
342
443
  log('[adopt --status] current project:');
343
444
  log(` cwd: ${cwd}`);
344
445
  log(` CLAUDE.md: ${adoptedHere ? `✓ adopted (${CURRENT_SENTINEL_VERSION})` : '✗ not adopted'}`);
446
+ const localRoot = localSteeringRoot(cwd);
447
+ const localHere = localRoot && readLocalSteering(localRoot, PLUGIN_SLUG).body !== null;
448
+ log(
449
+ ` local: ${localHere ? `✓ ${localMdPath(localRoot)} (auto-written, excluded from git)` : localRoot ? '✗ none' : '— none here: not a git work tree, or its root is $HOME or / (steering is injected at session start)'}`,
450
+ );
345
451
  if (hasLegacyMemdirSentinel(cwd, PLUGIN_SLUG)) {
346
452
  log(' legacy: ⚠ memory-dir sentinel still present (migrates on next SessionStart, or run `adopt`)');
347
453
  }
@@ -404,8 +510,21 @@ function unadoptAll(args) {
404
510
  // 1. New scheme: scrub CLAUDE.md managed blocks across known project paths.
405
511
  const projectDirs = listKnownProjectDirs();
406
512
  let blocks = 0,
407
- partial = 0;
513
+ partial = 0,
514
+ locals = 0;
408
515
  for (const dir of projectDirs) {
516
+ // The CLAUDE.local.md block auto-adopt writes (r3) is swept first and independently: a
517
+ // project carries one or the other, and either way nothing of ours should survive.
518
+ const root = localSteeringRoot(dir);
519
+ if (root && readLocalSteering(root, PLUGIN_SLUG).body !== null) {
520
+ if (dryRun) log(`[unadopt --all --dry-run] ${localMdPath(root)} → would-remove`);
521
+ else {
522
+ const lr = removeLocalSteering(root, PLUGIN_SLUG);
523
+ log(`[unadopt --all] ${localMdPath(root)} → ${lr.action}`);
524
+ if (lr.residue) log(` ⚠ ${lr.residue}`);
525
+ }
526
+ locals++;
527
+ }
409
528
  // hasResidue, not isAdopted: the sweep must also catch PARTIAL residue
410
529
  // (block without detail doc, or an orphaned doc/state sidecar) —
411
530
  // isAdopted's block-AND-doc gate skipped those projects forever.
@@ -452,7 +571,7 @@ function unadoptAll(args) {
452
571
  log('');
453
572
  const partialNote = partial > 0 ? ` (+${partial} partial-residue cleanup(s))` : '';
454
573
  log(
455
- `[unadopt --all] ${dryRun ? 'would remove' : 'removed'} ${blocks} CLAUDE.md block(s)${partialNote} across ${projectDirs.length} known project(s); ${legacy} legacy memory-dir sentinel(s) ${dryRun ? 'pending' : 'cleaned'}.`,
574
+ `[unadopt --all] ${dryRun ? 'would remove' : 'removed'} ${blocks} CLAUDE.md block(s)${partialNote} and ${locals} CLAUDE.local.md block(s) across ${projectDirs.length} known project(s); ${legacy} legacy memory-dir sentinel(s) ${dryRun ? 'pending' : 'cleaned'}.`,
456
575
  );
457
576
  if (projectDirs.length === 0) {
458
577
  log(
@@ -472,6 +591,9 @@ export function cmdUnadopt(args = []) {
472
591
 
473
592
  const cwd = detectCwd();
474
593
  if (dryRun) {
594
+ const root = localSteeringRoot(cwd);
595
+ if (root && readLocalSteering(root, PLUGIN_SLUG).body !== null)
596
+ log(`[unadopt --dry-run] would-remove the block in ${localMdPath(root)}`);
475
597
  const blockState = claudeMdHasResidue(cwd, PLUGIN_SLUG)
476
598
  ? 'would-remove CLAUDE.md block + detail doc'
477
599
  : 'no CLAUDE.md block';
@@ -488,6 +610,9 @@ export function cmdUnadopt(args = []) {
488
610
  const mig = migrateLegacyMemoryDir(cwd, PLUGIN_SLUG, { force });
489
611
  const migNote = mig.action === 'removed' ? ' (+cleaned legacy memdir)' : '';
490
612
  log(`[unadopt] ${cwd} → ${r.action}${migNote}`);
613
+ const local = dropLocalSteering(cwd);
614
+ if (local.action !== 'absent') log(`[unadopt] ${local.path} → ${local.action}`);
615
+ if (local.residue) log(` ⚠ ${local.residue}`);
491
616
  // 'partial' is the outcome that used to print as 'absent': the sidecar files are gone but
492
617
  // an unpaired sentinel still holds steering text in the user's CLAUDE.md, and only they can
493
618
  // decide where that text ends. Silence here is what let it survive every sweep.