pmem-ai 0.6.0 → 0.6.2

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 (65) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +11 -9
  3. package/dist/commands/distill.js +1 -1
  4. package/dist/commands/distill.js.map +1 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +15 -1
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/commands/integration.d.ts.map +1 -1
  9. package/dist/commands/integration.js +57 -3
  10. package/dist/commands/integration.js.map +1 -1
  11. package/dist/commands/new.d.ts +2 -0
  12. package/dist/commands/new.d.ts.map +1 -0
  13. package/dist/commands/new.js +117 -0
  14. package/dist/commands/new.js.map +1 -0
  15. package/dist/commands/rebuild.d.ts.map +1 -1
  16. package/dist/commands/rebuild.js +27 -1
  17. package/dist/commands/rebuild.js.map +1 -1
  18. package/dist/commands/recall.d.ts +1 -1
  19. package/dist/commands/recall.d.ts.map +1 -1
  20. package/dist/commands/recall.js +34 -3
  21. package/dist/commands/recall.js.map +1 -1
  22. package/dist/commands/rename.d.ts +6 -0
  23. package/dist/commands/rename.d.ts.map +1 -0
  24. package/dist/commands/rename.js +177 -0
  25. package/dist/commands/rename.js.map +1 -0
  26. package/dist/commands/status.d.ts.map +1 -1
  27. package/dist/commands/status.js +3 -4
  28. package/dist/commands/status.js.map +1 -1
  29. package/dist/commands/update.d.ts +1 -0
  30. package/dist/commands/update.d.ts.map +1 -1
  31. package/dist/commands/update.js +335 -133
  32. package/dist/commands/update.js.map +1 -1
  33. package/dist/commands/verify.d.ts +2 -0
  34. package/dist/commands/verify.d.ts.map +1 -1
  35. package/dist/commands/verify.js +61 -39
  36. package/dist/commands/verify.js.map +1 -1
  37. package/dist/core/consistency.d.ts +10 -0
  38. package/dist/core/consistency.d.ts.map +1 -0
  39. package/dist/core/consistency.js +97 -0
  40. package/dist/core/consistency.js.map +1 -0
  41. package/dist/core/db.d.ts +9 -0
  42. package/dist/core/db.d.ts.map +1 -1
  43. package/dist/core/db.js +4 -0
  44. package/dist/core/db.js.map +1 -1
  45. package/dist/core/fs.d.ts +6 -0
  46. package/dist/core/fs.d.ts.map +1 -1
  47. package/dist/core/fs.js +32 -2
  48. package/dist/core/fs.js.map +1 -1
  49. package/dist/core/manifest.js +1 -1
  50. package/dist/core/manifest.js.map +1 -1
  51. package/dist/index.js +24 -3
  52. package/dist/index.js.map +1 -1
  53. package/dist/types.d.ts +40 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/docs/handover-v0.4.md +2 -0
  56. package/docs/handover-v0.6.md +152 -0
  57. package/docs/project-roadmap.md +42 -5
  58. package/docs/usage.md +13 -14
  59. package/docs/v0.4 pre-design.md +2 -0
  60. package/docs/v0.5 pre-design.md +2 -0
  61. package/docs/v0.6 pre-design.md +2 -0
  62. package/docs/v0.6.1 pre-design.md +566 -0
  63. package/docs/v0.6.2 pre-design.md +657 -0
  64. package/package.json +3 -2
  65. package/skills/pmem/SKILL.md +12 -10
@@ -0,0 +1,657 @@
1
+ # v0.6.2 前置设计决策
2
+
3
+ 本文档记录 pmem v0.6.2 开工前的产品与技术决策。v0.6.2 的核心定位:
4
+
5
+ > **Real-User Friction Fixes**
6
+ >
7
+ > v0.6.1 已发布到 npm (`pmem-ai@0.6.1`) 并进入真实用户观察期。
8
+ >
9
+ > 2026-05-23 收到首份重度用户反馈(voxo v1.0 项目,25 张卡片、27 条 trace、完整 P0→发布周期)。
10
+ >
11
+ > v0.6.2 不新增大型子系统,不改变 memory schema,不做 embedding、MCP/REST、Graph UI、telemetry 或远程服务。
12
+ > v0.6.2 只解决真实用户反馈中暴露的三个 P0 摩擦点:**锁竞争修复、卡片大小可配置、跨卡片批量重命名**。外加从同一份反馈中提取的四项低风险体验改进。
13
+ >
14
+ > v0.6.2 要回答的唯一问题:**pmem 能否消除重度用户在真实项目中遭遇的 top-3 摩擦,让"从 init 到发布"的完整生命周期不被工具自身打断?**
15
+
16
+ ---
17
+
18
+ ## 一、背景:v0.6.1 首份重度用户反馈
19
+
20
+ 2026-05-23,voxo v1.0 项目 CTO 提交了 `pmem-0.6.1-feedback.md`(已存档为 trace 卡片)。这是 pmem 在真实跨平台 CLI 项目(从零到发布)中作为唯一项目记忆系统的首份完整实战报告。
21
+
22
+ ### 已验证正确的设计(4 项)
23
+
24
+ | 设计决策 | 用户原话 | 结论 |
25
+ |----------|---------|------|
26
+ | Markdown 卡片是唯一真相源 | "这个设计选择是决定性的" | 坚持 |
27
+ | 六种卡片类型 | "刚好覆盖,不会少也不会多到让人选择困难" | 不增减 |
28
+ | `pmem ask` 图展开搜索 | "这个图展开能力在实际使用中比我想象的有用" | 价值被低估 |
29
+ | Claude Code 集成开箱体验 | "新的 agent 进来两条命令就能恢复全部上下文" | 做对了 |
30
+
31
+ ### 暴露的摩擦点(6 项,按影响排序)
32
+
33
+ | # | 问题 | 当前代码状况 | 用户影响 |
34
+ |---|------|-------------|---------|
35
+ | 1 | SQLite 锁竞争 | `src/core/fs.ts:66-103`,busy-wait 3s 超时,stale 60s 从不自动清理,无查看/强制释放命令 | `pmem update --confirm` 失败 3 次,用户以为数据会丢 |
36
+ | 2 | 卡片大小限制过于激进 | `src/core/manifest.ts:114`,硬编码 module 1200/decision 800/task 600 token,不可配置 | 写分 P 计划被打 `card_too_large` 警告 2 次 |
37
+ | 3 | 跨卡片批量重命名缺失 | 无此命令 | 项目改名时手动 grep + sed + rebuild 6 个文件 |
38
+ | 4 | `pmem recall` 输出随卡片线性增长 | 无 `--since` 过滤 | 25 张卡片已感觉长,50-100 张时 2000 token 不够 |
39
+ | 5 | `pmem update --suggest` exit code 1 | 设计决策,文档写了 "workflow signal",但用户心理和 CI 都不认 | 每次看到 exit 1 心里一紧 |
40
+ | 6 | session 状态偶尔丢失 | `src/commands/session.ts:77-82`,报 "No active pmem session found" | 影响 session end 可用性 |
41
+
42
+ ### 用户自己排的优先级
43
+
44
+ > **锁竞争修复 > 卡片大小可配置 > 批量重命名**
45
+
46
+ ### 愿望清单(4 项)
47
+
48
+ 1. `pmem health`:更面向用户的一键诊断(现有 `pmem doctor` 用户觉得不够直观)
49
+ 2. 卡片模板生成:`pmem new decision "标题"` 自动生成 YAML frontmatter
50
+ 3. Frontmatter 创建时校验:必填字段缺失立即警告,不等 rebuild 静默跳过
51
+ 4. Git hook 集成:`pmem integration install git-hooks`
52
+
53
+ ---
54
+
55
+ ## 二、v0.6.2 产品目标
56
+
57
+ ### 一句话目标
58
+
59
+ > **消除重度用户在真实项目中遭遇的 top-3 摩擦,让 pmem 不成为自身工具链的阻塞点。**
60
+
61
+ ### 用户对象
62
+
63
+ | 用户 | 核心诉求 |
64
+ |------|---------|
65
+ | 重度项目使用者 | 锁不卡、卡片不被大小限制打断、改项目名不用手动批量 grep |
66
+ | Code agent | 锁竞争时的错误信息可直接指导恢复操作 |
67
+ | 新用户 | 创建卡片时有模板可用、必填字段当场提示 |
68
+
69
+ ### 成功标准
70
+
71
+ 1. `pmem update --confirm` 在锁竞争场景下不再需要用户手动 `rebuild --full` 才能恢复。
72
+ 2. stale lock(>60s)在获取锁前自动检测并清理,用户无感知。
73
+ 3. `pmem verify` 提供 `--fix-locks` 选项,可一键清理僵死锁。
74
+ 4. 卡片大小限制可通过 manifest `card_policy.max_tokens` 自定义,verify 提供 `--relaxed` 模式。
75
+ 5. `pmem rename --find "old" --replace "new"` 默认预览 diff 不写入,`--write` 时执行全文替换。
76
+ 6. `pmem recall --since 7d` 只返回近期变更的卡片。
77
+ 7. `pmem update --suggest` 在有建议时不再返回 exit code 1。
78
+ 8. `pmem new <type> "<title>"` 自动生成符合规范的卡片骨架。
79
+ 9. `pmem integration install git-hooks` 可安装 pre-commit verify hook。
80
+ 10. `pmem doctor` 新增 lock status 检查项。
81
+
82
+ ---
83
+
84
+ ## 三、scope 分级
85
+
86
+ ### P0 — 必须修复(来自反馈 top-3)
87
+
88
+ | 功能 | 说明 | 复杂度 |
89
+ |------|------|--------|
90
+ | 锁竞争修复 | stale lock 自动清理 + `--fix-locks` + 错误消息增强 + doctor 锁检查 | 低 |
91
+ | 卡片大小可配置 | 尊重 manifest `card_policy.max_tokens` + verify `--relaxed` 模式 + 文档 | 低 |
92
+ | 批量重命名 | `pmem rename --find/--replace [--write]`(默认预览) | 中 |
93
+
94
+ ### P1 — 强烈建议(来自反馈其余项 + 愿望清单)
95
+
96
+ | 功能 | 说明 | 复杂度 |
97
+ |------|------|--------|
98
+ | `recall --since` | 按时间过滤 recall 输出,时间契约已补全(§5.1) | 低 |
99
+ | exit code 修正 | `update --suggest`/`status`/`distill --suggest` 有结果时 exit 0,兼容性方案已通过(§5.2) | 低(代码)、中(文档) |
100
+ | 卡片模板生成 | `pmem new <type> "<title>"` | 低 |
101
+ | Frontmatter 创建时校验 | `pmem new` 时当场校验必填字段 | 低 |
102
+ | Git hook 集成 | `pmem integration install git-hooks` | 低 |
103
+ | session 健壮性排查 | 调查 session 丢失根因 + 修复 | 中 |
104
+
105
+ > **注:** `pmem doctor` 锁检查已在 P0 中随锁竞争修复一并实现,不再出现在 P1 列表中。
106
+
107
+ ### 明确不做(推迟到 v0.7+)
108
+
109
+ - 完整 dirty lifecycle(`open / resolved / superseded / ignored / archived`)—— v0.6.1 已推迟
110
+ - `pmem dirty list/resolve/prune` —— v0.6.1 已推迟
111
+ - `pmem health` 独立命令 —— 用 `pmem doctor` 增强替代,不新建命令
112
+ - embedding / semantic search
113
+ - MCP Server / REST API
114
+ - Graph UI
115
+ - telemetry
116
+ - 多项目 workspace
117
+
118
+ ### P0 完成状态
119
+
120
+ | 任务 | 状态 | 验证 |
121
+ |------|------|------|
122
+ | 锁竞争修复 + `verify --fix-locks` + `doctor` lock 检查 | ✅ 已实现并验收 | E2E 覆盖 stale/active lock 全部场景 |
123
+ | `verify --relaxed` + card policy 文档化 + 默认 token 上调 | ✅ 已实现并验收 | E2E 覆盖 oversized 检测、relaxed 模式、manifest 自定义 |
124
+ | `pmem rename` 默认预览 + `--write` | ✅ 已实现并验收 | E2E 覆盖 preview/write/rebuild/verify + frontmatter hash 不变 |
125
+
126
+ P0 验收通过(CTO,2026-05-23)。
127
+
128
+ **已知非阻塞 P2:** `rename.ts` 的 `splitFrontmatter()` 对 body 做了 `trimStart()`,因此"完全保留 body 原始空白"的说法不严谨。不影响 frontmatter hash、不影响 P0 安全边界。后续 polish 可改为记录 frontmatter end 后的原始 body slice。
129
+
130
+ ---
131
+
132
+ ## 四、P0 详细设计
133
+
134
+ ### 4.1 锁竞争修复
135
+
136
+ **现状:**
137
+
138
+ ```
139
+ src/core/fs.ts:
140
+ acquireLock() → mkdirSync 重试 3s,失败返回 false
141
+ releaseLock() → rmdirSync
142
+ isLockStale() → stat mtime > 60s 判定为 stale(存在但从未被调用)
143
+ breakLock() → 等同于 releaseLock(存在但从未暴露给 CLI)
144
+ ```
145
+
146
+ 三个问题:
147
+ 1. `isLockStale` 已实现但**没有任何调用方**——获取锁前不检查 stale。
148
+ 2. `breakLock` 已实现但**没有 CLI 入口**——用户无法手动清理。
149
+ 3. 获取锁失败时错误消息是通用的 `Failed to acquire pmem lock`,不含排查建议。
150
+
151
+ **修改点:**
152
+
153
+ **a) `acquireLock` 自动清理 stale lock**
154
+
155
+ 在 `acquireLock` 获取锁之前,先检查现有锁是否 stale,若 stale 则自动 break 再重试:
156
+
157
+ ```
158
+ acquireLock() 逻辑变更:
159
+ 1. try mkdirSync → 成功则返回 true(不变)
160
+ 2. catch → 检查现有锁是否 isLockStale(lockPath, 60s)
161
+ a. 若 stale → breakLock() → 重试 mkdirSync
162
+ b. 若不 stale → 继续 busy-wait(不变)
163
+ ```
164
+
165
+ **b) `pmem verify --fix-locks`**
166
+
167
+ verify 新增检查项:检测 `.pmem/.lock` 是否存在且 stale,若存在则:
168
+ - `--fix-locks` 时自动清理
169
+ - 无 `--fix-locks` 时报告 warning + 修复建议
170
+
171
+ **c) 错误消息增强**
172
+
173
+ ```
174
+ // 之前
175
+ Failed to acquire pmem lock
176
+
177
+ // 之后
178
+ Failed to acquire pmem lock after 3s.
179
+ The lock at .pmem/.lock may be held by another pmem process.
180
+ → Run: pmem verify --fix-locks (to check and clean stale locks)
181
+ → Or: pmem doctor (to diagnose lock status)
182
+ If no other pmem process is running, delete .pmem/.lock manually.
183
+ ```
184
+
185
+ **d) `pmem doctor` 新增锁检查**
186
+
187
+ 在第 8 项 integrations 之前插入锁状态检查:
188
+
189
+ ```
190
+ Lock: ✓ No stale lock.
191
+ Lock: ⚠ Stale lock detected at .pmem/.lock (age: 72s). Run: pmem verify --fix-locks
192
+ Lock: ✗ Active lock held at .pmem/.lock (age: 2s). Another pmem process may be running.
193
+ ```
194
+
195
+ ### 4.2 卡片大小可配置
196
+
197
+ **现状:**
198
+
199
+ `src/core/manifest.ts:114` 定义默认值,`src/commands/verify.ts:167` 从 `manifest.card_policy.max_tokens[card.type]` 读取。
200
+
201
+ **关键发现:** verify 已经读取用户 manifest 中的 `card_policy.max_tokens`,用户手动编辑 manifest.yml 即可修改限制。问题是:
202
+ 1. 这没有被文档化
203
+ 2. 没有简单的方式临时放宽限制
204
+ 3. 默认值对重度文档场景偏小
205
+
206
+ **修改点:**
207
+
208
+ **a) 提供 `--relaxed` 模式**
209
+
210
+ ```bash
211
+ pmem verify --relaxed # 所有 max_tokens 临时 ×2
212
+ ```
213
+
214
+ 实现:verify 读 `policy.max_tokens` 后,若 `--relaxed` 则每个值 ×2 再比较。
215
+
216
+ **b) 将 `card_policy` 配置文档化**
217
+
218
+ 在 `pmem verify` 的 `card_too_large` 警告信息中加入修改指引:
219
+
220
+ ```
221
+ Card "breakdown_p0_plan" (task) exceeds 600 token limit (~920 tokens).
222
+ → Edit .pmem/manifest.yml → card_policy → max_tokens → task to raise the limit.
223
+ → Or run: pmem verify --relaxed (temporarily doubles all limits).
224
+ → Or run: pmem distill --suggest-splits (check if this card can be split).
225
+ ```
226
+
227
+ **c) 调整默认值(保守上调)**
228
+
229
+ 当前默认值偏保守,基于用户反馈适度上调:
230
+
231
+ | 类型 | 旧默认 | 新默认 |
232
+ |------|--------|--------|
233
+ | module | 1200 | 1200(不变) |
234
+ | feature | 1000 | 1000(不变) |
235
+ | decision | 800 | 1000 |
236
+ | task | 600 | 800 |
237
+ | trace | 1000 | 1000(不变) |
238
+
239
+ decision 和 task 各上调 200 token。不改 manifest schema,只改 `getDefaultManifest` 中的默认值。
240
+
241
+ ### 4.3 批量重命名
242
+
243
+ **新命令:** `pmem rename`
244
+
245
+ ```bash
246
+ pmem rename --find <pattern> --replace <replacement> # 默认 dry-run,只预览
247
+ pmem rename --find <pattern> --replace <replacement> --write # 显式写入
248
+ ```
249
+
250
+ **安全设计:默认 dry-run**
251
+
252
+ `pmem rename` 不提供 `--dry-run` flag——默认行为就是 dry-run。真实写入必须通过显式 `--write` flag 确认。这避免了用户(或 agent)在未预览 diff 的情况下误改卡片内容。
253
+
254
+ ```
255
+ 无 --write → 扫描匹配文件 → 打印 diff 预览 → 输出 "N files would be changed. Add --write to apply." → exit 0
256
+ 有 --write → 扫描匹配文件 → atomicWrite 每个变更文件 → pmem rebuild --changed → 输出 "N files changed, M occurrences replaced." → exit 0
257
+ ```
258
+
259
+ **行为:**
260
+
261
+ 1. 扫描所有 `.pmem/**/*.md` 卡片文件的 **body 文本**(不碰 frontmatter 的 `id`、`aliases` 等结构化字段,不碰文件名)
262
+ 2. 对 body 内容执行 `String.prototype.replaceAll(pattern, replacement)`
263
+ 3. 默认模式(无 `--write`):显示 diff 预览,不写入,exit 0
264
+ 4. `--write` 模式:atomicWrite 每个变更文件 → 自动 rebuild --changed
265
+
266
+ **设计原则:**
267
+ - 只替换 body,不碰 frontmatter 结构字段。避免误改 `id` 导致图边断裂。
268
+ - 不重命名文件。卡片 ID 保持稳定。
269
+ - **默认 dry-run。** 任何不熟悉此命令的用户(或 agent)跑 `pmem rename` 不会产生副作用。
270
+ - 修改 frontmatter 需求明确超出 scope,不提供 `--include-frontmatter` flag。若用户需要改 ID/alias,引导他们手动编辑卡片。
271
+
272
+ **实现要点:**
273
+
274
+ ```
275
+ src/commands/rename.ts:
276
+ 1. loadManifest → 获取 source_of_truth.card_globs
277
+ 2. glob 匹配所有卡片文件
278
+ 3. 对每个文件:
279
+ a. 解析 frontmatter + body
280
+ b. body.replaceAll(find, replace)
281
+ c. 若 body 有变更 → 记录 diff
282
+ 4. 若 !options.write:
283
+ a. 打印 diff 预览
284
+ b. 输出 "N files would be changed. Add --write to apply."
285
+ c. exit 0
286
+ 5. 若 options.write:
287
+ a. atomicWrite 每个变更文件
288
+ b. 确保 frontmatter 部分逐字节不变(仅 body 变更,frontmatter hash 不变)
289
+ c. pmem rebuild --changed
290
+ 6. 输出:N files changed, M occurrences replaced
291
+ ```
292
+
293
+ **CLI 注册:**
294
+
295
+ ```typescript
296
+ program
297
+ .command('rename')
298
+ .description('Preview or apply batch text replacement in memory card bodies')
299
+ .requiredOption('--find <pattern>', 'Text to find in card bodies')
300
+ .requiredOption('--replace <replacement>', 'Replacement text')
301
+ .option('--write', 'Apply changes (default is dry-run preview only)', false)
302
+ .action(renameCommand);
303
+ ```
304
+
305
+ ---
306
+
307
+ ## 五、P1 详细设计
308
+
309
+ ### 5.1 `pmem recall --since`
310
+
311
+ recall 新增可选时间过滤参数:
312
+
313
+ ```bash
314
+ pmem recall --since 7d # 最近 7 天
315
+ pmem recall --since 24h # 最近 24 小时
316
+ pmem recall --since 1w # 最近 1 周
317
+ ```
318
+
319
+ 无效格式(如 `--since abc`、`--since 7x`)返回 exit 2 并输出清晰错误信息。
320
+
321
+ **时间来源契约**
322
+
323
+ 当前 `rebuild` 将 `cards.updated_at` 设为 `fm.updated ?? null`(`src/commands/rebuild.ts:131`)。若卡片 frontmatter 无 `updated` 字段,`updated_at` 为 `null`,导致 `--since` 过滤漏掉近期实际有变更的卡片。
324
+
325
+ 修正方案——时间来源优先级:
326
+
327
+ ```
328
+ 1. frontmatter.updated (用户显式声明的时间,最可信)
329
+ 2. 文件 mtime (文件系统修改时间,fallback)
330
+ 3. rebuild timestamp (本次 rebuild 的时间,最终兜底)
331
+ ```
332
+
333
+ **时间比较契约(钉死):**
334
+
335
+ | 层 | 格式 | 说明 |
336
+ |----|------|------|
337
+ | `cards.updated_at` | ISO 8601 字符串 (`2026-05-23T08:30:00.000Z`) | rebuild 阶段写入 |
338
+ | threshold | ISO 8601 字符串 | TS 解析 `--since` 值后计算 |
339
+ | SQL | `WHERE cards.updated_at >= ?` | 参数化查询,字符串字典序比较 |
340
+
341
+ **为什么不可以用 SQLite `datetime()` 比较:**
342
+
343
+ SQLite `datetime('now', '-7 days')` 返回 `YYYY-MM-DD HH:MM:SS`,与 ISO 8601 `2026-05-23T08:30:00.000Z` 格式不同。虽然字符串字典序碰巧可比较,但不构成契约。设计选择:**在 TS 层解析时间、计算 threshold ISO 字符串、参数化传入 SQL**,彻底避免格式依赖。
344
+
345
+ **--since 解析规则:**
346
+
347
+ ```typescript
348
+ function parseSince(since: string): string | null {
349
+ const match = since.match(/^(\d+)([hdw])$/);
350
+ if (!match) return null;
351
+ const value = parseInt(match[1], 10);
352
+ const unit = match[2];
353
+ const ms = unit === 'h' ? value * 3600000
354
+ : unit === 'd' ? value * 86400000
355
+ : unit === 'w' ? value * 604800000
356
+ : 0;
357
+ if (ms === 0) return null;
358
+ return new Date(Date.now() - ms).toISOString();
359
+ }
360
+ ```
361
+
362
+ 无效格式 → `null` → `console.log('Invalid --since format...')` → `process.exit(2)`。
363
+
364
+ 实现方式:
365
+
366
+ - `rebuild` 阶段:若 `fm.updated` 存在,写入 `updated_at`;否则使用文件 `mtime` 的 ISO 字符串。若两者皆不可得,使用当前 rebuild 时间的 ISO 字符串。
367
+ - `recall --since` 阶段:TS 解析 `--since` 值 → 计算 threshold ISO → SQL `WHERE cards.updated_at >= ?`。`updated_at = NULL` 的卡片不匹配(rebuild 兜底已消除 null)。
368
+
369
+ 此契约保证:
370
+ - 所有卡片都有 `updated_at`(消除 null)。
371
+ - 手动编辑卡片(mtime 更新)能正确反映在 `--since` 过滤中。
372
+ - `pmem update --confirm` 更新 frontmatter `updated` 字段的卡片,以声明时间为准。
373
+ - 时间比较使用同一种字符串格式(ISO 8601),字典序可正确比较。
374
+
375
+ ### 5.2 Exit code 修正(含兼容性验收)
376
+
377
+ **现状:**
378
+
379
+ | 命令 | 当前行为 | 代码位置 |
380
+ |------|---------|---------|
381
+ | `pmem status` | 有 changes → exit 1 | `src/commands/status.ts:206` |
382
+ | `pmem update --suggest` | 有 actionable suggestions → exit 1 | `src/commands/update.ts:699` |
383
+ | `pmem distill --suggest` | 有 distillation suggestions → exit 1 | `src/commands/distill.ts:94` |
384
+
385
+ **修改:** 三个命令统一为:有结果时 exit 0,仅在运行时错误(DB 打不开、卡片损坏等)时 exit 2。exit 1 不再作为 "workflow signal" 使用。
386
+
387
+ **影响面评估(全量):**
388
+
389
+ | 文件 | 影响 | 处理 |
390
+ |------|------|------|
391
+ | `src/commands/status.ts:206` | `process.exit(1)` → `process.exit(0)` | 改一行 |
392
+ | `src/commands/update.ts:699` | `process.exit(hasActionable ? 1 : 0)` → `process.exit(0)` | 改一行 |
393
+ | `src/commands/distill.ts:94` | `process.exit(1)` → `process.exit(0)` | 改一行 |
394
+ | `README.md:116,295` | 文档 "exits with code 1" | 更新为 exit 0 |
395
+ | `AGENTS.md:37` | 文档引用 exit code | 更新 |
396
+ | `docs/usage.md:131,199,316,333` | 多处 exit code 说明 | 更新 |
397
+ | `docs/handover-v0.4.md:37,41,44,143,251-255,318` | 退出码协议、命令表 | 标记为历史,更新当前约定 |
398
+ | `docs/handover-v0.6.md:81` | 设计规则 #7 | 更新 |
399
+ | `docs/v0.4 pre-design.md:251,253,255,261,406` | 退出码约定表 | 标记为 v0.4 历史 |
400
+ | `docs/v0.5 pre-design.md:104,106,212,286,300,304,405,428` | 退出码文档 | 标记为 v0.5 历史,README 已更新 |
401
+ | `docs/v0.6 pre-design.md:190,382,398` | v0.6 exit code 设计 | 标注为已变更 |
402
+ | `docs/v0.6.1 pre-design.md:98,202,206,406,438,451,499,523,545` | v0.6.1 exit code 设计 | 标注为已变更 |
403
+ | `skills/pmem/SKILL.md:93,143,177` | Agent skill 引用 | 更新 |
404
+ | `CLAUDE.md` | 项目级 agent 指令,exit code 表 | 更新 |
405
+ | `CHANGELOG.md` | 已有记录,需追加 breaking change | 追加 v0.6.2 条目 |
406
+
407
+ > **影响面计数:15 个文件(3 个代码文件 + 12 个文档/配置/技能文件)。**
408
+
409
+ **兼容性验收清单(开工后执行):**
410
+
411
+ - [ ] 三个命令的 exit code 在 "有结果" 场景为 0
412
+ - [ ] 三个命令的 exit code 在 "运行时错误" 场景为 2
413
+ - [ ] `README.md` exit code 文档已更新
414
+ - [ ] `docs/usage.md` exit code 文档已更新
415
+ - [ ] `skills/pmem/SKILL.md` 已更新
416
+ - [ ] `AGENTS.md` 已更新
417
+ - [ ] `CLAUDE.md` exit code 表已更新
418
+ - [ ] `CHANGELOG.md` 标注 breaking change
419
+ - [ ] 所有 `docs/*pre-design*.md` 历史文档加注 "v0.6.2 已变更"
420
+ - [ ] E2E 覆盖三个命令的 exit code 场景
421
+
422
+ **风险与缓解:**
423
+
424
+ - 依赖 exit 1 的现有脚本会行为变化 → CHANGELOG 显著标注 + README migration note
425
+ - CI pipeline 中若有 `pmem update --suggest || echo "needs update"` 会失效 → 改为检查 JSON 输出中的 `summary.has_actionable`
426
+
427
+ ### 5.3 卡片模板生成 `pmem new`
428
+
429
+ ```bash
430
+ pmem new decision "MLX-only Mac support decision"
431
+ pmem new task "Implement voice input module"
432
+ pmem new module "audio-pipeline"
433
+ ```
434
+
435
+ **行为:**
436
+ 1. 根据卡片类型确定目标目录(`decisions/`、`tasks/`、`modules/` 等)
437
+ 2. 生成唯一 ID(基于标题 + 时间戳)
438
+ 3. 写入带完整 YAML frontmatter 的空卡片文件
439
+ 4. 当场校验必填字段完整性
440
+ 5. 输出文件路径和下一步建议
441
+
442
+ **CLI 可触发的错误场景(E2E 验收用):**
443
+
444
+ | 场景 | 命令 | 预期结果 |
445
+ |------|------|---------|
446
+ | 正常生成 | `pmem new decision "My Decision"` | 写入文件,rebuild/verify 通过 |
447
+ | 空 title | `pmem new task ""` | 拒绝写入,exit 2,提示 title 必填 |
448
+ | 非法 type | `pmem new invalid_type "Test"` | 拒绝写入,exit 2,列出有效类型 |
449
+ | 无参数 | `pmem new` | Commander 报 missing argument |
450
+
451
+ 内部 validator 可额外检查 frontmatter 缺字段等边界,但 E2E 仅测 CLI 可触发的错误。
452
+
453
+ **生成的 frontmatter 模板(以 decision 为例):**
454
+
455
+ ```yaml
456
+ ---
457
+ id: decision.mlx_only_mac_20260523
458
+ type: decision
459
+ title: "MLX-only Mac support decision"
460
+ status: draft
461
+ tags: []
462
+ created: "2026-05-23"
463
+ source_files: []
464
+ depends_on: []
465
+ related_to: []
466
+ ---
467
+ # MLX-only Mac support decision
468
+
469
+ <!-- TODO: describe the decision, alternatives considered, and rationale -->
470
+ ```
471
+
472
+ ### 5.4 Frontmatter 创建时校验
473
+
474
+ 在 `pmem new` 写入文件前校验:
475
+ - `type` 是有效类型(`decision|module|task|feature|risk|trace`)
476
+ - `title` 非空且非空白字符串
477
+ - 必填字段(id、type、title、status、created)完整性
478
+
479
+ 校验失败时当场报错,列出缺失/无效字段,不写入文件。这比当前"rebuild 时静默跳过无效卡片"的行为更友好。
480
+
481
+ ### 5.5 Git hook 集成
482
+
483
+ ```bash
484
+ pmem integration install git-hooks
485
+ ```
486
+
487
+ **行为:**
488
+ 1. 检测 `.git/hooks/` 是否存在
489
+ 2. 写入 `.git/hooks/pre-commit`(或追加到已有 hook):
490
+
491
+ ```bash
492
+ #!/bin/sh
493
+ # pmem pre-commit hook — verify memory consistency before commit
494
+ pmem verify --relaxed
495
+ ```
496
+
497
+ 3. `pmem integration verify` 检查 hook 是否已安装
498
+ 4. `pmem integration list` 增加 `git-hooks` 条目
499
+
500
+ ### 5.6 Session 健壮性排查
501
+
502
+ 调查 "No active pmem session found" 在已知 `session start` 已执行后仍然出现的根因。可能原因:
503
+
504
+ - `session start` 写入 SQLite 后进程退出,下次 `session end` 在另一个进程中读取,WAL 未同步
505
+ - 数据库连接关闭时机导致未提交
506
+ - `session start` 检查到已有 active session 但用户在不知情的情况下多次 start
507
+
508
+ 排查策略:
509
+ 1. 复现:在 temp 目录模拟快速 start → end 周期
510
+ 2. 检查 WAL 模式下的写入可见性
511
+ 3. 若根因在 WAL,在 `session start` 后显式执行 checkpoint
512
+ 4. 若根因在其他,针对性修复
513
+
514
+ ---
515
+
516
+ ## 六、测试计划
517
+
518
+ ### P0 测试(已通过 ✅)
519
+
520
+ | 测试项 | 类型 | 状态 |
521
+ |--------|------|------|
522
+ | `acquireLock` stale lock 自动清理 | E2E | ✅ |
523
+ | `acquireLock` active lock 超时保留 | E2E | ✅ |
524
+ | `verify --fix-locks` 清理 stale lock | E2E | ✅ |
525
+ | `verify --fix-locks` 不删除 active lock | E2E | ✅ |
526
+ | `doctor` lock 三种状态输出 | E2E | ✅ |
527
+ | `verify` 检测 oversized card | E2E | ✅ |
528
+ | `verify --relaxed` 翻倍限制 | E2E | ✅ |
529
+ | manifest `max_tokens` 自定义生效 | E2E | ✅ |
530
+ | `rename` 默认 preview 不写入 | E2E | ✅ |
531
+ | `rename --write` 写入 + rebuild + verify | E2E | ✅ |
532
+ | `rename` 后 frontmatter hash 不变 | E2E | ✅ |
533
+ | `rename --find ""` reject + exit 2 | CLI | ✅ |
534
+ | `update --confirm` 锁失败文案更新 | CLI | ✅ |
535
+
536
+ ### P1 测试(待执行)
537
+
538
+ - 单元测试:rebuild `updated_at` 时间契约(`fm.updated` / mtime / rebuild time fallback)
539
+ - E2E:`recall --since 7d` 只返回近期卡片
540
+ - 单元测试:三个命令 exit code 在有结果时为 0
541
+ - 单元测试:三个命令 exit code 在运行时错误时为 2
542
+ - E2E:`pmem new` 各类型模板生成正确
543
+ - E2E:`pmem new` 空 title / 非法 type / 无参数拒绝写入
544
+ - E2E:`pmem integration install git-hooks` 写入 pre-commit hook
545
+ - E2E:`pmem integration verify` 检测 git-hooks
546
+ - E2E:session start → end 周期复现 + 修复验证
547
+
548
+ ---
549
+
550
+ ## 七、无 schema migration
551
+
552
+ v0.6.2 不改变 manifest schema_version、不改变卡片 frontmatter 结构、不改变数据库表结构。
553
+
554
+ 变更仅涉及:
555
+ - CLI 行为(exit code、新命令、新参数)
556
+ - 运行时行为(lock 自动清理、verify --relaxed)
557
+ - 默认值(card_policy.max_tokens)
558
+
559
+ 所有现有 `.pmem/` 目录在 v0.6.2 下无需迁移即可正常使用。
560
+
561
+ ---
562
+
563
+ ## 八、与已有推迟项的关系
564
+
565
+ v0.6.1 pre-design 中推迟到 v0.6.2 的项:
566
+
567
+ | 推迟项 | v0.6.2 处理 |
568
+ |--------|------------|
569
+ | `pmem dirty list` | 不进 v0.6.2 |
570
+ | `pmem dirty resolve --auto` | 不进 v0.6.2 |
571
+ | `pmem dirty prune --older-than 7d` | 不进 v0.6.2 |
572
+ | 完整 dirty lifecycle | 不进 v0.6.2 |
573
+ | `--current-session` / auto-cleanup | 不进 v0.6.2 |
574
+
575
+ 这些项继续推迟。v0.6.2 由用户反馈驱动,不主动扩展 dirty lifecycle。
576
+
577
+ ---
578
+
579
+ ## 九、P1 实现批次
580
+
581
+ ### 9.1 实现顺序
582
+
583
+ | # | 任务 | 复杂度 | 依赖 |
584
+ |---|------|--------|------|
585
+ | 1 | `recall --since` + `rebuild` 时间契约落地 | 低 | 无 |
586
+ | 2 | exit code 修正(3 个命令 + 15 个文件) | 中 | 无 |
587
+ | 3 | `pmem new <type> "<title>"` + frontmatter 创建时校验 | 低 | 无 |
588
+ | 4 | `pmem integration install git-hooks` | 低 | 无 |
589
+ | 5 | session 健壮性排查 + 修复 | 中 | 需在 temp 目录复现 |
590
+
591
+ P1 各任务相互独立,可并行实现,但打包为同一批次提交。
592
+
593
+ ### 9.2 P1 DoD
594
+
595
+ - [ ] `npm test` 通过
596
+ - [ ] `recall --since 7d` 正确过滤,`rebuild` 阶段 `updated_at` 无 null
597
+ - [ ] `recall --since abc` / `recall --since 7x` 返回 exit 2 + 错误信息
598
+ - [ ] `pmem status` / `update --suggest` / `distill --suggest` 有结果时 exit 0,错误时 exit 2
599
+ - [ ] 15 个受影响文件已更新(3 代码 + 12 文档/配置/技能)
600
+ - [ ] `CHANGELOG.md` 标注 exit code breaking change
601
+ - [ ] `pmem new decision "test"` 生成有效卡片 → rebuild → verify 通过
602
+ - [ ] `pmem new task ""` 拒绝写入 + exit 2
603
+ - [ ] `pmem new invalid_type "test"` 拒绝写入 + exit 2
604
+ - [ ] `pmem integration install git-hooks` 写入 `.git/hooks/pre-commit`
605
+ - [ ] `pmem integration verify` 检测到 git-hooks
606
+ - [ ] session 排查结论记录,若有修复则附带 E2E 验证
607
+ - [ ] E2E 覆盖 recall --since、exit code、pmem new、git-hooks
608
+
609
+ ### 9.3 不与 P0 混包
610
+
611
+ P1 作为独立批次,在 P0 已验收的代码基础上增量提交。P1 验收不重新检查 P0 项,除非 P1 修改触及 P0 代码路径。
612
+
613
+ ---
614
+
615
+ ## 十、发布验收
616
+
617
+ ### P0(已验收 ✅)
618
+
619
+ - [x] `npm test` 通过(90/90)
620
+ - [x] E2E 覆盖 stale lock 自动清理、active lock 超时、`verify --fix-locks`、`doctor` lock 输出
621
+ - [x] E2E 覆盖 manifest token 上调、`verify --relaxed`
622
+ - [x] E2E 覆盖 `rename` 默认 dry-run 不写入、`--write` 后 rebuild verify 通过、frontmatter hash 不因 rename 变化
623
+ - [x] CTO 验收通过(2026-05-23)
624
+
625
+ ### P1(待实现)
626
+
627
+ - [ ] 所有 P1 DoD 项通过
628
+ - [ ] CI 通过(Node 18/20/22)
629
+ - [ ] E2E 覆盖 recall --since、exit code、pmem new、git-hooks、session
630
+
631
+ ### 通用
632
+
633
+ - [ ] CI 通过(Node 18/20/22)
634
+ - [ ] CHANGELOG 记录 exit code breaking change
635
+ - [ ] docs/project-roadmap.md 更新 v0.6.2 状态为"已发布"
636
+
637
+ ---
638
+
639
+ ## 十一、审批记录
640
+
641
+ | 角色 | 决定 | 日期 |
642
+ |------|------|------|
643
+ | 提交人 | Claude Opus 4.6 | 2026-05-23 |
644
+ | CTO | 有条件批准 P0 开工;P1 设计待补强 | 2026-05-23 |
645
+ | CTO | P0 验收通过;批准 P1 作为独立批次进入实现,不与 P0 混包 | 2026-05-23 |
646
+
647
+ ### CTO 修正点(已在此文档中落实)
648
+
649
+ 1. **`pmem rename` 安全语义修正**(§4.3):默认 dry-run,显式 `--write` 才写入。`--dry-run` flag 已移除。
650
+ 2. **`recall --since` 时间契约补全**(§5.1):明确时间来源优先级 `frontmatter.updated > 文件 mtime > rebuild timestamp`,消除 `updated_at IS NULL` 漏洞。
651
+ 3. **exit code 兼容性验收单列**(§5.2):全量影响面评估 15 个文件,兼容性验收清单 11 项。
652
+
653
+ ### P0 复验修正(已落实)
654
+
655
+ - `update --confirm` 锁失败文案已改为可恢复指引(`update.ts:247`)
656
+ - `rename --find ""` 已拒绝执行并返回 exit 2(`rename.ts:28`)
657
+ - rename body 不再 `.trim()`:仅移除显式 trim 调用,`splitFrontmatter` 中的 `trimStart()` 记为已知非阻塞 P2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmem-ai",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Project Memory for AI Agents: a local CLI runtime for project context, recall, and memory updates",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "test:e2e:v06-claude": "bash scripts/e2e-v06-claude-integration.sh",
20
20
  "test:e2e:v06-empty": "bash scripts/e2e-v06-empty-guidance.sh",
21
21
  "test:e2e:v06-nongit": "bash scripts/e2e-v06-non-git-ux.sh",
22
- "test:e2e:v06-skills": "bash scripts/e2e-v06-skills.sh"
22
+ "test:e2e:v06-skills": "bash scripts/e2e-v06-skills.sh",
23
+ "test:e2e:v061-suggest": "bash scripts/e2e-v061-suggest.sh"
23
24
  },
24
25
  "keywords": [
25
26
  "agent",