claude-mem-lite 6.13.5 → 6.14.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.13.5",
12
+ "version": "6.14.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.13.5",
3
+ "version": "6.14.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
@@ -138,7 +138,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
138
138
  |----------|--------|-------|
139
139
  | **Linux** | Supported | Primary development and testing platform; the whole CI matrix runs here |
140
140
  | **macOS** | Supported | Fully compatible (Intel and Apple Silicon) |
141
- | **Windows** | Installs, not CI-covered | The MCP server, the CLI and the `node` hooks work (`better-sqlite3` ships `win32-x64` and `win32-arm64` prebuilds, so nothing is compiled). **Three hook commands run under `bash`** — `setup.sh`, `post-tool-use.sh`, `pre-agent-inject.sh` — and need Git for Windows or WSL on `PATH`; `claude-mem-lite doctor` reports it when `bash` cannot be found. No GitHub Actions runner exercises Windows, so this rests on user reports ([#28](https://github.com/sdsrss/claude-mem-lite/issues/28)), not on a green pipeline |
141
+ | **Windows** | Installs, not CI-covered | The MCP server, the CLI and the `node` hooks work (`better-sqlite3` ships `win32-x64` and `win32-arm64` prebuilds, so nothing is compiled). **Four hook commands run under `bash`** — `setup.sh`, `post-tool-use.sh`, `pre-agent-inject.sh`, `pre-tool-recall-bash.sh` — and need Git for Windows or WSL on `PATH`; `claude-mem-lite doctor` reports it when `bash` cannot be found. No GitHub Actions runner exercises Windows, so this rests on user reports ([#28](https://github.com/sdsrss/claude-mem-lite/issues/28)), not on a green pipeline |
142
142
  | **WSL2** | Untested | Linux under the hood, so it should behave as the Linux row; nobody has reported either way |
143
143
 
144
144
  From v5.1.0 through v6.1.0, `package.json` declared `os: ["darwin", "linux"]`. That is an npm *install*
@@ -152,7 +152,7 @@ is still outside it gets a message naming both sides of the mismatch instead of
152
152
  - **Node.js** >= 22
153
153
  - **Claude Code** CLI installed and configured (`claude` command available)
154
154
  - **SQLite3** support (provided by `better-sqlite3` 13, which ships prebuilt binaries for 8 platforms — no compiler needed on any of them; a platform it has no prebuild for falls back to building from source)
155
- - **Platform**: Linux or macOS; Windows installs and runs but is not CI-covered and needs Git Bash or WSL for three hooks (see [Platform Support](#platform-support))
155
+ - **Platform**: Linux or macOS; Windows installs and runs but is not CI-covered and needs Git Bash or WSL for four hooks (see [Platform Support](#platform-support))
156
156
 
157
157
  ## Installation
158
158
 
@@ -237,6 +237,29 @@ 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.14.0
241
+
242
+ **Five defaults change; three have an off switch.** No schema change and no migration: an
243
+ older build still opens the database, so reverting everything is pinning
244
+ `claude-mem-lite@6.13.6`.
245
+
246
+ - **File recall now also fires before Bash commands that view or write a file** (`cat`,
247
+ `sed -n`, `head`; `sed -i`, `cat > f`, a python patch) — on recent models most reads and
248
+ edits go through Bash, and the recall face with the highest measured cite rate had almost
249
+ stopped firing. A **fourth hook command runs under `bash`**, `pre-tool-recall-bash.sh`; on
250
+ Windows it needs Git Bash or WSL like the other three. Off: `CLAUDE_MEM_BASH_RECALL=off`.
251
+ - **Error recall stays quiet on a failure you meant to cause** (running a test file you just
252
+ wrote or edited) and on commands that only print data and exit 0. No switch; pin 6.13.6 to
253
+ revert. `error_recall` counts in `citation-stats` drop from here on — do not compare them
254
+ across this version.
255
+ - **Auto-captured lessons must quote what happened.** The episode summarizer ignores
256
+ mutation-test probes, the agent's own failing inline scripts and subagent calls that do not
257
+ edit the project, and a lesson that quotes nothing from its window is dropped (the event is
258
+ kept, at importance 1). Off: `CLAUDE_MEM_EPISODE_INPUT_FILTER=off` /
259
+ `CLAUDE_MEM_LESSON_GROUNDING=off`.
260
+ - **The `[mem] episode flushed: N entries` line is no longer injected.** The hints that
261
+ followed it are unchanged.
262
+
240
263
  ## Upgrading to 6.13.0
241
264
 
242
265
  **One default changes: SessionStart no longer injects `### Key Events`.** That section listed
@@ -916,6 +939,9 @@ claude-mem-lite.
916
939
  | `CLAUDE_MEM_DEBUG` | Enable debug logging (`1` to enable). | _(disabled)_ |
917
940
  | `MEM_QUIET_HOOKS` | Low-noise hooks. `1` drops the `File Lessons` / `Key Context` sections from SessionStart injection, the lesson suffix from `[mem] Related memories`, and the `WHEN TO USE` / `Decision rules` blocks from MCP server instructions. IDs and the `Recent` table still surface so `mem_get(ids=[…])` remains reachable. Intended for users running the invited-memory adopt path or who otherwise want minimal auto-injection. **Since v2.82.0 this env no longer gates auto-adopt — use `MEM_NO_AUTO_ADOPT=1` for that.** | _(disabled)_ |
918
941
  | `CLAUDE_MEM_SESSION_EVENTS` | `1`/`on` restores the SessionStart `### Key Events` section (recent high-importance rows from the `events` table). **Off by default since v6.13.0**: an audit of 30 events read 2 accurate and 16 wrong. The UserPromptSubmit events block and PreToolUse recall are query-matched and stay on; `mem_search` still reaches every event. | _(off)_ |
942
+ | `CLAUDE_MEM_EPISODE_INPUT_FILTER` | What the episode summarizer may learn from. A subagent's tool calls stay out of the episode buffer unless they edit a file inside the project, and mutation probes (mutate → RED run → restore) and the agent's own failing inline scripts (a patch's `anchor not found`) are dropped before a window is saved or summarized — replayed over the 30 audited events, this removes 3 of the 16 that were wrong outright; with `CLAUDE_MEM_LESSON_GROUNDING` none of the 16 lessons is injected. `off` restores the unfiltered input. | _(on)_ |
943
+ | `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)_ |
944
+ | `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)_ |
919
945
  | `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)_ |
920
946
  | `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)_ |
921
947
 
package/README.zh-CN.md CHANGED
@@ -103,7 +103,7 @@
103
103
  |------|------|------|
104
104
  | **Linux** | 支持 | 主要开发和测试平台;整个 CI 矩阵都跑在这里 |
105
105
  | **macOS** | 支持 | 完全兼容(Intel 和 Apple Silicon) |
106
- | **Windows** | 可安装,但无 CI 覆盖 | MCP server、CLI 和 `node` 类 hook 均可用(`better-sqlite3` 自带 `win32-x64` / `win32-arm64` 预编译产物,无需编译)。**有三个 hook 命令走 `bash`** —— `setup.sh`、`post-tool-use.sh`、`pre-agent-inject.sh` —— 需要 PATH 上有 Git for Windows 或 WSL;`bash` 找不到时 `claude-mem-lite doctor` 会报出来。GitHub Actions 没有 Windows runner,所以这一行依据的是用户报告([#28](https://github.com/sdsrss/claude-mem-lite/issues/28))而不是绿色流水线 |
106
+ | **Windows** | 可安装,但无 CI 覆盖 | MCP server、CLI 和 `node` 类 hook 均可用(`better-sqlite3` 自带 `win32-x64` / `win32-arm64` 预编译产物,无需编译)。**有四个 hook 命令走 `bash`** —— `setup.sh`、`post-tool-use.sh`、`pre-agent-inject.sh`、`pre-tool-recall-bash.sh` —— 需要 PATH 上有 Git for Windows 或 WSL;`bash` 找不到时 `claude-mem-lite doctor` 会报出来。GitHub Actions 没有 Windows runner,所以这一行依据的是用户报告([#28](https://github.com/sdsrss/claude-mem-lite/issues/28))而不是绿色流水线 |
107
107
  | **WSL2** | 未测试 | 底层就是 Linux,预期与 Linux 行一致;但无人报告过实际结果 |
108
108
 
109
109
  v5.1.0 到 v6.1.0 之间,`package.json` 声明的是 `os: ["darwin", "linux"]`。那是 npm 的**安装**门禁,不是运行时检查:
@@ -116,7 +116,7 @@ v5.1.0 到 v6.1.0 之间,`package.json` 声明的是 `os: ["darwin", "linux"]`
116
116
  - **Node.js** >= 22(v4.0.0 起:better-sqlite3 13 要求 >=22,Node 20 已于 2026-04 EOL;`package.json` 的 `engines` 是唯一事实来源)
117
117
  - **Claude Code** CLI 已安装并配置(`claude` 命令可用)
118
118
  - **SQLite3** 支持(由 `better-sqlite3` 13 提供,它自带 8 个平台的预编译产物,这些平台上都不需要编译器;没有对应预编译产物的平台才会回退到源码编译)
119
- - **平台**:Linux 或 macOS;Windows 可安装运行,但无 CI 覆盖,且三个 hook 需要 Git Bash 或 WSL(参见[平台支持](#平台支持))
119
+ - **平台**:Linux 或 macOS;Windows 可安装运行,但无 CI 覆盖,且四个 hook 需要 Git Bash 或 WSL(参见[平台支持](#平台支持))
120
120
 
121
121
  ## 安装
122
122
 
@@ -199,6 +199,23 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
199
199
  repos/ # 浅克隆的源代码仓库
200
200
  ```
201
201
 
202
+ ## 升级到 6.14.0
203
+
204
+ **五个默认行为变化,其中三个有关闭开关。** 没有 schema 变更、没有迁移:旧版本仍能打开数据库,
205
+ 全部回退就是固定到 `claude-mem-lite@6.13.6`。
206
+
207
+ - **查看或写入文件的 Bash 命令执行前,也会做文件召回**(`cat`、`sed -n`、`head`;`sed -i`、
208
+ `cat > f`、python 补丁)。新模型的读写大多走 Bash,实测引用率最高的召回面几乎不再触发。
209
+ **第四个 hook 命令走 `bash`**:`pre-tool-recall-bash.sh`;在 Windows 上与另外三个一样需要
210
+ Git Bash 或 WSL。关闭:`CLAUDE_MEM_BASH_RECALL=off`。
211
+ - **error recall 不再回应你故意制造的失败**(刚写好或刚改过的测试文件跑红),也不回应只打印数据、
212
+ 退出码为 0 的命令。没有开关,回退请固定 6.13.6。从这个版本起 `citation-stats` 里的
213
+ `error_recall` 计数会下降,不要跨版本直接比较。
214
+ - **自动捕获的教训必须引用原文。** episode 摘要器忽略变异测试探针、agent 自己失败的内联脚本,以及
215
+ 不修改项目的子代理调用;不引用窗口内任何原文的教训会被丢弃(event 保留,importance 降为 1)。
216
+ 关闭:`CLAUDE_MEM_EPISODE_INPUT_FILTER=off` / `CLAUDE_MEM_LESSON_GROUNDING=off`。
217
+ - **不再注入 `[mem] episode flushed: N entries` 这一行。** 它后面的提示不变。
218
+
202
219
  ## 升级到 6.13.0
203
220
 
204
221
  **只有一个默认行为变化:SessionStart 不再注入 `### Key Events`。** 这一节在每个会话开头列出
@@ -702,6 +719,9 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
702
719
  | `CLAUDE_MEM_DEBUG` | 启用调试日志(设为 `1` 启用)。 | _(禁用)_ |
703
720
  | `MEM_QUIET_HOOKS` | 低噪声 hook。设为 `1` 时,SessionStart 注入去掉 `File Lessons` / `Key Context` 两节,`[mem] Related memories` 去掉 lesson 后缀,MCP server instructions 去掉 `WHEN TO USE` / `Decision rules` 两段。ID 与 `Recent` 表仍保留,`mem_get(ids=[…])` 可继续展开细节。适用于启用了 invited-memory adopt 流程或偏好最小化自动注入的用户。**v2.82.0 起此 env 不再阻挡 auto-adopt——如需关闭 auto-adopt 用 `MEM_NO_AUTO_ADOPT=1`。** | _(禁用)_ |
704
721
  | `CLAUDE_MEM_SESSION_EVENTS` | 设为 `1`/`on` 时恢复 SessionStart 的 `### Key Events` 一节(`events` 表中最近的高重要度条目)。**v6.13.0 起默认关闭**:对 30 条 event 的核验只有 2 条属实、16 条错误。UserPromptSubmit 的 events 块与 PreToolUse 召回按查询匹配,保持开启;`mem_search` 仍可检索全部 event。 | _(关闭)_ |
722
+ | `CLAUDE_MEM_EPISODE_INPUT_FILTER` | 决定 episode 摘要器能从哪些输入里学。子代理的工具调用不进入 episode 缓冲(修改项目内文件的除外);变异探针(改文件 → 跑红 → 还原)和 agent 自己失败的内联脚本(补丁的 `anchor not found`)在保存或摘要前被剔除——在 30 条已核验 event 上重放,这一项直接去掉 16 条错误中的 3 条;配合 `CLAUDE_MEM_LESSON_GROUNDING`,16 条错误教训一条都不会被注入。设为 `off` 恢复未过滤的输入。 | _(开启)_ |
723
+ | `CLAUDE_MEM_BASH_RECALL` | 在查看(`cat`、`sed -n`、`head`…)或写入(`sed -i`、`cat > f`、python 补丁…)文件的 Bash 命令执行前做文件召回,与 Read / Edit 召回相同。bash 预过滤让其他命令不启动 Node。设为 `off` 只关闭这一路。 | _(开启)_ |
724
+ | `CLAUDE_MEM_LESSON_GROUNDING` | 自动捕获的 event 只有在教训引用了本窗口自己的诊断文字(失败输出行、编辑新增的注释或提交信息)时才保留教训;否则保留这一行但去掉教训,importance 降为 1,低于所有注入面的门槛。设为 `off` 保留未引用原文的教训。 | _(开启)_ |
705
725
  | `MEM_NO_AUTO_ADOPT` | auto-adopt 全局关闭开关(v2.82.0+)。设为 `1` 阻止每次 SessionStart 在**所有**项目自动写入 `CLAUDE.md` 托管块。项目级关闭走 `claude-mem-lite adopt --disable`(写 `<memdir>/.mem-no-auto-adopt` 哨兵,存活于 marker 删除)。 | _(禁用)_ |
706
726
  | `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`)的项目才会出现。 | _(禁用)_ |
707
727
 
package/bash-utils.mjs CHANGED
@@ -9,6 +9,8 @@ import { basename } from 'path';
9
9
  // import this file. Cold-start scripts are unaffected — scripts/pre-tool-recall.js
10
10
  // deliberately imports nothing from the utils.mjs barrel that re-exports this.
11
11
  import { toolEditPath } from './lib/file-edge-match.mjs';
12
+ import { bashFileTargets, isTransientPath, isScratchCommandPath } from './lib/bash-file-targets.mjs';
13
+ export { isTransientPath };
12
14
 
13
15
  // Read/search commands whose output legitimately contains "error"-like keywords without
14
16
  // being a failure. Matched against the PRIMARY command (see isReadOnlyCommand).
@@ -522,11 +524,89 @@ function commandKind(cmd, depth = 0) {
522
524
  return sawRead ? 'read' : 'neutral';
523
525
  }
524
526
 
527
+ // ─── Data printers and writers (error-recall N2) ─────────────────────────────
528
+
529
+ // Interpreters whose INLINE program (`-e`/`-c`/a heredoc on stdin) is the agent's own
530
+ // throwaway code. Run by a script FILE they are a program like any other.
531
+ const INLINE_INTERPRETER_RE = /^(?:node|nodejs|bun|python(?:\d+(?:\.\d+)?)?)$/;
532
+
533
+ // A redirection operator token (`>`, `2>>`, `&>`, `<`, `<<-`), optionally glued to its target.
534
+ const REDIRECT_TOKEN_RE = /^(?:\d*|&)(?:>>?\|?|<<?-?)(.*)$/;
535
+
536
+ /**
537
+ * True when one simple command runs an INLINE program or prints a CI log — the shapes
538
+ * whose output at exit 0 is data (a transcript line, a log excerpt, a script's own
539
+ * `assert`), not a failure of anything the corpus could explain.
540
+ */
541
+ function isPrinterElement(text) {
542
+ const toks = shellWords(text);
543
+ let i = 0;
544
+ while (i < toks.length && (/^\w+=/.test(toks[i]) || CMD_WRAPPERS.has(toks[i]))) i++;
545
+ const prog = toks[i];
546
+ if (!prog) return false;
547
+ const args = toks.slice(i + 1);
548
+ // `gh run view … --log[-failed]`: a CI log is somebody else's failure, reprinted. `--json`
549
+ // is gh's structured READ (list/view/status only), typically feeding a run id to the former.
550
+ if (prog === 'gh')
551
+ return args.some((a) => a === '--log' || a === '--log-failed' || /^--json(?:=|$)/.test(a));
552
+ if (!INLINE_INTERPRETER_RE.test(prog)) return false;
553
+ const py = prog.startsWith('python');
554
+ for (let k = 0; k < args.length; k++) {
555
+ const a = args[k];
556
+ if (py ? a === '-c' || a === '-' : /^-(?:e|p|pe|ep)$|^--(?:eval|print)(?:=|$)/.test(a)) return true;
557
+ const redirect = REDIRECT_TOKEN_RE.exec(a);
558
+ if (redirect) {
559
+ if (!redirect[1]) k++; // `<< EOF` / `> f`: the next word is the operand, not a script
560
+ continue;
561
+ }
562
+ // A positional is a script file (or `-m`'s module): a program, not an inline print.
563
+ if (!a.startsWith('-') || (py && a === '-m')) return false;
564
+ }
565
+ // No positional at all: the program comes from stdin, i.e. a heredoc.
566
+ return /<</.test(text);
567
+ }
568
+
569
+ /** 'print' | 'read' | 'neutral' | 'other' — commandKind with printers admitted. */
570
+ function printKind(cmd, depth = 0) {
571
+ if (depth > MAX_SUBST_DEPTH) return 'other';
572
+ const parsed = splitStatements(cmd);
573
+ if (!parsed) return 'other';
574
+ let sawPrint = false;
575
+ for (const body of parsed.subs) {
576
+ const kind = printKind(body, depth + 1);
577
+ if (kind === 'other') return 'other';
578
+ if (kind === 'print') sawPrint = true;
579
+ }
580
+ for (const pipeline of parsed.statements) {
581
+ for (const element of pipeline) {
582
+ if (isPrinterElement(element)) {
583
+ sawPrint = true;
584
+ continue;
585
+ }
586
+ if (classifySimpleCommand(element) === 'other') return 'other';
587
+ }
588
+ }
589
+ return sawPrint ? 'print' : 'read';
590
+ }
591
+
592
+ /**
593
+ * True when the command's only programs are inline scripts (`node -e/-p`, `python3 -c`,
594
+ * `python3 - <<EOF`, a heredoc-fed interpreter) or `gh … --log[-failed]`, plus reads
595
+ * (isReadOnlyCommand's verbs) and neutral set-up. Parsed with the same statement/pipeline
596
+ * splitter, so `node -e '…'; npm test` and `python3 - <<PY … PY` followed by a vitest run
597
+ * are NOT printers: the real run is judged on its own.
598
+ *
599
+ * Deliberately a SEPARATE predicate from isReadOnlyCommand, not a new verb in it: that one
600
+ * also decides `isError` for the episode narrative and the bugfix save-nudge, and an inline
601
+ * script that crashes is a real error there. Only error-recall's injection is narrowed.
602
+ */
603
+ export function isDataPrintingCommand(cmd) {
604
+ if (typeof cmd !== 'string' || !cmd) return false;
605
+ return printKind(stripNonCommands(cmd)) === 'print';
606
+ }
607
+
525
608
  // Paths excluded from observation capture (ephemeral / virtual filesystems) — applied
526
609
  // uniformly to both command-parsed paths and direct file_path/path/filePath fields.
527
- function isExcludedPath(p) {
528
- return p.startsWith('/dev/') || p.startsWith('/proc/') || p.startsWith('/tmp/');
529
- }
530
610
 
531
611
  /**
532
612
  * Detect significance signals in a Bash command and its response.
@@ -887,16 +967,24 @@ export function planErrorRecall(cmd, response) {
887
967
  // ─── File Paths ──────────────────────────────────────────────────────────────
888
968
 
889
969
  /**
890
- * Extract file paths from tool input (file_path, path, filePath, or command args).
891
- * Deduplicates and excludes /dev/, /proc/, and /tmp/ paths.
970
+ * Files a tool call touched, split so callers can tell an edit from a read.
971
+ * `files`: every file edge (direct path fields + all Bash targets);
972
+ * `writes`: the subset a Bash command WROTE (`sed -i`, `cat > f`, a python patch, …) —
973
+ * the Bash counterpart of an Edit/Write `file_path`, consumed via `entryEditedFiles`.
892
974
  * @param {object} input Tool input object
893
- * @returns {string[]} Unique array of file paths
975
+ * @param {{cwd?: string|null, projectDir?: string|null}} [opts] the hook's `cwd`, to resolve
976
+ * relative Bash paths; the project root, whose files count even when it lives under /tmp
977
+ * @returns {{files: string[], writes: string[]}}
894
978
  */
895
- export function extractFilePaths(input) {
979
+ export function extractFileTargets(input, opts = {}) {
896
980
  const paths = [];
897
- // Direct fields (Edit/Write file_path) are kept unconditionally — an explicit edit to a
981
+ const writes = [];
982
+ if (!input || typeof input !== 'object') return { files: [], writes: [] };
983
+ // Direct fields (Edit/Write file_path) are kept even under /tmp — an explicit edit to a
898
984
  // /tmp path is real work the user chose to make, unlike a /tmp path that merely appears as
899
985
  // a transient argument inside a Bash command (excluded as noise in the command branch below).
986
+ // Session-scoped paths (isTransientPath) are the exception: they are the agent's own
987
+ // scratch, not the user's work.
900
988
  //
901
989
  // `toolEditPath`, not a fourth hand-spelling of the same rule: this function knew
902
990
  // file_path/path/filePath and not `notebook_path`, while hooks.json matches PostToolUse
@@ -905,27 +993,39 @@ export function extractFilePaths(input) {
905
993
  // files, so the observation built from it got no observation_files edge and no file-keyed
906
994
  // recall could reach it. Same root cause as R12 B-2; the fourth site, and the one its
907
995
  // own follow-up note did not name.
908
- const editedPath = toolEditPath(input);
909
- if (editedPath) paths.push(editedPath);
910
- if (input.path) paths.push(input.path);
911
- if (input.filePath) paths.push(input.filePath);
912
- if (input.command) {
913
- // Match absolute paths; extension optional to support Makefile, Dockerfile etc.
914
- const match = input.command.match(/(?:^|\s)(\/[\w./-]+\w)/g);
915
- if (match) {
916
- for (const m of match) {
917
- const p = m.trim();
918
- if (
919
- !isExcludedPath(p) &&
920
- // Skip single-component paths like /exit, /clear — likely slash commands, not files
921
- (p.indexOf('/', 1) !== -1 || /\.\w+$/.test(p))
922
- ) {
923
- paths.push(p);
924
- }
925
- }
996
+ for (const p of [toolEditPath(input), input.path, input.filePath]) {
997
+ if (typeof p === 'string' && p && !isTransientPath(p)) paths.push(p);
998
+ }
999
+ if (typeof input.command === 'string' && input.command) {
1000
+ // Shell-aware: cwd prefixes, relative and quoted paths, write-verb targets and
1001
+ // interpreter-script literals (lib/bash-file-targets.mjs). The `cd` target itself is
1002
+ // never an edge — it made every `cd <repo> && …` command look like it touched the
1003
+ // repo root and nothing else.
1004
+ const keep = (p) =>
1005
+ !isScratchCommandPath(p, opts.projectDir) &&
1006
+ !isTransientPath(p) &&
1007
+ // Skip single-component paths like /exit, /clear — likely slash commands, not files
1008
+ (p.indexOf('/', 1) !== -1 || /\.\w+$/.test(p));
1009
+ const t = bashFileTargets(input.command, { cwd: opts.cwd || null });
1010
+ for (const p of t.writes) {
1011
+ if (!keep(p)) continue;
1012
+ writes.push(p);
1013
+ paths.push(p);
926
1014
  }
1015
+ for (const p of [...t.reads, ...t.mentions]) if (keep(p)) paths.push(p);
927
1016
  }
928
- return [...new Set(paths)];
1017
+ return { files: [...new Set(paths)], writes: [...new Set(writes)] };
1018
+ }
1019
+
1020
+ /**
1021
+ * Extract file paths from tool input (file_path, path, filePath, or command args).
1022
+ * Deduplicates and excludes /dev/, /proc/, /tmp/ command paths and session-scoped paths.
1023
+ * @param {object} input Tool input object
1024
+ * @param {{cwd?: string|null}} [opts] the hook's `cwd`, to resolve relative Bash paths
1025
+ * @returns {string[]} Unique array of file paths
1026
+ */
1027
+ export function extractFilePaths(input, opts = {}) {
1028
+ return extractFileTargets(input, opts).files;
929
1029
  }
930
1030
 
931
1031
  // ─── Episode Logic ───────────────────────────────────────────────────────────
package/cli/doctor.mjs CHANGED
@@ -34,7 +34,7 @@ export async function cmdDoctor(db, args) {
34
34
  WHERE s.project = ?
35
35
  AND p.prompt_text IS NOT NULL
36
36
  AND length(p.prompt_text) >= 15
37
- ORDER BY p.created_at_epoch DESC
37
+ ORDER BY p.created_at_epoch DESC, p.id DESC
38
38
  LIMIT ?
39
39
  `,
40
40
  )
package/hook-context.mjs CHANGED
@@ -850,11 +850,11 @@ export function buildSessionContextLines(
850
850
  `
851
851
  SELECT id, title, priority,
852
852
  ROW_NUMBER() OVER (
853
- ORDER BY priority DESC, created_at_epoch ASC
853
+ ORDER BY priority DESC, created_at_epoch ASC, id ASC
854
854
  ) AS ordinal
855
855
  FROM deferred_work
856
856
  WHERE project = ? AND status = 'open'
857
- ORDER BY priority DESC, created_at_epoch ASC
857
+ ORDER BY priority DESC, created_at_epoch ASC, id ASC
858
858
  LIMIT 5
859
859
  `,
860
860
  )
package/hook-episode.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  statSync,
15
15
  constants as fsConstants,
16
16
  } from 'fs';
17
- import { inferProject, EDIT_TOOLS } from './utils.mjs';
17
+ import { inferProject, isEditEntry } from './utils.mjs';
18
18
  import { RUNTIME_DIR } from './hook-shared.mjs';
19
19
 
20
20
  /**
@@ -454,7 +454,7 @@ export function explainSignificance(episode) {
454
454
  const base = { readCount, grepCount, grepDecisive: false };
455
455
 
456
456
  // 1. File edits → always significant (code changes matter)
457
- if (entries.some((e) => EDIT_TOOLS.has(e.tool))) return { ...base, significant: true, rule: 1 };
457
+ if (entries.some(isEditEntry)) return { ...base, significant: true, rule: 1 };
458
458
 
459
459
  // 2. Test/build errors → significant (actionable failures)
460
460
  // Plain bash errors without edits are noise (e.g. typos, exploration errors)
package/hook-handoff.mjs CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  isSpecificTerm,
11
11
  scrubSecrets,
12
12
  LOW_SIGNAL_TITLE,
13
- EDIT_TOOLS,
13
+ isEditEntry,
14
14
  isMetaTriggerPrompt,
15
15
  notLowSignalTitleClause,
16
16
  safeText,
@@ -262,7 +262,7 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
262
262
  if (episodeSnapshot?.entries) {
263
263
  const seenDescs = new Set();
264
264
  const pendingDescs = episodeSnapshot.entries
265
- .filter((e) => e.isError || EDIT_TOOLS.has(e.tool))
265
+ .filter((e) => e.isError || isEditEntry(e))
266
266
  .map((e) => e.desc)
267
267
  .filter((d) => {
268
268
  if (seenDescs.has(d)) return false;