@fanchao8609/agent_brain_sync 1.17.0 → 1.17.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.
package/README.md CHANGED
@@ -105,7 +105,8 @@ abs load # 开机读状态(Rules + 图谱计数 + 看板
105
105
  abs todo # 看板 Todo / Done(行首带状态标记)
106
106
 
107
107
  abs todo add TASK-1 --note "要做什么"
108
- abs todo note TASK-1 --note "改到 X 文件 L40" # 实时断点
108
+ abs todo note TASK-1 --note "改到 X 文件 L40" # 实时断点(一句话,上看板)
109
+ abs todo memo TASK-1 "随手记一段(可多句,不上看板)" # 任务笔记 → sessions/;无参=查看
109
110
  abs todo state TASK-1 --note "进行中|讨论中|滞留中" # 改行首状态标记
110
111
  abs todo done TASK-1
111
112
 
@@ -188,7 +189,8 @@ agent_brain_sync/
188
189
  ├── bin/mcp.js MCP server (stdio)
189
190
  ├── src/index.js 图谱定位(只认当前目录的 .brain/)+ 全局技术日志目录
190
191
  ├── src/lock.js 并发写保护(原子锁 + 排队 + SKIP)
191
- ├── src/todo.js todo.md 分区读写 + 格式闸门
192
+ ├── src/todo.js todo 对外入口(re-export,实现见 src/todo/)
193
+ ├── src/todo/ todo.md 分区读写 + 格式闸门(common/spec/structure/validate/archive/tasks)
192
194
  ├── src/store.js CLI 命令实现
193
195
  ├── src/lint.js 图谱体检(死链/孤岛/超尺寸/堆积;同类问题折叠计数)
194
196
  ├── src/query.js 检索知识页(多词 OR)
@@ -201,7 +203,10 @@ agent_brain_sync/
201
203
  ├── hooks/event.sh hook 模板
202
204
  ├── hooks/abs.pi.ts pi 扩展模板(含 todo 面板 / 随机昵称 / 常驻指引)
203
205
  ├── skill/<名称>/SKILL.md 技能(每个子目录 = 一个 skill,装到各智能体)
204
- └── test/ 单测(448,node:test,零外部测试依赖)
206
+ ├── scripts/ 构建期脚本(语法检查等,不进 npm 包)
207
+ ├── docs/plans/ 方案文件(YYYYMMDD-<名>.md,不进 npm 包)
208
+ ├── .githooks/ pre-commit(语法+图谱 lint)/ pre-push(全量测试)
209
+ └── test/ 单测(node:test,零外测试依赖)
205
210
  ```
206
211
 
207
212
  MIT
package/bin/abs.js CHANGED
@@ -310,6 +310,7 @@ const subUsage = {
310
310
  ' abs todo add <id> [--note "做什么"] [--section 讨论中|滞留中|搁置]',
311
311
  ' abs todo rename <旧id> --note "<新id>" 改任务 id(起错名/不可读时)',
312
312
  ' abs todo note <id> --note "断点/进度"',
313
+ ' abs todo memo <id> "随手记(可多句)" 任务笔记 → sessions/,不上看板;无文字=查看',
313
314
  ' abs todo state <id> --note 进行中|讨论中|滞留中|搁置',
314
315
  ' abs todo done <id> [--as 落地|否决|仅方案] [结语文字]',
315
316
  ' abs todo archive [--keep-days N] [--dry-run]',
@@ -373,6 +374,7 @@ const TODO_ACTIONS = {
373
374
  add: 'start',
374
375
  start: 'start', // add 的别名(老习惯保留)
375
376
  note: 'note',
377
+ memo: 'memo', // 任务随手记:多句/追加/不占看板(无文字时=查看)
376
378
  state: 'state', // 改行首状态标记:进行中|讨论中|滞留中|搁置(原地,不搬区)
377
379
  rename: 'rename', // 改任务 id:人工起错名、或旧 id 不可读时用
378
380
  done: 'done',
@@ -467,7 +469,7 @@ async function main() {
467
469
  if (!action) {
468
470
  throw new Error(
469
471
  `✗ 未知子命令 "${sub}"\n` +
470
- ` 可用: add / start / note / state / done / archive\n` +
472
+ ` 可用: add / start / note / memo / state / done / archive\n` +
471
473
  ` 看板: abs todo(不带参数)\n` +
472
474
  ` 登记任务: abs todo add <id> --note "做什么"`,
473
475
  );
@@ -475,13 +477,21 @@ async function main() {
475
477
  // done 的结语:--note "结语" 或位置参数(`abs todo done X 验证: 全绿`)都收。
476
478
  // 坑(2026-09-16 实测): --help 里写了 [结语文字] 但实现只读 --note,
477
479
  // 位置参数被 rejectExtra 当多余参数拒掉 —— 文档与实现不一致。
480
+ // memo 同:随手记常是多句带空格,位置参数收全段比 --note 顺手。
478
481
  const isDone = action === 'done';
482
+ const isMemo = action === 'memo';
479
483
  const usage = `abs todo ${sub} <id>${isDone ? ' [--as 落地|否决|仅方案] [--note "结语"]' : ' --note "…"'}`;
480
484
  if (isDone) {
481
485
  const note = opts.note || (rest2.length ? rest2.join(' ') : undefined);
482
486
  console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note, as: opts.as, session: opts.session }));
483
487
  break;
484
488
  }
489
+ if (isMemo) {
490
+ // 无文字 = 查看该任务所有随手记
491
+ const note = opts.note || (rest2.length ? rest2.join(' ') : undefined);
492
+ console.log(await cmdTask({ dir: opts.dir, action, id, note, session: opts.session }));
493
+ break;
494
+ }
485
495
  rejectExtra(rest2, usage);
486
496
  if (!id) throw new Error(`✗ 缺 <id>\n 用法: ${usage}`);
487
497
  console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note, as: opts.as, session: opts.session }));
package/bin/mcp.js CHANGED
@@ -112,12 +112,12 @@ tool(
112
112
 
113
113
  tool(
114
114
  'abs_task',
115
- '任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补断点(改到哪文件哪行) / state 改状态 / done 完成归位 Done(done 时 note=结语文字,as=结语类型:落地|否决|仅方案)。必填: cwd, action, id。多会话并行时,start/add 带上 session(值见 system prompt 的 [会话] 段)以标记「这条任务谁在做」。',
115
+ '任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补断点(改到哪文件哪行) / memo 任务随手记(多句/追加/不上看板)/ state 改状态 / done 完成归位 Done(done 时 note=结语文字,as=结语类型:落地|否决|仅方案)。必填: cwd, action, id。多会话并行时,start/add 带上 session(值见 system prompt 的 [会话] 段)以标记「这条任务谁在做」。',
116
116
  {
117
- action: z.enum(['add', 'start', 'done', 'note', 'state']),
117
+ action: z.enum(['add', 'start', 'done', 'note', 'memo', 'state']),
118
118
  id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
119
119
  cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
120
- note: z.string().optional().describe('add/start=做什么; note=断点(文件/到哪步); state=进行中|讨论中|滞留中|搁置(搁置=用户改方向/不做了;滞留中=还要做只是卡住); done=结语文字'),
120
+ note: z.string().optional().describe('add/start=做什么; note=断点(文件/到哪步,一句话); memo=随手记(可多句、不定长,追加式;不传=查看已有笔记); state=进行中|讨论中|滞留中|搁置(搁置=用户改方向/不做了;滞留中=还要做只是卡住); done=结语文字'),
121
121
  as: z.enum(['落地', '否决', '仅方案']).optional().describe('仅 done:结语类型。默认 落地。做了又撤/评估后不做用 否决,只设计过用 仅方案 —— 别让假【落地】污染看板'),
122
122
  // 会话 id:多会话并行时标记「这条任务是谁在做」。
123
123
  // 值取自 system prompt 里的 `[会话] 本会话 id = xxx`(hook 注入),由模型原样回传。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,44 +1,42 @@
1
1
  ---
2
2
  name: abs-agent-brain-sync
3
- description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。触发词:用户直接输入 abs(单独一个词,或 abs + 命令,如 'abs 开工'、'abs 沉淀');以及「开工续接状态」「沉淀会话收获」「任务登记不丢」「经验落成知识页」「图谱体检」等任务。"
3
+ description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。触发词:用户直接输入 abs(单独一个词或 abs + 命令,如 'abs 开工'、'abs 沉淀'),以及「开工续接状态」「沉淀会话收获」「任务登记不丢」「经验落成知识页」「图谱体检」等任务。"
4
4
  ---
5
5
 
6
6
  # abs — 跨会话记忆 (agent-brain-sync)
7
7
 
8
8
  ## 最高优先:触发总则(凌驾本文所有流程)
9
9
 
10
- **关键词不是触发器,意图才是。**句子里出现 abs 词(收尾/todo/沉淀/load/log/note)不等于要执行 abs 动作:谈论、提问、吐槽、定规则("太啰嗦""这个设计怎样")**只回应,不执行动作**;明确让我做事才执行。
10
+ **关键词不是触发器,意图才是。**句子里出现 abs 词(收尾/todo/沉淀/load/log/note)不等于要执行:谈论、提问、吐槽、定规则**只回应**,明确让我做事才执行。
11
11
 
12
- **例外(唯一):用户直接输入 `abs`** —— 单独一个词、或 `abs` 后跟命令/意图(如 `abs`、`abs 开工`、`abs 沉淀`、`abs load`)。这是明确的"我要用 abs",不算关键词误触发,**直接按 abs 流程执行**(无子命令时默认走开工续接)。
12
+ **例外(唯一):用户直接输入 `abs`** —— 单独一个词、或 `abs` 后跟命令/意图(如 `abs 开工`、`abs 沉淀`)。这是明确的"我要用 abs",**直接按 abs 流程执行**(无子命令时默认走开工续接)。
13
13
 
14
- - 拿不准时**先问**。规则变更("以后简短点")写入本文,不执行动作。
15
- - 输出简短:收尾/提示/todo 汇报一两句。本总则适用于所有工具。
14
+ - 拿不准**先问**。规则变更("以后简短点")写入本文,不执行动作;输出简短(收尾/提示/todo 一两句)。
16
15
 
17
16
  ## 命令速查
18
17
 
19
- 完整帮助:`abs help`。agent 读写优先走 MCP(`abs_load`/`abs_task`/`abs_note`/`abs_query`/`abs_lint`/`abs_rule`)比 bash 跑 CLI 快;`abs wrapup` / `abs teardown-check` 是 hook 内部命令。
20
- 分工:hook 自动记技术日志到 `~/.abs/log/`(不用管);CLI/MCP 实时落盘;**skill 负责深提炼 + 收尾 + 修 index**。
18
+ 完整帮助:`abs help`。agent 读写优先走 MCP(`abs_load`/`abs_task`/`abs_note`/`abs_query`/`abs_lint`/`abs_rule`)比 CLI 快;`abs wrapup`/`abs teardown-check` 是 hook 内部命令。分工:hook 记技术日志(不用管);CLI/MCP 实时落盘;**skill 负责深提炼 + 收尾 + 修 index**。
21
19
 
22
20
  ```bash
23
21
  abs load # 开机读状态;abs query <词> 检索;abs resolve <页名> 反查路径
24
- abs todo add <id> --note "做什么" # 登记(note 补断点 / state 改标记 / done 完成);abs log "完成 X" 记成果
22
+ abs todo add <id> --note "做什么" # 登记(note/memo/state/done 见下「进行中」);abs log "完成 X" 记成果
25
23
  abs note "经验" [--tags a,b] # 暂存经验;abs concept <slug> --title "…" 建概念页骨架
26
- abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs todo archive 归档;abs init 建图谱
24
+ abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs init 建图谱;abs todo archive 归档
27
25
  ```
28
26
 
29
27
  ## 触发总入口(每次命中技能,第一步先走这里)
30
28
 
31
29
  技能被触发时第一步走这条链,再看用户要什么:
32
30
 
33
- 1. **找图谱**:只看**当前目录**是否含 `.brain/`。不许向上搜索(会命中 `~/.brain` 的无关图谱,把项目静默挂错),必须 cd 到项目根。
34
- 2. **没有?按意图定建档**:意图是**沉淀**或**即将开工的长期任务**("帮我做 X / 开工 / 修 X")→ **先 `abs init`**,别在没图谱时就开写;**闲聊 / 一次性问句 / 明说不要建档 / 只读目录** → 不建档,当普通会话处理。
31
+ 1. **找图谱**:只看**当前目录**是否含 `.brain/`,必须 cd 到项目根。不许向上搜索(会命中 `~/.brain` 的无关图谱,把项目静默挂错)。
32
+ 2. **没有图谱?按意图定建档**:意图是**沉淀**或**即将开工的长期任务**("帮我做 X / 开工 / 修 X")→ **先 `abs init`**;**闲聊 / 一次性问句 / 明说不要建档 / 只读目录** → 不建档。
35
33
  3. **有/刚建好?分流**:
36
34
 
37
35
  | 用户意图 | 走哪 |
38
36
  |---|---|
39
37
  | 总结经验 / 结束了 / "把这次记下来" | 收尾循环(沉淀) |
40
38
  | 查询坑 / "我上次怎么解决 X" | `abs query` 检索 |
41
- | 体检图谱 / `abs lint` | `abs lint` |
39
+ | 体检图谱 | `abs lint` |
42
40
  | 开新任务 / 继续开发 / "帮我做 X" | 开场 Init Sync(续接);新需求先走受理协议 |
43
41
  | 报 bug / 要求加功能 | **受理协议**:先方案 → 再登记 → 问开工 |
44
42
  | 意图不明 / 默认 | **续 todo**:读未完成项开工续做 |
@@ -55,49 +53,34 @@ abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs t
55
53
 
56
54
  **例外(可直接开工,回复里须说明援引哪一条)**:用户已说"开工/直接做/修它";一行级修字、纯查询、纯收尾沉淀;同一需求已登记且用户确认过。
57
55
 
58
- `.brain/` 放项目根,一个项目一份;所有命令只认当前目录的 `.brain/`,不向上搜索。`abs status` 显示当前定位。
56
+ `.brain/` 放项目根,一个项目一份。`abs status` 显示当前定位。
59
57
 
60
58
  ## `.brain/` 怎么组织
61
59
 
62
- `.brain/` 文件结构(`abs init` 生成,`--repair` 只补缺不覆盖):
63
-
64
- 文件结构(全部在 `.brain/` 下):
65
-
66
- ```
67
- / .brain/
68
- index.md log.md todo.md
69
- entities/ concepts/ sources/ syntheses/ sessions/
70
- ```
71
-
72
60
  | 位置 | 放什么 | 硬约束 |
73
61
  |---|---|---|
74
- | `index.md` `## Rules` | 本项目铁律 | 一句一条,不带链接;只有「违反会丢数据/静默失效/白干活」级才进(普通经验进 `concepts/`);>42 字符或含链接/URL 被拒 |
75
- | `index.md` 清单区 | 各类页清单 | 每页一行 `- [[slug]] — 一句话` |
62
+ | `index.md` | `## Rules` 铁律 + 各类页清单 | Rules 一句一条不带链接,只有「违反会丢数据/静默失效/白干活」级才进(普通经验进 `concepts/`),>42 字符或含链接/URL 被拒;清单每页一行 `- [[slug]] — 一句话` |
76
63
  | `log.md` | 成果流水,倒序一行 | 只记成果,不收工具动作流水(那在 `~/.abs/log/`) |
77
- | `todo.md` | 活看板,只有 `## Todo` + `## Done` | 未完成用行首标记 `[进行中]`/`[讨论中]`/`[滞留中]`;Done 必带 `【落地/否决/仅方案】` + `(完成 YYYY-MM-DD)` |
78
- | `entities/` | 具名事物("它是什么") | `Docker.md` |
79
- | `concepts/` | 可复用规律/坑("这么做就避坑") | `docker-prisma-429.md`;归类拿不准时默认这里 |
80
- | `sources/` | 经验暂存(`abs note` 自动落) | `YYYY-MM-DD-slug.md`;是暂存,不是归档 |
81
- | `syntheses/` | 横向选型/架构取舍 | `synthesis-slug.md` |
82
- | `sessions/` | 会话快照 + 当日归档段 | **一天只允许一个文件,只能叫 `log-<日期>.md`** |
83
-
84
- `todo.md` 行形态:`- [ ] [进行中] <id> [[认领人]] — 说明 (认领 YYYY-MM-DD)`(`[[认领人]]` = 使用者名,非任务名)。`abs todo done <id>` 勾选并归位到 Done 日期组顶部,断点(`↳` 行)随迁;超 3 天且整天完成的组由 `abs wrapup` 迁进当天快照的归档段。
64
+ | `todo.md` | 活看板,只有 `## Todo`/`## Done` | 未完成带行首标记;Done 必带 `【落地/否决/仅方案】`+`(完成 YYYY-MM-DD)` |
65
+ | `entities/` `concepts/` | 具名事物(`Docker.md`)/ 可复用规律坑(`docker-prisma-429.md`) | 归类拿不准时默认进 `concepts/` |
66
+ | `sources/` `syntheses/` | 经验暂存(`abs note` 自动落 `YYYY-MM-DD-slug.md`)/ 横向选型取舍(`synthesis-slug.md`) | sources 是暂存不是归档,提炼后删 |
67
+ | `sessions/` | 会话快照 + 当日归档段 | **一天只允许一个文件,只能叫 `log-<日期>.md`**(多主题就多写 `##` 段) |
85
68
 
86
- **`sessions/` 一天一个文件**(多主题多写几个 `##` 段,不许拆文件),含快照正文(AI 手写)+ `## 关联连接` + `## 🪝 Next Session Hook`(强制)+ `## 📦 任务归档`(机器写,勿手改)。归档唯一动作 `abs todo archive`:已有快照只替换归档段(段外内容逐字保留),没快照则建一个只有归档段的文件,同日二次归档段内追加。
69
+ `todo.md` 行形态:`- [ ] [进行中] <id> [[认领人]] — 说明 (认领 YYYY-MM-DD)`(标记还有 `[讨论中]`/`[滞留中]`;`[[认领人]]` = 使用者名,非任务名)。`abs todo done <id>` 勾选并归位到 Done 日期组顶部,断点(`↳` 行)随迁;超 3 天**且整天已完成**的组由 `abs wrapup` 迁进当天快照归档段(任一天还有未完成则整天不归档,不拆半天)。
87
70
 
88
- `abs lint` 兜底:死链 / 孤岛 / 悬挂页 / 缺 frontmatter / 超限 / sources 堆积 / 超龄未提炼 / index 漏列 / Rules 超限 / 缺尾。
71
+ `sessions/` 快照含正文 + `## 关联连接` + `## 🪝 Next Session Hook`(强制)+ `## 📦 任务归档`(机器写勿手改)。归档唯一动作 `abs todo archive`:已存在快照只替换归档段、段外逐字保留;那天没写过快照则建一个只有归档段的页;同日重复归档在段内追加。
89
72
 
90
73
  ### 容量纪律(写任何页之前过四关)
91
74
 
92
- 不过关就不写或压缩:① **再命中** —— 下会话不知道这条会踩同坑/重做同决定?不会就不存,代码能 grep 到的一律不记。② **单页上限** —— `entities/concepts/syntheses` 单页 <150 行且 <8KB,超了拆或外链。③ **sources 是暂存** —— 提炼成规律后删/归档,并清引用(防死链)。④ 能不能用一行链接代替新增整页?
75
+ 不过关就不写或压缩:① **再命中** —— 下会话不知道这条会踩同坑/重做同决定?不会就不存,代码 grep 得到的不记。② **单页上限** —— `entities/concepts/syntheses` 单页 <150 行且 <8KB,超了拆或外链。③ **sources 是暂存** —— 提炼后删/归档并清引用。④ 能用一行链接代替新增整页吗?
93
76
 
94
77
  ## 开场:Init Sync(开工 / 默认续 todo)
95
78
 
96
79
  收到第一个核心开发指令**之前**走这条链载入上下文:
97
80
 
98
- 1. **读状态**:`abs load`(MCP `abs_load`)读 Rules + 图谱计数 + todo + 最近 log。**开工前先看 `## Rules`**。load 输出是折叠的(Done 按日期计数、清单只给页数);全量明细用 `abs todo --full` / `abs index`。
99
- 2. **对账滞留(强制,别跳过)**:若顶部出现 `⏳ 上会话滞留`,说明上会话任务断了,先收尾再开工 —— 真做完的 `abs todo done <id>`;没做完的 `abs todo note <id> --note "接到哪/改到哪个文件"` 补断点。
100
- 3. **读命中页**:`abs query <词>` → `abs resolve <页名或id>` 拿路径后**只读命中那几页**。**绝不通读 `.brain/`**;汇总多文件用 `ctx_execute` 类工具在沙箱里处理,只打印结论。
81
+ 1. **读状态**:`abs load`(MCP `abs_load`)读 Rules + 图谱计数 + todo + 最近 log,**先看 `## Rules`**。输出折叠(Done 按日期计数、清单只给页数);全量明细用 `abs todo --full` / `abs index`。
82
+ 2. **对账滞留(强制)**:顶部出现 `⏳ 上会话滞留` 说明上会话断了,先收尾再开工 —— 做完的 `abs todo done <id>`;没做完的补断点 `abs todo note <id> --note "接到哪"`。
83
+ 3. **读命中页**:`abs query <词>` → `abs resolve <页名或id>` 拿路径后**只读命中那几页**,**绝不通读 `.brain/`**;多文件汇总交给 `ctx_execute` 在沙箱处理,**只打印结论**。
101
84
  4. **续 todo**:把顶部未完成项当当前任务开做;有明确新任务而 todo 没有 → `abs todo add <id> --note 做什么` 再动工。
102
85
 
103
86
  ## 进行中:什么时候动它
@@ -106,24 +89,34 @@ abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs t
106
89
 
107
90
  | 时机 | `abs_task` action | 落到哪 |
108
91
  |---|---|---|
109
- | 认领新任务 / 聊出一个话题 | `start` + note 做什么 | Todo `[进行中]` |
110
- | 只在讨论、还没动手 | `state` + note 讨论中 | 原地改标记 |
111
- | 卡住了/等人等数据 | `state` + note 滞留中 | 原地改标记 |
112
- | 被打断/干到一半 | `note` + 改到哪个文件/到哪步 | 原地 ↳断点 |
92
+ | 认领新任务 | `start` + note 做什么 | Todo `[进行中]` |
93
+ | 只讨论没动手 / 卡住等人等数据 | `state` + note 讨论中\|滞留中 | 原地改标记 |
94
+ | 被打断/干到一半 | `note` + 改到哪个文件哪一步 | 原地 ↳断点(**一句话**) |
95
+ | 干活中想随手记一段(多句/发现/取舍) | **`memo`** + 原文 | `sessions/log-<日期>-<id>.md`,**不上看板** |
113
96
  | 子任务做完 | `done` + note 结语 + as 落地\|否决\|仅方案 | Done(别让假【落地】污染看板) |
114
97
  | 总结出经验/坑/规律 | 改用 `abs_note` text + tags | sources/ |
115
98
 
99
+ **选错落点会被闸门拒或被 lint 报。四个落点的分别:**
100
+ - `note` = **断点**,一句话**覆盖式**(旧值就该被替换),上看板。
101
+ - `memo` = **过程随手记**,可多句**追加式**(旧值不能丢),不上看板。
102
+ - `abs_note` = 提炼过的**经验/坑**,一句话结论 → `sources/` 待提炼,不上看板。
103
+ - `abs log` = 一轮干完的**成果流水**行(带 kind),不上看板。
104
+
105
+ 过程随手记一律走 `memo`,别塞 `todo.md`(Todo 区有**白名单闸门**:只收任务行、断点行、空行、标题,其余拒写报错)。
106
+
107
+ ```bash
108
+ abs todo memo <id> "第一句:根因是判据极性。第二句:改成白名单就不漏了。" # 记(可多句)
109
+ abs todo memo <id> # 查看
110
+ ```
111
+ 任务 `done` 后笔记文件留在 `sessions/`(随当日归档)。
112
+
116
113
  **硬规则(开工前触发器,别被状态表漏掉):**
117
114
 
118
115
  1. **开工前先登记** —— 认领任何要动文件/跑命令的任务,**第一步是 `abs_task` action=start**,然后才读第一个文件。名字想不到就先起粗糙的(`fix-hook-nudge`)—— 名字可事后改,**未登记的开工补不回来**。
119
- - **登记只能由你主动做**(hook 自动登记已在 v1.15.2 移除 —— 试过两版,用户实报
120
- "卡顿 + 看板冒出大量 `auto-TBD-xxx 待命名`":每轮 spawn 进程 + 占位直接写看板)。
121
- 结论:hook 只会"看到动过文件",不知道"这是件什么事";名字必须由读懂上下文的你起。
122
- - 名字要像 `fix-skill-trigger` / `fix-opencode-v2-plugin` 那样说清**在干什么**,
123
- 不是抄用户的原话(实测抄出来的是 `我已经重启测试一下` 这种开场白)。
124
- - 需要改已有任务的 id:`abs todo rename <旧id> --note "<新id>"`。
125
- 2. **一段活儿干完立刻 done** —— 不是等整个需求收尾。宁可拆成 5 条小的,别攒成 1 条大的。
126
- 3. **动手超过两三轮还没登记 = 已在失控路上** —— 立刻补 `start`。
116
+ - **登记只能由你主动做**(hook 看不到"这是件什么事",名字必须由读懂上下文的你起);别抄用户原话(「我已经重启测试一下」这种开场白不是任务名)。
117
+ - 改已有任务的 id:`abs todo rename <旧id> --note "<新id>"`。
118
+ 2. **一段活儿干完立刻 done** —— 不等整个需求收尾。宁可拆成 5 条小的,别攒 1 条大的。
119
+ 3. **超过两三轮还没登记 = 已在失控路上** —— 立刻补 `start`。
127
120
 
128
121
  **经验刚冒出来就落**:`abs_note` 暂存 `sources/`(幂等去重),宁少勿滥。
129
122
  **跨会话任务只用 abs todo,别用宿主原生 todo**(TodoWrite / todo-list / `/list`)—— 那些是会话内临时,不写 `.brain/todo.md`,下会话接不上。
@@ -133,25 +126,24 @@ abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs t
133
126
  用户**明确说要收尾/结束/切别的事**时按下面走(不是每个词都触发,见开头总则):
134
127
 
135
128
  1. **读 todo** → `abs load`,看 Todo 还有哪些没完成。
136
- 2. **对账** → 漏登记的 `abs todo done <id>`;做一半补 `abs todo note <id> --note 断点`;卡住的 `abs todo state <id> --note 滞留中`。别让干完的事还留在 Todo。
129
+ 2. **对账** → 按上节「进行中」的四个落点补记:漏登记的 `done`、做一半的补 `note` 断点、卡住的转 `滞留中`。别让干完的事留在 Todo。
137
130
  3. **沉淀经验(该沉淀才沉淀)** → 值得记的坑/可复用技巧 → `abs note` 暂存;值得深提炼的按 Teardown 走。
138
- 4. **判教训够不够格进 Rules(别跳过)** → 是否「违反会丢数据/静默失效/白干活」级?
131
+ 4. **判教训够不够格进 Rules** → 是否「违反会丢数据/静默失效/白干活」级?够格就提议(Rules 是唯一每次 load 全量送达的通道,差这一步下次就不送达):
139
132
  - **够格 → 提议,不直写**:输出一行 `[Rules 提议] <一句话>` 问用户要不要加,确认后才 `abs rule add "<一句话>"`(短句,不带链接/解释)。
140
133
  - 不够格 → 不提,普通经验留在概念页。
141
- **为什么单列**:Rules 是唯一每次 load 全量送达的通道,概念页只在关键词命中时出现;够格不加 = 下次不送达。
142
- 5. **更新 index/log/todo** → 新页同步进 index;`abs log "完成 X:..."` 记一行成果;跑 `abs lint` 确认自洽。
134
+ 5. **更新 index/log/todo** → 新页同步进 index;`abs log "完成 X:..."` 记一行成果;`abs lint` 确认自洽。
143
135
 
144
- **完成标准**:看板反映真实状态(Done 无滞留半成品)、该沉淀已落、够格的教训已提议进 Rules、index/log/todo 与事实一致。
136
+ **完成标准**:看板反映真实状态(Done 无滞留半成品)、该沉淀已落、够格教训已提议进 Rules、index/log/todo 与事实一致。
145
137
 
146
- Stop 时 hook 把「未完成任务 + 断点」快照进 `~/.abs/log/wrapup.log`,下会话 `abs load` 自动把滞留顶到顶部。**不主动往对话里插收尾提醒** —— 插一个 turn 就是打断(挂钩只在 `~/.abs/log/hooks.log` 留痕)。
138
+ Stop 时 hook 把「未完成 + 断点」快照进 `~/.abs/log/wrapup.log`,下会话 `abs load` 自动把滞留顶到顶部。**不往对话里插收尾提醒** —— 插 turn 就是打断。
147
139
 
148
140
  ## 收尾:Teardown Sync(深提炼,工具不替判断)
149
141
 
150
- 任务告一段落/结束前,把**真实发生**写回图谱。只写做过/跑过/测过的事实,禁止脑补。按序:
142
+ 任务告一段落/结束前,把**真实发生**写回图谱,只写做过/跑过/测过的事实。按序:
151
143
 
152
- 1. **暂存线索**:`abs note` 记做了什么、改哪些文件、验证命令。
153
- 2. **抽规律**:值得留的 → `abs concept <slug> --title "…"` 建页(自动带四段骨架)再填内容。四段别缺,尤其末尾「验证」段(`abs lint` 报 NO-TAIL)。**判断仍归你**:值不值得留、新建还是并入已有页;核实过的把 `status` 改 `active`。
154
- 3. **查推翻**:旧页有被本次推翻的说法 → `abs supersede <旧页> --by <新页>`(别删页)。
144
+ 1. **暂存线索**:`abs note` 记做了什么、改了哪些文件、验证命令。
145
+ 2. **抽规律**:`abs concept <slug> --title "…"` 建页(自动带四段骨架)再填内容;四段别缺,尤其末尾「验证」段(`abs lint` 报 NO-TAIL)。判断仍归你:值不值得留、新建还是并入;核实过的改 `status` 为 `active`。
146
+ 3. **查推翻**:旧页有被推翻的说法 → `abs supersede <旧页> --by <新页>`(别删页)。
155
147
  4. **沉淀实体 / 综合**:重要未记录的事物 → `entities/<TitleCase>.md`;选型/取舍 → `syntheses/`。
156
148
  5. **收拢 sources**:提炼成规律的删 source,**同步清指向它的引用**(防死链)。
157
149
  6. **修 index + 记 log**:新页同步 index;过 Rules 门槛的规律加一行到 `## Rules`。
@@ -161,11 +153,9 @@ Stop 时 hook 把「未完成任务 + 断点」快照进 `~/.abs/log/wrapup.log`
161
153
 
162
154
  统一 frontmatter:`tags / author / updated / status`。`tags` 首标签 ∈ `entity|concept|source|synthesis|session-log`;`author` 与 `entities/<name>.md` 同名。
163
155
 
164
- **`status` 三个值(别写别的)**:`active` 当前有效(缺字段默认就是它,正常展示);`draft` 待核实(`abs note` 默认落这个,展示但标 `[draft 未核实]`);`superseded` 已被推翻,别再依据(`query` 默认隐藏)。
165
-
166
- 推翻一条经验用 `abs supersede <页名> --by <新页>`(改 `status` 为 `superseded` 并写 `superseded-by`;不写 `--by` = 单纯弃用)。历史必须留;核实后仍有效就把 `status` 改回 `active`。`--by` 指向不存在的页会被拒。
156
+ **`status` 三个值(别写别的)**:`active` 当前有效(缺字段默认就是它);`draft` 待核实(`abs note` 默认落这个,展示标 `[draft 未核实]`);`superseded` 已被推翻,别再依据(`query` 默认隐藏)。推翻用 `abs supersede <页名> --by <新页>`(写 `superseded-by`;不写 `--by` = 单纯弃用;指向不存在的页会被拒);历史必须留,核实后仍有效就改回 `active`。
167
157
 
168
- - **每页必须有 `## 关联连接`**,用 `[[页面名]]` 链相关页,严禁孤岛页。链路解释写在 `—` 后面(`[[hook-throttle-alignment]] — 节流判据要对齐「真收尾」`),只写链点等于没链。
158
+ - **每页必须有 `## 关联连接`**,用 `[[页面名]]` 链相关页,严禁孤岛;解释写在 `—` 后(`[[x]] — 节流判据要对齐真收尾`),只写链点等于没链。
169
159
  - **命名即链接**:`[[Docker]]` → `entities/Docker.md`;`[[docker-prisma-429]]` → `concepts/`。不建别名层。
170
160
  - **知识冲突**:不静默覆盖,加 `## 知识冲突` 两版都留、标来源时间,交人工裁决。
171
161
  - 概念页骨架:`触发场景 / ❌表现(贴报错) / 🛠解法(根因+修复+验证命令) / 关联连接`。
@@ -173,21 +163,21 @@ Stop 时 hook 把「未完成任务 + 断点」快照进 `~/.abs/log/wrapup.log`
173
163
  ## 维护:query / lint
174
164
 
175
165
  - `abs query <词>` → 读命中页 → 答用 `[[页面名]]` 标来源。**代码问题(符号在哪/谁调用)不在 .brain,直接读源码**;已推翻经验默认不返回(`--all` 可看)。
176
- - `abs lint` 报错码:`NO-INBOUND` 有出边无人链接 / `SOURCE-UNDISTILLED` source 超 7 天未链 concept / `SUPERSEDED-DANGLING` superseded-by 页不存在 / `DRAFT-STALE` 停在 draft / `NO-TAIL` 缺「做完怎么确认」段。
166
+ - `abs lint` 报错码(`NO-INBOUND`/`SOURCE-UNDISTILLED`/`SUPERSEDED-DANGLING`/`DRAFT-STALE`/`NO-TAIL`)跑一次即见。
177
167
 
178
168
  ## ⛔ 禁止做
179
169
 
180
170
  1. **禁止手工建骨架 / 手工登记任务** —— 用 `abs init` / `abs todo add`。
181
- 2. **禁止由关键词直接触发动作**("简短""收尾"等词出现在定规则的句子里时只回应,不执行)—— 拿不准先问。
182
- 3. **禁止通读 `.brain/`** —— 要状态用 `abs load`,要主题用 `abs query`,要单页用 `abs resolve` 后只读那页。
183
- 4. **禁止删知识页来表达"已失效"** —— 用 `abs supersede <旧页> --by <新页>`。
184
- 5. **禁止归档单独建文件 / 一天拆多个快照 / 手工搬大文件** —— 用 `abs todo archive`;一天只有一个 `log-<日期>.md`;大文件进 `sources/` 或摘出规律进 `concepts/`。
171
+ 2. **禁止由关键词直接触发动作**("简短""收尾"出现在定规则的句子里时只回应)—— 拿不准先问。
172
+ 3. **禁止通读 `.brain/`** —— 要状态用 `abs load`、要主题用 `abs query`、要单页用 `abs resolve` 后只读那页。
173
+ 4. **禁止删知识页表达"已失效"** —— 用 `abs supersede <旧页> --by <新页>`。
174
+ 5. **禁止归档单独建文件 / 一天拆多个快照 / 手工搬大文件** —— 用 `abs todo archive`;一天只有一个 `log-<日期>.md`;大文件进 `sources/` 或摘规律进 `concepts/`。
185
175
  6. **禁止编造没跑过的验证结论** —— 只写做过/跑过/测过的事实;不清楚就写"未定位"。
186
176
  7. **禁止直写 Rules** —— 够格的先输出 `[Rules 提议]` 问用户,确认后才 `abs rule add`。
187
177
  8. **禁止用宿主原生 todo 承载跨会话任务** —— 用 `abs_task`,原生 todo 只记本会话临时拆解。
188
178
 
189
179
  ## 自我约束
190
180
 
191
- - 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外)。只写真实发生的事实,遵守容量纪律。
181
+ - 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外);只写真实发生的事实,守容量纪律。
192
182
  - 双链/frontmatter/index 必须自洽 —— 坏链 = 掰断接力棒。
193
183
 
package/src/lint.js CHANGED
@@ -5,6 +5,7 @@ import { join } from 'node:path';
5
5
  import { requireBrain, brainPath, BRAIN_DIR } from './index.js';
6
6
  import { readRules, idOfPage, statusOfPage, supersededByOf, clip, RULE_BODY_MAX } from './store.js';
7
7
  import { doneKindOf, doneDateOf, checkFileShape, TODO_MAX_LINES, LOG_MAX_LINES, INDEX_MAX_LINES } from './todo.js';
8
+ import { isAllowedTodoLine, todoSectionOf } from './todo/common.js';
8
9
 
9
10
  // ---------- 图谱遍历 ----------
10
11
 
@@ -135,6 +136,13 @@ async function checkRootFiles(vault) {
135
136
  if (file !== 'todo.md') continue;
136
137
  for (let i = 0; i < lines.length; i++) {
137
138
  const l = lines[i];
139
+ // 裸文本行(与闸门 isAllowedTodoLine 同源,2026-10-11):Todo 区只能有任务行/断点/空行/标题。
140
+ if (todoSectionOf(lines, i) === 'Todo' && !isAllowedTodoLine(l)) {
141
+ issues.push(
142
+ `TODO-STRAY-PROSE: ${BRAIN_DIR}/${file}:${i + 1} (Todo 区里的裸文本,不是任务行也不是断点;` +
143
+ `总结/报告/清单请分流:流水 → abs log,经验 → abs note,长设计 → sources/)`,
144
+ );
145
+ }
138
146
  if (!l.trimStart().startsWith('↳ 断点:')) continue;
139
147
  const bp = l.trimStart().slice('↳ 断点:'.length).trim();
140
148
  if (bp.length > LINT_BREAKPOINT_MAX) {
@@ -203,7 +211,12 @@ function checkPage(pg, names, linkedNames, inbound) {
203
211
  if (pg.dir === 'concepts' && !hasTail(pg.body)) {
204
212
  issues.push(`NO-TAIL: ${pg.rel}(无「做完怎么确认」;尾巴写清跑什么/看什么/按什么判据,别只有头)`);
205
213
  }
206
- if (pg.dir !== 'sources' && !pg.indexed) {
214
+ // 「任务随手记」(abs todo memo)不登记 index:它是**某任务的过程草稿**,
215
+ // 数量随任务数增长且生命周期短(任务完就没人看),登记会把 index 冲垮 ——
216
+ // 而 index 的契约是「可扫描的图谱导航」。与 sources/ 同理豁免(都是「暂存待处理」)。
217
+ // 判据用 tag 而非文件名:文件名规则将来可能调,tag 是语义标记。
218
+ const isTaskMemo = pg.dir === 'sessions' && /task-memo/.test(pg.frontmatter);
219
+ if (pg.dir !== 'sources' && !isTaskMemo && !pg.indexed) {
207
220
  issues.push(`INDEX-MISSING: ${pg.rel} not listed as [[${pg.slug}]] in index.md`);
208
221
  }
209
222
  return issues;
package/src/store.js CHANGED
@@ -4,7 +4,7 @@ import { promises as fs } from 'node:fs';
4
4
  import { join, resolve, dirname } from 'node:path';
5
5
  import { requireBrain, brainPath, absLogDir } from './index.js';
6
6
  import { requireUser, atTag, getUser, placeholderWarn } from './userconfig.js';
7
- import { stripStateMark, ensureStateMark, stateOfTaskLine, TODO_MAX_LINES, upsertTask, boardText, readTodo, todoTemplate, today, localStamp, setBreakpoint, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText, upsertArchiveSection, DONE_KINDS, withDoneKind, collapseDone, SEC, rebuildStructure, enforceBrainFormat, sessionOf } from './todo.js';
7
+ import { stripStateMark, ensureStateMark, stateOfTaskLine, TODO_MAX_LINES, upsertTask, boardText, readTodo, todoTemplate, today, localStamp, setBreakpoint, setMemo, readMemo, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText, upsertArchiveSection, DONE_KINDS, withDoneKind, collapseDone, SEC, rebuildStructure, enforceBrainFormat, sessionOf } from './todo.js';
8
8
  import { setFormatGate, editFile, SKIP } from './lock.js';
9
9
  import { appendWrapup, strandedFor } from './wrapup.js';
10
10
  import { keywords, recentFiles, topicStrength } from './relevant.js';
@@ -63,7 +63,7 @@ async function createSkeleton(root, brain) {
63
63
  // 中间 init/load 都不提,根本不知道该设。
64
64
  const who = await getUser();
65
65
  const hint = who ? '' : `\n ⚠ 尚未设置使用者姓名,写操作(todo add/log/note)会先报错。\n 请先设置: abs config set user <你的名字>`;
66
- return `✓ 已建图谱 ${brain}\n (模板见 SKILL.md「文件标准」)${hint}`;
66
+ return `✓ 已建图谱 ${brain}\n (模板见 SKILL.md「.brain/ 怎么组织」)${hint}`;
67
67
  }
68
68
 
69
69
  /** 结构体检:6 目录 + 3 文件逐一核对。ok=false 时 problems 列出缺失项。 */
@@ -929,6 +929,16 @@ export async function cmdTask({ dir, action, id, section, note, as, session }) {
929
929
  const r = await setBreakpoint(root, { id, text: note });
930
930
  return r.msg;
931
931
  }
932
+ if (action === 'memo') {
933
+ // 任务随手记:多句/不定长/可追加,不占看板。无 note 时 = 查看已有笔记。
934
+ if (!id) return '用法: abs todo memo <id> "随手记(可多句)" | abs todo memo <id>(查看)';
935
+ if (!note) {
936
+ const body = await readMemo(root, id);
937
+ return body || `(${id} 没有随手记)`;
938
+ }
939
+ const r = await setMemo(root, { id, text: note });
940
+ return r.msg;
941
+ }
932
942
  if (action === 'done') {
933
943
  // 找到匹配 id 的行,勾选并归位 Done(简化:若行在某 section 则标记完成)
934
944
  // --as 结语:落地(默认) / 否决 / 仅方案 —— 让 [x] 可被信任(见 todo.js DONE_KINDS)
@@ -52,6 +52,17 @@ export function sigOfLine(l) {
52
52
  return String(l).replace(/\s+/g, ' ').trim().slice(0, 120);
53
53
  }
54
54
 
55
+ /** 一行落在 todo.md 的哪个分区(`## Todo` / `## Done`)。
56
+ * 返回 'Todo' | 'Done' | null。用于「按区判」—— Done/Archived 区的行不该按
57
+ * Todo 区的白名单判(日期分组、归档链接都是合法内容)。 */
58
+ export function todoSectionOf(lines, i) {
59
+ for (let k = i; k >= 0; k--) {
60
+ const m = String(lines[k]).match(/^##\s+(\S.*)$/);
61
+ if (m) return m[1].trim();
62
+ }
63
+ return null;
64
+ }
65
+
55
66
  /** 把一份文本里所有「违规行」的签名收成集合(供存量/新增比对)。
56
67
  * 只有三个根文件用得上;非目标文件直接返回空集。
57
68
  * ★ 空行特殊:签名取「上下两行拼接」不可靠 —— 插入一行就会让空行的
@@ -96,6 +107,24 @@ export function isChildLine(l) {
96
107
  return /^\s*↳/.test(l);
97
108
  }
98
109
 
110
+ /** Todo 分区里「合法行」以外的东西 = 裸文本(总结/报告/清单)。
111
+ * 为什么补这个判据(2026-10-11,同一个坑第三次复发):
112
+ * 闸门原来只认 `- [ ]` 任务行与 `↳ 断点:` 行,**其余行直接跳过**(无 else 分支)。
113
+ * 于是把总结写成「紧跟任务行的裸文本行」就三层全部放行:闸门不看、形状规则不管、
114
+ * lint 只查断点行 —— 实测 .agents/.brain/todo.md 被塞进 10 行实施报告而 lint 报 0 问题。
115
+ * 前面的修法都挂在「行的形态」上(先修超长断点、再修多句断点),换个形态就绕过。
116
+ * 改成白名单:Todo 区里除了任务行/断点/空行/标题,别的都是非法。
117
+ * 只在 `## Todo` 区生效 —— Done/Archived 区有日期分组与归档链接,判据不同。
118
+ * 返回 false 才叫非法。 */
119
+ export function isAllowedTodoLine(l) {
120
+ const t = l.trim();
121
+ if (!t) return true; // 空行
122
+ if (/^#{1,3}\s/.test(t)) return true; // `## Todo` / `### …` 标题
123
+ if (isTaskLine(l)) return true; // 任务行
124
+ if (isChildLine(l)) return true; // ↳ 断点附属行
125
+ return false;
126
+ }
127
+
99
128
  /** 把 Done 区文本(bodyLines,不含 `## Done` 标题)解析成任务单元 [{ date, lines:[main,...children] }]。
100
129
  * 剥 `### 日期` 分组标题与空行;任务行下紧跟的 ↳ 行并入该单元。 */
101
130
 
package/src/todo/tasks.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // src/todo/tasks.js — 任务读写入口:读、加、改状态、断点、看板。
2
2
 
3
3
  import { promises as fs } from 'node:fs';
4
+ import { join } from 'node:path';
4
5
  import { brainPath } from '../index.js';
5
6
  import { editFile, SKIP } from '../lock.js';
6
- import { today, extractId, extractNote, extractAuthor, isLegacyAuthorTag, claimOf } from './common.js';
7
+ import { today, localStamp, extractId, extractNote, extractAuthor, isLegacyAuthorTag, claimOf } from './common.js';
7
8
  import { todoTemplate, normalizeTodo, stateOfTaskLine, stripStateMark } from './structure.js';
8
9
  import { TASK_STATES } from './spec.js';
9
10
  import { collapseDone, groupDoneSection } from './archive.js';
@@ -154,6 +155,76 @@ export async function setBreakpoint(brainRoot, { id, text }) {
154
155
  : { ok: true, msg: `✓ 断点已落 → ${id}\n ${bp.trim()}` };
155
156
  }
156
157
 
158
+ // ---------- 任务随手记(memo)----------
159
+ /** 任务进行中的随手记:多句、不定长、可追加,**不占看板**。
160
+ *
161
+ * 为何需要它(2026-10-11,用户定):断点只该一句话(用户明确要求保持),
162
+ * 而 `abs log` 要起 kind、进 log.md,对「随手记一句」太重;`abs note` 只管
163
+ * 经验/坑且落 sources/。于是任务干到一半想记「现在做到哪、发现了什么」时
164
+ * **没有轻档位**,AI 就顺手塞进 todo.md —— 实测 .agents/.brain/todo.md 被塞进
165
+ * 10 行实施报告,而清理时发现「原内容只存在 todo.md,删任务时差点永久丢失」。
166
+ * 堵(白名单闸门)只治标:不给出路,下次还会换个写法再塞。本命令就是那条出路。
167
+ *
168
+ * 落点选 sessions/(不是新目录):那里已有目录遍历/frontmatter/归档机制,
169
+ * lint 的豁免判据也是现成的(两条契约都已核实):
170
+ * · `archive` tag → SESSIONS-SPLIT / SESSIONS-NAMING 豁免(它们只算无 archive 的)
171
+ * · `log-` 前缀 → ORPHAN-PAGE 豁免(sessions 里非 log- 开头要求有链接)
172
+ * 所以文件名取 `log-<日期>-<id>.md`,两个条件同时满足,**不改 lint 任何契约**。
173
+ *
174
+ * 追加式(不是覆盖):每次调用多一条带时间戳的条目,与断点的语义有意不同 ——
175
+ * 断点是「现在到哪了」(旧值就该被替换),memo 是「过程流水」(旧值不能丢)。 */
176
+ export async function setMemo(brainRoot, { id, text, at }) {
177
+ const stamp = at || localStamp();
178
+ const dir = brainPath(brainRoot, 'sessions');
179
+ const file = join(dir, `log-${today()}-${id}.md`);
180
+ const rel = `sessions/log-${today()}-${id}.md`;
181
+ const line = `- ${stamp} ${text}`;
182
+ const head = [
183
+ '---',
184
+ 'tags: [archive, task-memo]', // archive → 豁免一天一文件;task-memo → 标明用途
185
+ `id: log-${today()}-${id}`,
186
+ `updated: ${today()}`,
187
+ '---',
188
+ '',
189
+ `# 任务随手记:${id}`,
190
+ '',
191
+ `━━ ${id} 的过程笔记(不上看板;任务结束后随 sessions 留存)`,
192
+ '',
193
+ ].join('\n');
194
+ await fs.mkdir(dir, { recursive: true });
195
+ // 全程走 editFile 的锁:同一任务的并发 memo 不能丢条目(append 是 read-modify-write)。
196
+ // 初次创建也走它 —— editFile 对「文件尚不存在」已经处理(current=null),
197
+ // 自己再写一份 tmp+rename 分支反而绕过锁,首次并发两个 memo 会互相覆盖。
198
+ await editFile(file, (cur) => {
199
+ if (cur === null) return { text: `${head}${line}\n` };
200
+ const t = cur.replace(/\n+$/, '');
201
+ const next = /^updated:\s*.+$/m.test(t) ? t.replace(/^updated:\s*.+$/m, `updated: ${today()}`) : t;
202
+ return { text: `${next}\n${line}\n` };
203
+ });
204
+ return { ok: true, msg: `✓ 随手记 → ${rel}\n ${line}` };
205
+ }
206
+
207
+ /** 读回某任务的随手记(无则空)。供 CLI/MCP 查看与 done 时提醒。
208
+ * 按**完整文件名后缀**匹配而不是 id 后缀:`endsWith('-${id}.md')` 会让
209
+ * `t-1` 误命中 `task-t-1`(同族坑见 idOfTaskLine 的全等比对注释)。
210
+ * 文件名形如 `log-<日期>-<id>.md`,所以匹配 `-<id>.md` 时必须确认 id 是**最后一段**。 */
211
+ export async function readMemo(brainRoot, id) {
212
+ const dir = brainPath(brainRoot, 'sessions');
213
+ const files = await fs.readdir(dir).catch(() => []);
214
+ const want = String(id).replace(/\u200b/g, '');
215
+ const hit = files.filter((f) => {
216
+ const m = f.match(/^log-\d{4}-\d{2}-\d{2}-(.+)\.md$/);
217
+ return m && m[1] === want;
218
+ }).sort();
219
+ if (!hit.length) return '';
220
+ const parts = [];
221
+ for (const f of hit) {
222
+ const body = await fs.readFile(join(dir, f), 'utf8').catch(() => '');
223
+ parts.push(body.trim());
224
+ }
225
+ return parts.join('\n\n');
226
+ }
227
+
157
228
  // ---------- 看板输出 ----------
158
229
 
159
230
  export async function boardText(brainRoot, textOverride, { full = false } = {}) {
@@ -1,7 +1,7 @@
1
1
  // src/todo/validate.js — 条目级闸门:形状校验、长度限制、空行检查。
2
2
 
3
3
  import { TODO_SECTIONS, isAllowedTodoSub, INDEX_SECTIONS, ENTRY_SHAPES, indexLinkSectionNeeds, BREAKPOINT_MAX, TASK_LINE_MAX } from './spec.js';
4
- import { sigOfLine, REPORT_MARKERS, sentenceCount, isTaskLine } from './common.js';
4
+ import { sigOfLine, REPORT_MARKERS, sentenceCount, isTaskLine, isAllowedTodoLine, todoSectionOf } from './common.js';
5
5
 
6
6
  export function checkFileShape(file, text, { lintMode = false } = {}) {
7
7
  const issues = [];
@@ -193,6 +193,18 @@ export function assertTodoContent(text, prevSig = null) {
193
193
  }
194
194
  continue;
195
195
  }
196
+ // 裸文本行(2026-10-11):Todo 区里既不是任务行也不是断点的行 = 总结/报告/清单。
197
+ // 前面的修法都挂在「行的形态」上(超长断点、多句断点),把总结写成裸文本行就全部绕过
198
+ // ——同一个坑第三次复发。改成白名单后,换形态也落回这里。
199
+ // 按区判:Todo 区才管;Done/Archived 区有日期分组与归档链接,判据不同。
200
+ // 存量行(prevSig)放行,交给 lint 报 —— 否则老库直接被锁死写不进去。
201
+ if (todoSectionOf(lines, i) === 'Todo' && !isAllowedTodoLine(l) && !isLegacy(l)) {
202
+ throw new Error(
203
+ `✗ todo 的 Todo 区不允许裸文本(第 ${lineNo} 行)—— 这里只放任务行(- [ ] …)与断点行(↳ 断点: …)。\n` +
204
+ ` 「总结/实施报告/清单」请分流:流水与测试数据 → abs log;经验与坑 → abs note;长设计与方案 → .brain/sources/ 页。\n` +
205
+ ` 原文开头: ${l.trim().slice(0, 60)}...`,
206
+ );
207
+ }
196
208
  }
197
209
  return true;
198
210
  }
package/src/todo.js CHANGED
@@ -7,4 +7,4 @@ export { LEGACY_SECTION_RENAMES, TODO_SECTIONS, TODO_SUBSECTIONS, isAllowedTodoS
7
7
  export { todoTemplate, rebuildStructure, dropRetiredSections, enforceBrainFormat, normalizeTodo, ensureStateMark, stateOfTaskLine, stripStateMark } from './todo/structure.js';
8
8
  export { checkFileShape, violationSignatures, assertNoStrayBlank, assertTodoContent } from './todo/validate.js';
9
9
  export { doneDateOf, doneKindOf, withDoneKind, collapseDone, archiveDoneInText, renderArchiveBody, upsertArchiveSection, groupDoneSection, insertDoneGrouped } from './todo/archive.js';
10
- export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, boardText } from './todo/tasks.js';
10
+ export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setMemo, readMemo, boardText } from './todo/tasks.js';