@fanchao8609/agent_brain_sync 1.19.6 → 1.20.1

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.
@@ -5,179 +5,184 @@ description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。
5
5
 
6
6
  # abs — 跨会话记忆 (agent-brain-sync)
7
7
 
8
- ## 最高优先:触发总则(凌驾本文所有流程)
8
+ ## 触发器
9
9
 
10
- **关键词不是触发器,意图才是。**句子里出现 abs 词(收尾/todo/沉淀/load/log/note)不等于要执行:谈论、提问、吐槽、定规则**只回应**,明确让我做事才执行。
10
+ **关键词不是触发器,意图才是。**句子里出现 todo / 沉淀 / load / note 这些词,若是谈论、提问、定规则,只回应不执行。
11
11
 
12
- **例外(唯一):用户直接输入 `abs`** —— 单独一个词、或 `abs` 后跟命令/意图(如 `abs 开工`、`abs 沉淀`)。这是明确的"我要用 abs",**直接按 abs 流程执行**(无子命令时默认走开工续接)。
13
-
14
- - 拿不准**先问**。规则变更("以后简短点")写入本文,不执行动作;输出简短(收尾/提示/todo 一两句)。
12
+ 唯一例外:用户直接输入 `abs`(单独一个词,或 `abs` + 命令,如 `abs 开工`)—— 这是明确要用 abs,直接执行;无子命令时走开工续接。
15
13
 
16
14
  ## 命令速查
17
15
 
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**。
16
+ 完整帮助 `abs help`。读写优先走 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。
19
17
 
20
18
  ```bash
21
- abs load # 开机读状态;abs query <词> 检索;abs resolve <页名> 反查路径
22
- abs todo add <id> --note "做什么" # 登记(note/memo/state/done 见下「进行中」);abs log "完成 X" 记成果
23
- abs note "经验" [--tags a,b] # 暂存经验;abs concept <slug> --title "…" 建概念页骨架
24
- abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs init 建图谱;abs todo archive 归档
19
+ abs load # 开机读状态
20
+ abs query <词> # 检索;abs resolve <页名> 反查路径
21
+ abs todo add <id> --note "做什么" # 登记;note/memo/state/done 见「进行中」
22
+ abs note "经验" [--tags a,b] # 暂存经验
23
+ abs concept <slug> --title "…" # 建概念页骨架
24
+ abs rule add "一句话" # 加 Rules;abs lint 体检;abs init 建图谱
25
25
  ```
26
26
 
27
- ## 触发总入口(每次命中技能,第一步先走这里)
27
+ ## 文档精简(硬规则,写 todo / 知识库文档必须遵守)
28
+
29
+ 1. 所有内容必须服务于主题目标,与主题目标无关的直接去掉。
30
+ 2. 不允许盲目总结,没有经过验证的经验和总结,直接去掉。
31
+ 3. 文档永远保持秩序,提高可阅读性,层次分明。
32
+ 4. 清爽的文字风格,禁止反转反问、猜测。
33
+ 5. 加快运行速度,提高运行效率。
34
+ 6. 拒绝重复,其他文档存在的方法优先引用,拒绝反反复复写。
35
+ 7. 禁令内容不要唠叨,直接了当。
36
+ 8. 不要过度揣测:用户没说的不要替他补,给了要求就做要求内的。
37
+ 9. 说中文,说人话;表达通畅、有前因后果,不堆抽象名词、不自造新词。已经说过、已经确定的事不重复。
38
+
39
+ 写入 `.brain/` 的 todo 行、log 行、知识页、sessions 快照,生成时按上面九条自检。
28
40
 
29
- 技能被触发时第一步走这条链,再看用户要什么:
41
+ ## 入口:先找图谱再分流
30
42
 
31
- 1. **找图谱**:只看**当前目录**是否含 `.brain/`,必须 cd 到项目根。不许向上搜索(会命中 `~/.brain` 的无关图谱,把项目静默挂错)。
32
- 2. **没有图谱?按意图定建档**:意图是**沉淀**或**即将开工的长期任务**("帮我做 X / 开工 / 修 X")→ **先 `abs init`**;**闲聊 / 一次性问句 / 明说不要建档 / 只读目录** → 不建档。
33
- 3. **有/刚建好?分流**:
43
+ 1. **找图谱**:只看**当前目录**是否含 `.brain/`,先 cd 到项目根。不向上搜索(会命中 `~/.brain`,把项目挂错)。
44
+ 2. **没有图谱**:意图是沉淀或即将开工的长期任务("帮我做 X / 开工 / 修 X")→ 先 `abs init`;闲聊、一次性问句、明说不要建档、只读目录 → 不建档。
45
+ 3. **分流**:
34
46
 
35
47
  | 用户意图 | 走哪 |
36
48
  |---|---|
37
- | 总结经验 / 结束了 / "把这次记下来" | 收尾循环(沉淀) |
38
- | 查询坑 / "我上次怎么解决 X" | `abs query` 检索 |
49
+ | 总结经验 / 结束了 / "把这次记下来" | 收尾循环 |
50
+ | 查询坑 / "我上次怎么解决 X" | `abs query` |
39
51
  | 体检图谱 | `abs lint` |
40
- | 开新任务 / 继续开发 / "帮我做 X" | 开场 Init Sync(续接);新需求先走受理协议 |
41
- | 报 bug / 要求加功能 | **受理协议**:先方案 → 再登记 → 问开工 |
42
- | 意图不明 / 默认 | **续 todo**:读未完成项开工续做 |
52
+ | 开新任务 / 继续开发 | 开场 Init Sync |
53
+ | 报 bug / 要求加功能 | 受理协议 |
54
+ | 意图不明 | 续 todo |
43
55
 
44
- **主心骨**:意图不明且项目有 `.brain/` 时,默认**续 todo**。
56
+ 意图不明且项目有 `.brain/` 时,默认续 todo。
45
57
 
46
- ## 新需求受理协议(bug / 新功能:先方案 → 再登记 → 问开工)
58
+ ## 新需求受理协议(先方案 → 再登记 → 问开工)
47
59
 
48
- 用户报 bug 或要求加功能时先别动代码,三步:
60
+ 用户报 bug 或要求加功能时先别动代码:
49
61
 
50
- 1. **总结方案**(一屏内,给用户过目):准确复述需求(不确定就写明假设);bug 给根因、功能给做法;要改哪些文件、怎么验证。
51
- 2. **登记 todo**:`abs todo add <id> --note "<需求+关键约束>"`;根因/取舍用 `abs todo note <id>` 落到断点。
52
- 3. **问是否开工**:明确问一句,**等确认再改代码**。
62
+ 1. **总结方案**(一屏内):准确复述需求(不确定就写明假设);bug 给根因,功能给做法;要改哪些文件、怎么验证。
63
+ 2. **登记 todo**:`abs todo add <id> --note "<需求+关键约束>"`;根因/取舍用 `abs todo note <id>` 追加成子项。
64
+ 3. **问是否开工**:明确问一句,等确认再改代码。
53
65
 
54
- **例外(可直接开工,回复里须说明援引哪一条)**:用户已说"开工/直接做/修它";一行级修字、纯查询、纯收尾沉淀;同一需求已登记且用户确认过。
55
-
56
- `.brain/` 放项目根,一个项目一份。`abs status` 显示当前定位。
66
+ **例外(可直接开工,回复里说明援引哪一条)**:用户已说"开工/直接做/修它";一行级修字、纯查询、纯收尾沉淀;同一需求已登记且用户确认过。
57
67
 
58
68
  ## `.brain/` 怎么组织
59
69
 
70
+ `.brain/` 放项目根,一个项目一份,`abs status` 显示当前定位。
71
+
60
72
  | 位置 | 放什么 | 硬约束 |
61
73
  |---|---|---|
62
- | `index.md` | `## Rules` 铁律 + 各类页清单 | Rules 一句一条不带链接,只有「违反会丢数据/静默失效/白干活」级才进(普通经验进 `concepts/`),>42 字符或含链接/URL 被拒;清单每页一行 `- [[slug]] — 一句话` |
63
- | `log.md` | 成果流水,倒序一行 | 只记成果,不收工具动作流水(那在 `~/.abs/log/`) |
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`**(多主题就多写 `##` 段) |
74
+ | `index.md` | `## Rules` 铁律 + 各类页清单 | Rules 一句一条不带链接,只有「违反会丢数据/静默失效/白干活」级才进,>42 字符或含链接被拒;清单每页一行 `- [[slug]] — 一句话` |
75
+ | `log.md` | 成果流水,倒序一行 | 只记成果,不记工具动作 |
76
+ | `todo.md` | 活看板 | 只有 `## Todo`/`## Done`;Done 必带 `【落地/否决/仅方案】`+`(完成 YYYY-MM-DD)` |
77
+ | `entities/` `concepts/` | 具名事物(`Docker.md`)/ 可复用规律坑(`docker-prisma-429.md`) | 拿不准默认进 `concepts/` |
78
+ | `sources/` `syntheses/` | 经验暂存 / 横向选型取舍 | sources 是暂存,提炼后删 |
79
+ | `sessions/` | 会话快照 + 当日归档段 | **一天只允许一个 `log-<日期>.md`**,多主题写多个 `##` 段 |
68
80
 
69
- `todo.md` 行形态:`- [ ] [进行中] <id> [[认领人]] — 说明 (认领 YYYY-MM-DD)`(标记还有 `[讨论中]`/`[滞留中]`;`[[认领人]]` = 使用者名,非任务名)。`abs todo done <id>` 勾选并归位到 Done 日期组顶部,断点(`↳` 行)随迁;超 3 天**且整天已完成**的组由 `abs wrapup` 迁进当天快照归档段(任一天还有未完成则整天不归档,不拆半天)。
81
+ `todo.md` 行形态:`- [ ] [进行中] <id> [[认领人]] — 说明 (认领 YYYY-MM-DD)`(标记还有 `[讨论中]`/`[滞留中]`;`[[认领人]]` 是使用者名)。`abs todo done <id>` 勾选并归位到 Done 日期组顶部,已勾子项随迁;超 3 天**且整天已完成**的组由 `abs wrapup` 迁进当天快照归档段(任一天还有未完成则整天不归档)。
70
82
 
71
- `sessions/` 快照含正文 + `## 关联连接` + `## 🪝 Next Session Hook`(强制)+ `## 📦 任务归档`(机器写勿手改)。归档唯一动作 `abs todo archive`:已存在快照只替换归档段、段外逐字保留;那天没写过快照则建一个只有归档段的页;同日重复归档在段内追加。
83
+ `sessions/` 快照 = 正文 + `## 关联连接` + `## 🪝 Next Session Hook`(强制)+ `## 📦 任务归档`(机器写勿手改)。归档唯一动作 `abs todo archive`:已有快照只替换归档段,段外逐字保留;那天没有快照就建一个只含归档段的页;同日重复归档在段内追加。
72
84
 
73
85
  ### 容量纪律(写任何页之前过四关)
74
86
 
75
- 不过关就不写或压缩:① **再命中** —— 下会话不知道这条会踩同坑/重做同决定?不会就不存,代码 grep 得到的不记。② **单页上限** —— `entities/concepts/syntheses` 单页 <150 行且 <8KB,超了拆或外链。③ **sources 是暂存** —— 提炼后删/归档并清引用。④ 能用一行链接代替新增整页吗?
87
+ ① **再命中** —— 下会话不知道这条会踩同坑/重做同决定?不会就不存,grep 得到的不记。② **单页上限** —— `entities/concepts/syntheses` 单页 <150 行且 <8KB,超了拆或外链。③ **sources 是暂存** —— 提炼后删并清引用。④ 能用一行链接代替新增整页吗?
76
88
 
77
89
  ## 开场:Init Sync(开工 / 默认续 todo)
78
90
 
79
- 收到第一个核心开发指令**之前**走这条链载入上下文:
80
-
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` 在沙箱处理,**只打印结论**。
84
- 4. **续 todo**:把顶部未完成项当当前任务开做;有明确新任务而 todo 没有 → `abs todo add <id> --note 做什么` 再动工。
91
+ 1. **读状态**:`abs load`(MCP `abs_load`)读 Rules + 图谱计数 + todo + 最近 log,**先看 `## Rules`**。输出已折叠,全量明细用 `abs todo --full` / `abs index`。
92
+ 2. **对账滞留(强制)**:顶部出现 `⏳ 上会话滞留` 说明上会话断了,先收尾再开工 —— 做完的 `abs todo done <id>`;没做完的 `abs todo note <id> --note "接到哪"`。
93
+ 3. **读命中页**:`abs query <词>` → `abs resolve <页名或id>` 拿路径,**只读命中那几页**,绝不通读 `.brain/`;多文件汇总交 `ctx_execute` 在沙箱处理,只打印结论。
94
+ 4. **续 todo**:把顶部未完成项当当前任务开做;有新任务而 todo 没有 → `abs todo add <id> --note 做什么` 再动工。
85
95
 
86
96
  ## 进行中:什么时候动它
87
97
 
88
- **todo 是随改随写的活看板,同 git commit 同反射。**
98
+ todo 是随改随写的活看板,和 git commit 同反射。
89
99
 
90
100
  | 时机 | `abs_task` action | 落到哪 |
91
101
  |---|---|---|
92
102
  | 认领新任务 | `start` + note 做什么 | Todo `[进行中]` |
93
103
  | 只讨论没动手 / 卡住等人等数据 | `state` + note 讨论中\|滞留中 | 原地改标记 |
94
- | 被打断/干到一半 | `note` + 改到哪个文件哪一步 | 原地 ↳断点(**一句话**) |
95
- | 干活中想随手记一段(多句/发现/取舍) | **`memo`** + 原文 | `sessions/log-<日期>-<id>.md`,**不上看板** |
96
- | 子任务做完 | `done` + note 结语 + as 落地\|否决\|仅方案 | Done(别让假【落地】污染看板) |
97
- | 总结出经验/坑/规律 | 改用 `abs_note` text + tags | sources/ |
104
+ | 被打断 / 干到一半 | `note` + 改到哪个文件哪一步 | 任务下追加一条子项(**一句话**) |
105
+ | 干活中随手记多句发现 | `memo` + 原文 | `sessions/` 当日笔记,**不上看板** |
106
+ | 子任务做完 | `done` + note 结语 + as 落地\|否决\|仅方案 | Done |
107
+ | 总结出经验/坑/规律 | `abs_note` text + tags | `sources/` |
108
+
109
+ **四个落点的区别**:
98
110
 
99
- **选错落点会被闸门拒或被 lint 报。四个落点的分别:**
100
- - `note` = **断点**,一句话**覆盖式**(旧值就该被替换),上看板。
101
- - `memo` = **过程随手记**,可多句**追加式**(旧值不能丢),不上看板。
102
- - `abs_note` = 提炼过的**经验/坑**,一句话结论 → `sources/` 待提炼,不上看板。
103
- - `abs log` = 一轮干完的**成果流水**行(带 kind),不上看板。
111
+ - `note` = **子项**,一句话**追加式**(每次多一条,不覆盖),上看板。
112
+ - `memo` = **随手记**,可多句**追加式**,不占看板。
113
+ - `abs_note` = 提炼过的**经验/坑**,一句话结论 → `sources/` 待提炼。
114
+ - `abs log` = 一轮干完的**成果流水**行,不占看板。
104
115
 
105
- 过程随手记一律走 `memo`,别塞 `todo.md`(Todo 区有**白名单闸门**:只收任务行、断点行、空行、标题,其余拒写报错)。
116
+ 过程随手记走 `memo`,别塞 `todo.md`(Todo 区有**白名单闸门**:只收任务行、缩进子项、空行、标题,其余拒写报错)。
106
117
 
107
118
  ```bash
108
119
  abs todo memo <id> "第一句:根因是判据极性。第二句:改成白名单就不漏了。" # 记(可多句)
109
- abs todo memo <id> # 查看
120
+ abs todo memo <id> # 查看
110
121
  ```
111
- 任务 `done` 后笔记文件留在 `sessions/`(随当日归档)。
112
122
 
113
- **硬规则(开工前触发器,别被状态表漏掉):**
123
+ 任务 `done` 后笔记文件留在 `sessions/`,随当日归档。
124
+
125
+ **开工前三条硬规则**:
114
126
 
115
- 1. **开工前先登记** —— 认领任何要动文件/跑命令的任务,**第一步是 `abs_task` action=start**,然后才读第一个文件。名字想不到就先起粗糙的(`fix-hook-nudge`)—— 名字可事后改,**未登记的开工补不回来**。
116
- - **登记只能由你主动做**(hook 看不到"这是件什么事",名字必须由读懂上下文的你起);别抄用户原话(「我已经重启测试一下」这种开场白不是任务名)。
117
- - 改已有任务的 id:`abs todo rename <旧id> --note "<新id>"`。
118
- 2. **一段活儿干完立刻 done** —— 不等整个需求收尾。宁可拆成 5 条小的,别攒 1 条大的。
127
+ 1. **开工前先登记** —— 认领任何要动文件/跑命令的任务,第一步是 `abs_task` action=start,然后才读第一个文件。名字想不到就先起粗糙的(`fix-hook-nudge`):名字可事后改,**未登记的开工补不回来**。
128
+ - 登记只能由你主动做(hook 看不到"这是件什么事");别抄用户原话。
129
+ - 改 id:`abs todo rename <旧id> --note "<新id>"`。
130
+ 2. **一段活儿干完立刻 done** —— 不等整个需求收尾。宁可拆 5 条小的,别攒 1 条大的。
119
131
  3. **超过两三轮还没登记 = 已在失控路上** —— 立刻补 `start`。
120
132
 
121
- **经验刚冒出来就落**:`abs_note` 暂存 `sources/`(幂等去重),宁少勿滥。
122
- **跨会话任务只用 abs todo,别用宿主原生 todo**(TodoWrite / todo-list / `/list`)—— 那些是会话内临时,不写 `.brain/todo.md`,下会话接不上。
133
+ 经验刚冒出来就 `abs_note` 暂存(幂等去重),宁少勿滥。**跨会话任务只用 abs todo**,别用宿主原生 todo(TodoWrite / `/list`),那些不写 `.brain/todo.md`,下会话接不上。
123
134
 
124
135
  ## 收尾循环(用户明确要求收尾时才走)
125
136
 
126
- 用户**明确说要收尾/结束/切别的事**时按下面走(不是每个词都触发,见开头总则):
137
+ 1. **读 todo**:`abs load`,看还有哪些没完成。
138
+ 2. **对账**:按「进行中」四个落点补记 —— 漏登记的 `done`、做一半的补 `note` 子项、卡住的转 `滞留中`。
139
+ 3. **沉淀经验**(值得才沉淀):坑/可复用技巧 → `abs note` 暂存;值得深提炼的走 Teardown。
140
+ 4. **判教训够不够格进 Rules**:是否「违反会丢数据/静默失效/白干活」级?够格就输出一行 `[Rules 提议] <一句话>` 问用户,确认后才 `abs rule add "<一句话>"`;不够格不提。
141
+ 5. **更新 index/log/todo**:新页同步进 index;`abs log "完成 X:..."`;`abs lint` 确认自洽。
127
142
 
128
- 1. **读 todo** → `abs load`,看 Todo 还有哪些没完成。
129
- 2. **对账** → 按上节「进行中」的四个落点补记:漏登记的 `done`、做一半的补 `note` 断点、卡住的转 `滞留中`。别让干完的事留在 Todo。
130
- 3. **沉淀经验(该沉淀才沉淀)** → 值得记的坑/可复用技巧 → `abs note` 暂存;值得深提炼的按 Teardown 走。
131
- 4. **判教训够不够格进 Rules** → 是否「违反会丢数据/静默失效/白干活」级?够格就提议(Rules 是唯一每次 load 全量送达的通道,差这一步下次就不送达):
132
- - **够格 → 提议,不直写**:输出一行 `[Rules 提议] <一句话>` 问用户要不要加,确认后才 `abs rule add "<一句话>"`(短句,不带链接/解释)。
133
- - 不够格 → 不提,普通经验留在概念页。
134
- 5. **更新 index/log/todo** → 新页同步进 index;`abs log "完成 X:..."` 记一行成果;`abs lint` 确认自洽。
143
+ **完成标准**:看板反映真实状态(Done 无滞留半成品)、该沉淀已落、够格教训已提议、index/log/todo 与事实一致。
135
144
 
136
- **完成标准**:看板反映真实状态(Done 无滞留半成品)、该沉淀已落、够格教训已提议进 Rules、index/log/todo 与事实一致。
145
+ Stop 时 hook 把「未完成 + 子项」快照进 `~/.abs/log/wrapup.log`,下会话 `abs load` 自动顶到顶部。不往对话里插收尾提醒。
137
146
 
138
- Stop 时 hook 把「未完成 + 断点」快照进 `~/.abs/log/wrapup.log`,下会话 `abs load` 自动把滞留顶到顶部。**不往对话里插收尾提醒** —— 插 turn 就是打断。
147
+ ## 收尾:Teardown Sync(深提炼)
139
148
 
140
- ## 收尾:Teardown Sync(深提炼,工具不替判断)
141
-
142
- 任务告一段落/结束前,把**真实发生**写回图谱,只写做过/跑过/测过的事实。按序:
149
+ 把**真实发生**的写回图谱,只写做过/跑过/测过的事实。按序:
143
150
 
144
151
  1. **暂存线索**:`abs note` 记做了什么、改了哪些文件、验证命令。
145
- 2. **抽规律**:`abs concept <slug> --title "…"` 建页(自动带四段骨架)再填内容;四段别缺,尤其末尾「验证」段(`abs lint` 报 NO-TAIL)。判断仍归你:值不值得留、新建还是并入;核实过的改 `status` 为 `active`。
146
- 3. **查推翻**:旧页有被推翻的说法 → `abs supersede <旧页> --by <新页>`(别删页)。
147
- 4. **沉淀实体 / 综合**:重要未记录的事物 → `entities/<TitleCase>.md`;选型/取舍 → `syntheses/`。
152
+ 2. **抽规律**:`abs concept <slug> --title "…"` 建页(自动带四段骨架)再填;四段别缺,尤其末尾「验证」段(`abs lint` 报 NO-TAIL)。留不留、新建还是并入由你判断;核实过的 `status` 改 `active`。
153
+ 3. **查推翻**:旧页有被推翻的说法 → `abs supersede <旧页> --by <新页>`,别删页。
154
+ 4. **沉淀实体 / 综合**:重要未记录的事物 → `entities/<TitleCase>.md`;选型取舍 → `syntheses/`。
148
155
  5. **收拢 sources**:提炼成规律的删 source,**同步清指向它的引用**(防死链)。
149
- 6. **修 index + 记 log**:新页同步 index;过 Rules 门槛的规律加一行到 `## Rules`。
150
- 7. **留接力棒**:`sessions/log-YYYY-MM-DD.md` 强制写 `## 🪝 Next Session Hook`。
156
+ 6. **修 index + 记 log**:新页同步 index;过门槛的规律加进 `## Rules`。
157
+ 7. **留接力棒**:`sessions/log-YYYY-MM-DD.md` 写 `## 🪝 Next Session Hook`。
151
158
 
152
159
  ## 知识页格式
153
160
 
154
161
  统一 frontmatter:`tags / author / updated / status`。`tags` 首标签 ∈ `entity|concept|source|synthesis|session-log`;`author` 与 `entities/<name>.md` 同名。
155
162
 
156
- **`status` 三个值(别写别的)**:`active` 当前有效(缺字段默认就是它);`draft` 待核实(`abs note` 默认落这个,展示标 `[draft 未核实]`);`superseded` 已被推翻,别再依据(`query` 默认隐藏)。推翻用 `abs supersede <页名> --by <新页>`(写 `superseded-by`;不写 `--by` = 单纯弃用;指向不存在的页会被拒);历史必须留,核实后仍有效就改回 `active`。
163
+ **`status` 只有三个值**:`active` 当前有效(缺字段默认);`draft` 待核实(`abs note` 默认,展示标 `[draft 未核实]`);`superseded` 已被推翻,别再依据(`query` 默认隐藏)。推翻用 `abs supersede <页名> --by <新页>`(写 `superseded-by`;不写 `--by` = 单纯弃用;指向不存在的页会被拒)。历史必须留,核实后仍有效就改回 `active`。
157
164
 
158
- - **每页必须有 `## 关联连接`**,用 `[[页面名]]` 链相关页,严禁孤岛;解释写在 `—` 后(`[[x]] — 节流判据要对齐真收尾`),只写链点等于没链。
159
- - **命名即链接**:`[[Docker]]` → `entities/Docker.md`;`[[docker-prisma-429]]` → `concepts/`。不建别名层。
160
- - **知识冲突**:不静默覆盖,加 `## 知识冲突` 两版都留、标来源时间,交人工裁决。
165
+ - **每页必须有 `## 关联连接`**,用 `[[页面名]]` 链相关页,解释写在 `—` 后(`[[x]] — 节流判据要对齐真收尾`),只写链点等于没链。
166
+ - **命名即链接**:`[[Docker]]` → `entities/Docker.md`,`[[docker-prisma-429]]` → `concepts/`。不建别名层。
167
+ - **知识冲突**:不静默覆盖,加 `## 知识冲突`,两版都留、标来源时间,交人工裁决。
161
168
  - 概念页骨架:`触发场景 / ❌表现(贴报错) / 🛠解法(根因+修复+验证命令) / 关联连接`。
162
169
 
163
170
  ## 维护:query / lint
164
171
 
165
- - `abs query <词>` → 读命中页 → 答用 `[[页面名]]` 标来源。**代码问题(符号在哪/谁调用)不在 .brain,直接读源码**;已推翻经验默认不返回(`--all` 可看)。
166
- - `abs lint` 报错码(`NO-INBOUND`/`SOURCE-UNDISTILLED`/`SUPERSEDED-DANGLING`/`DRAFT-STALE`/`NO-TAIL`)跑一次即见。
172
+ - `abs query <词>` → 读命中页 → 答用 `[[页面名]]` 标来源。代码问题(符号在哪/谁调用)不在 `.brain`,直接读源码;已推翻经验默认不返回(`--all` 可看)。
173
+ - `abs lint` 报错码:`NO-INBOUND` / `SOURCE-UNDISTILLED` / `SUPERSEDED-DANGLING` / `DRAFT-STALE` / `NO-TAIL`。
167
174
 
168
175
  ## ⛔ 禁止做
169
176
 
170
- 1. **禁止手工建骨架 / 手工登记任务** —— 用 `abs init` / `abs todo add`。
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/`。
175
- 6. **禁止编造没跑过的验证结论** —— 只写做过/跑过/测过的事实;不清楚就写"未定位"。
176
- 7. **禁止直写 Rules** —— 够格的先输出 `[Rules 提议]` 问用户,确认后才 `abs rule add`。
177
- 8. **禁止用宿主原生 todo 承载跨会话任务** —— 用 `abs_task`,原生 todo 只记本会话临时拆解。
177
+ 1. 禁止手工建骨架 / 手工登记任务 —— 用 `abs init` / `abs todo add`。
178
+ 2. 禁止由关键词直接触发动作 —— 拿不准先问。
179
+ 3. 禁止通读 `.brain/` —— 要状态用 `abs load`,要主题用 `abs query`,要单页用 `abs resolve` 后只读那页。
180
+ 4. 禁止删知识页表达"已失效" —— 用 `abs supersede`。
181
+ 5. 禁止归档单独建文件 / 一天拆多个快照 / 手工搬大文件 —— 用 `abs todo archive`;一天只有一个 `log-<日期>.md`;大文件进 `sources/` 或摘规律进 `concepts/`。
182
+ 6. 禁止编造没跑过的验证结论 —— 只写做过/跑过/测过的事实。
183
+ 7. 禁止直写 Rules —— 先 `[Rules 提议]` 问用户,确认后才 `abs rule add`。
184
+ 8. 禁止用宿主原生 todo 承载跨会话任务 —— 用 `abs_task`。
178
185
 
179
186
  ## 自我约束
180
187
 
181
- - 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外);只写真实发生的事实,守容量纪律。
182
- - 双链/frontmatter/index 必须自洽 —— 坏链 = 掰断接力棒。
183
-
188
+ 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外);双链、frontmatter、index 必须自洽,坏链等于掰断接力棒。
package/src/lint.js CHANGED
@@ -5,7 +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, parseChild } from './todo/common.js';
8
+ import { isAllowedTodoLine, todoSectionOf, parseChild, migrateLegacyChild } from './todo/common.js';
9
9
 
10
10
  // ---------- 图谱遍历 ----------
11
11
 
@@ -136,10 +136,27 @@ async function checkRootFiles(vault) {
136
136
  if (file !== 'todo.md') continue;
137
137
  for (let i = 0; i < lines.length; i++) {
138
138
  const l = lines[i];
139
+ // 旧 `↳` 子项写法(2026-10-20 废弃):lint 仍要能看见它,
140
+ // 报专门的迁移码而不是「裸文本」—— 两者出路不同。
141
+ const legacy = migrateLegacyChild(l);
142
+ if (legacy !== null) {
143
+ issues.push(
144
+ `TODO-LEGACY-CHILD: ${BRAIN_DIR}/${file}:${i + 1} (旧 ↳ 子项写法;` +
145
+ `跑一次 abs load 即可自动迁移为标准 md 缩进子项)`,
146
+ );
147
+ const legacyText = (parseChild(legacy) || {}).text || '';
148
+ if (legacyText.length > LINT_BREAKPOINT_MAX) {
149
+ issues.push(
150
+ `TODO-BREAKPOINT-BLOAT: ${BRAIN_DIR}/${file}:${i + 1} (子项 ${legacyText.length} 字 > ${LINT_BREAKPOINT_MAX};` +
151
+ `子项只写「改到哪个文件哪一步」,报告迁 sessions/ 或 sources/)`,
152
+ );
153
+ }
154
+ continue;
155
+ }
139
156
  // 裸文本行(与闸门 isAllowedTodoLine 同源,2026-10-11):Todo 区只能有任务行/断点/空行/标题。
140
157
  if (todoSectionOf(lines, i) === 'Todo' && !isAllowedTodoLine(l)) {
141
158
  issues.push(
142
- `TODO-STRAY-PROSE: ${BRAIN_DIR}/${file}:${i + 1} (Todo 区里的裸文本,不是任务行也不是断点;` +
159
+ `TODO-STRAY-PROSE: ${BRAIN_DIR}/${file}:${i + 1} (Todo 区里的裸文本,不是任务行也不是子项;` +
143
160
  `总结/报告/清单请分流:流水 → abs log,经验 → abs note,长设计 → sources/)`,
144
161
  );
145
162
  }
@@ -149,12 +166,12 @@ async function checkRootFiles(vault) {
149
166
  const bp = bpLine.text;
150
167
  if (bp.length > LINT_BREAKPOINT_MAX) {
151
168
  issues.push(
152
- `TODO-BREAKPOINT-BLOAT: ${BRAIN_DIR}/${file}:${i + 1} (断点 ${bp.length} 字 > ${LINT_BREAKPOINT_MAX};` +
153
- `断点只写「改到哪个文件哪一步」,报告迁 sessions/ 或 sources/)`,
169
+ `TODO-BREAKPOINT-BLOAT: ${BRAIN_DIR}/${file}:${i + 1} (子项 ${bp.length} 字 > ${LINT_BREAKPOINT_MAX};` +
170
+ `子项只写「改到哪个文件哪一步」,报告迁 sessions/ 或 sources/)`,
154
171
  );
155
172
  } else if (bp.split(/[。!?!?]+|\s*[\u2460-\u2473]\s*/).map((x) => x.trim()).filter(Boolean).length > 2) {
156
173
  issues.push(
157
- `TODO-BREAKPOINT-PROSE: ${BRAIN_DIR}/${file}:${i + 1} (断点写了多句 = 报告体;` +
174
+ `TODO-BREAKPOINT-PROSE: ${BRAIN_DIR}/${file}:${i + 1} (子项写了多句 = 报告体;` +
158
175
  `todo 只放待办,长文迁 sessions/ 或 sources/)`,
159
176
  );
160
177
  }
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, checkTaskBox, ensureStateMark, stateOfTaskLine, TODO_MAX_LINES, upsertTask, boardText, readTodo, todoTemplate, today, localStamp, setBreakpoint, setBreakpointDone, setMemo, readMemo, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText, upsertArchiveSection, DONE_KINDS, withDoneKind, collapseDone, SEC, rebuildStructure, enforceBrainFormat, sessionOf, parseChild } from './todo.js';
7
+ import { stripStateMark, checkTaskBox, ensureStateMark, stateOfTaskLine, TODO_MAX_LINES, upsertTask, boardText, readTodo, todoTemplate, today, localStamp, setBreakpoint, setBreakpointDone, setMemo, readMemo, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText, upsertArchiveSection, DONE_KINDS, withDoneKind, collapseDone, SEC, rebuildStructure, enforceBrainFormat, sessionOf, parseChild, migrateLegacyChild } 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';
@@ -367,7 +367,7 @@ export async function cmdLoad({ dir }) {
367
367
  );
368
368
  if (stranded.length) {
369
369
  const rows = stranded.map((t) => {
370
- const bp = t.bp.length ? `\n ${t.bp.map((b) => (b.startsWith('[') ? `↳ ${b}` : `↳ 断点: ${b}`)).join('\n ')}` : '';
370
+ const bp = t.bp.length ? `\n ${t.bp.map((b) => (b.startsWith('[') ? `- ${b}` : `- [ ] ${b}`)).join('\n ')}` : '';
371
371
  return ` - ${t.body}${bp}`;
372
372
  });
373
373
  // 插在项目行之后、其它内容之前
@@ -375,7 +375,7 @@ export async function cmdLoad({ dir }) {
375
375
  1, 0,
376
376
  '⏳ 上会话滞留(未 done,先对账)',
377
377
  rows.join('\n'),
378
- '→ 完成: abs todo done <id>;未完: abs todo note <id> --note 断点',
378
+ '→ 完成: abs todo done <id>;未完: abs todo note <id> --note 子项',
379
379
  ''
380
380
  );
381
381
  }
@@ -430,8 +430,9 @@ export function extractBlocks(todoText) {
430
430
  // 完成行:不再跟踪
431
431
  if (/^\s*-\s*\[x\]/.test(line)) { curId = null; continue; }
432
432
  if (!curId) continue;
433
- // 附属行(↳ 开头或缩进行)里找卡点前缀
434
- const m = line.match(/^\s*(?:↳\s*断点:\s*)?(?:阻塞|等待|等)[::]\s*(.+)$/);
433
+ // 附属行(缩进子项)里找卡点前缀。
434
+ // 旧写法 `↳ 断点: 阻塞: …`(存量)也认 —— 保留一条前缀兼容,以免漏报历史卡点。
435
+ const m = line.match(/^\s*(?:↳\s*(?:断点|卡点)?[::]\s*)?(?:阻塞|等待|等)[::]\s*(.+)$/);
435
436
  if (m && m[1].trim()) out.push({ id: curId, why: m[1].trim() });
436
437
  }
437
438
  return out;
@@ -957,10 +958,12 @@ export async function cmdTask({ dir, action, id, section, note, as, session, n }
957
958
  const r = await setBreakpoint(root, { id, text: note });
958
959
  return r.msg;
959
960
  }
960
- // 勾/取消勾断点:断点是待办,做完要勾(2026-10-16)。
961
+ // 勾/取消勾子项:子项也是待办,做完要勾。
961
962
  // 单独两个子命令而不是 `--done true/false` —— 勾与取消是两件事,各自一个动词更顺手。
962
- if (action === 'bp-done' || action === 'bp-undone') {
963
- const r = await setBreakpointDone(root, { id, n, done: action === 'bp-done' });
963
+ // 2026-10-20:命令名从 bp-done/bp-undone 改为 child-done/child-undone
964
+ //(「断点」这个叫法已废弃,子项就是标准 md 嵌套列表项)。
965
+ if (action === 'child-done' || action === 'child-undone') {
966
+ const r = await setBreakpointDone(root, { id, n, done: action === 'child-done' });
964
967
  return r.msg;
965
968
  }
966
969
  if (action === 'memo') {
@@ -1039,10 +1042,14 @@ async function markDone(file, id, kind = '落地', conclusion, bySid = '') {
1039
1042
  else leftover.push(kid.text);
1040
1043
  continue;
1041
1044
  }
1042
- if (nx.trimStart().startsWith('↳')) {
1043
- const ch = lines[++i];
1044
- if (/^\s*↳\s*\[ \]/.test(ch)) leftover.push(ch.trim().replace(/^\s*↳\s*\[ \]\s*/, ''));
1045
- else bp.push(ch);
1045
+ // 防御:旧 `↳` 行本应已在 normalizeTodo 里迁掉。万一有路径绕过迁移,
1046
+ // 这里必须**消费掉该行**(i++),否则外层 while 原地打转(2026-10-18 OOM 实测)。
1047
+ const legacyChild = migrateLegacyChild(nx);
1048
+ if (legacyChild !== null) {
1049
+ i++; // 消费掉这一行,防止原地打转
1050
+ const parsed2 = parseChild(legacyChild);
1051
+ if (parsed2 && !parsed2.done) leftover.push(parsed2.text);
1052
+ else bp.push(legacyChild);
1046
1053
  continue;
1047
1054
  }
1048
1055
  break;
@@ -1065,7 +1072,7 @@ async function markDone(file, id, kind = '落地', conclusion, bySid = '') {
1065
1072
  `✗ 任务 "${wantId}" 还有 ${leftover.length} 条子任务未完成,不能 done:\n` +
1066
1073
  leftover.map((p) => ` - [ ] ${p}`).join('\n') +
1067
1074
  `\n子任务也是任务,主子全部完成才能 done。先二选一:\n` +
1068
- ` • 做完了 → abs todo bp-done ${wantId} (勾最后一条;指定第 N 条:bp-done ${wantId} --n N)\n` +
1075
+ ` • 做完了 → abs todo child-done ${wantId} (勾最后一条;指定第 N 条:child-done ${wantId} --n N)\n` +
1069
1076
  ` • 不做了 → 删掉那几行子项,再 abs todo done ${wantId}`,
1070
1077
  );
1071
1078
  }
@@ -1,6 +1,6 @@
1
1
  // src/todo/archive.js — Done 结语契约与任务归档(分组、折叠、迁归档区)。
2
2
 
3
- import { today, SEC, isTaskLine, isChildLine, daysAgo } from './common.js';
3
+ import { today, SEC, isTaskLine, isChildLine, daysAgo, migrateLegacyChild } from './common.js';
4
4
  import { ARCHIVE_HEADING, ARCHIVE_SECTION } from './spec.js';
5
5
 
6
6
  export function doneDateOf(line) {
@@ -39,7 +39,10 @@ export function withDoneKind(line, kind) {
39
39
  function parseDoneUnits(bodyLines) {
40
40
  const units = [];
41
41
  let cur = null;
42
- for (const l of bodyLines) {
42
+ for (const raw of bodyLines) {
43
+ // 旧 `↳` 子项先迁移(2026-10-20)——否则它不再被认成子项,会从所属任务里“掉出去”,
44
+ // 归档时要么丢失、要么归错日期组。
45
+ const l = migrateLegacyChild(raw) ?? raw;
43
46
  if (/^### /.test(l.trim()) || l.trim() === '') { cur = null; continue; } // 分组标题/空行断开会话
44
47
  // 先判子项再判任务:缩进的 `- [ ]` 两个判据都为真,而它应挂到父任务。
45
48
  // (标准 md 嵌套下子项本身就是任务行 —— 靠缩进区分层级,不靠形态。)
@@ -104,47 +104,56 @@ export function sentenceCount(s) {
104
104
  * 子任务:缩进的 `- [ ]` 行(标准 Markdown 嵌套列表,2 空格一层)。
105
105
  * 两者**都是任务行**(子任务可各自勾选);层级的区分看缩进,不在本函数。
106
106
  *
107
- * 为何也收 `↳ [ ]`:2026-10-16 早上刚把断点任务升级成一等任务(存量数据里有),
108
- * 现在改用标准 md 嵌套列表,旧形态继续认(存量不能变“看不见”)。 */
107
+ * 2026-10-20:旧 `↳ [ ]` 写法已彻底废弃,本函数不再认它。
108
+ * 存量由 migrateLegacyChild 在 load 时就地转成标准 md(见下)。 */
109
109
  export function isTaskLine(l) {
110
- // 先排旧式断点行:`↳ [ ] 断点: x` 形似任务行,但它带 `断点:` ⇒ 是子项文本,不是任务。
111
- if (/^\s*↳\s*\[[ x]\]\s*断点[::]/.test(l)) return false;
112
- return /^\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])/.test(l);
110
+ return /^\s*-\s*\[[ x]\]/.test(l);
113
111
  }
114
112
 
115
- /** 一行是否任务附属行(子项):缩进的 `- [ ]`,或旧的 `↳ …` 形态。
116
- *
117
- * 标准 md 下子项就是缩进的列表项 —— 它本身也是任务行(isTaskLine 为真),
118
- * 但**不单独成条**:它挂在父任务下,不占顶层 id、不参与 `abs todo done`。
119
- * 故 isChild 的判据是「有缩进」,不是「形态不同」。 */
113
+ /** 一行是否任务附属行(子项):缩进的 `- [ ]`。 */
120
114
  export function isChildLine(l) {
121
- if (/^\s*↳/.test(l)) return true; // 旧形态(含 `↳ [ ]` 断点任务)
122
- return /^\s+-\s*\[[ x]\]/.test(l); // 缩进的 `- [ ]`(新形态)
115
+ return /^\s+-\s*\[[ x]\]/.test(l);
123
116
  }
124
117
 
125
118
  // ---------- 子项行(标准 md 嵌套列表:缩进 2 空格 + `- [ ]`) ----------
126
- /** 子项 = 挂在任务下的可勾条目(旧的「断点」就是一条子项)。
119
+ /** 子项 = 挂在任务下的可勾条目。
127
120
  *
128
- * 格式(2026-10-16 用户定):标准 Markdown 嵌套列表,不搞自定义前缀。
121
+ * 格式(2026-10-16 用户定,2026-10-20 彻底收口):标准 Markdown 嵌套列表。
129
122
  * - [ ] [进行中] 主任务
130
123
  * - [x] 已完成的子项
131
124
  * - [ ] 还没做的子项
132
- * 旧格式 `↳ 断点: 内容` **继续承认**(存量数据不能坏),新写入一律用标准 md。 */
125
+ *
126
+ * 旧的 `↳ 断点: 内容` / `↳ [ ] 断点: 内容` 写法**已移除**,不再被解析。
127
+ * 存量靠 migrateLegacyChild 一次性迁移(那是全仓唯一还认得 `↳` 的地方)。 */
133
128
  export const CHILD_INDENT = ' ';
134
129
 
130
+ /** 唯一还认得旧 `↳` 写法的地方 —— 存量迁移。
131
+ *
132
+ * 为何保留这一个函数(2026-10-20 用户定:彻底去掉断点写法,但存量要自动迁移):
133
+ * 其它项目的 todo.md 里已有旧行(实测 corp_agent 1 行、obsidian_bridge 1 行、
134
+ * zj_shop 6 行,形态还包括 `↳ 内容:` 这种变体)。直接删判据会让这些行变成
135
+ * 「裸文本」而被闸门拒写 → 那几个项目的 todo 全部锁死。
136
+ * 所以保留这一个「只读旧形态、产出新形态」的转换器,写回后旧行就不存在了。
137
+ *
138
+ * 返回 null = 不是旧形态(不用迁移);否则返回 ` - [ ] 内容` 形态的行。
139
+ * 裸 `↳ 内容`(无复选框)按未完成子项处理 —— 旧格式里它就是「待办的一句话」。
140
+ * `↳ [x] 内容` 保留已勾状态。
141
+ * `↳ [ ]` 后面没内容 = 旧一等任务的空壳,迁移成 `- [ ] (空)` 没意义 → 返回 null 由闸门拒。 */
142
+ export function migrateLegacyChild(l) {
143
+ const s = String(l || '');
144
+ const m = s.match(/^(\s*)↳\s*(?:\[([ x])\]\s*)?(?:断点|卡点|内容)[::]?\s*(.*)$/)
145
+ || s.match(/^(\s*)↳\s*(?:\[([ x])\]\s*)?(.*)$/);
146
+ if (!m) return null;
147
+ const text = (m[3] || '').trim();
148
+ if (!text) return null; // 空壳子:不猜,交给闸门拒
149
+ const done = m[2] === 'x';
150
+ return `${m[1]}- ${done ? '[x]' : '[ ]'} ${text}`;
151
+ }
152
+
135
153
  /** 解析子项行 → { done, text, indent };不是子项返回 null。
136
- * 两种形态都收:新的 ` - [ ] 内容` 与旧的 ` ↳ [ ] 断点: 内容` / ` ↳ 断点: 内容`。 */
154
+ * 只认标准 md(缩进的 `- [ ]`)。旧 `↳` 形态请先过 migrateLegacyChild。 */
137
155
  export function parseChild(l) {
138
- const s = String(l || '');
139
- // 旧形态(↳ 开头):剥掉 ↳ 与可选复选框及可选的 `断点:` 标签
140
- const old = s.match(/^(\s*)↳\s*(\[[ x]\])?\s*(?:断点[::]\s*)?(.*)$/);
141
- if (old) {
142
- // 勾选看 ↳ 后的那个复选框;旧格式无勾 ⇒ false
143
- if (!old[3].trim()) return null; // `↳ [ ]` 后面没内容 = 不是子项(是旧一等任务的空壳)
144
- return { done: old[2] === '[x]', text: old[3].trim(), indent: old[1].length };
145
- }
146
- // 新形态(缩进的 `- [ ]`)—— 必须缩进,否则那是顶层任务
147
- const m = s.match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/);
156
+ const m = String(l || '').match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/);
148
157
  if (!m) return null;
149
158
  return { done: m[2] === 'x', text: m[3].trim(), indent: m[1].length };
150
159
  }
@@ -161,7 +170,7 @@ export function renderChild(text, { done = false, indent = CHILD_INDENT } = {})
161
170
  * 于是把总结写成「紧跟任务行的裸文本行」就三层全部放行:闸门不看、形状规则不管、
162
171
  * lint 只查断点行 —— 实测 .agents/.brain/todo.md 被塞进 10 行实施报告而 lint 报 0 问题。
163
172
  * 前面的修法都挂在「行的形态」上(先修超长断点、再修多句断点),换个形态就绕过。
164
- * 改成白名单:Todo 区里除了任务行/断点/空行/标题,别的都是非法。
173
+ * 改成白名单:Todo 区里除了任务行/子项/空行/标题,别的都是非法。
165
174
  * 只在 `## Todo` 区生效 —— Done/Archived 区有日期分组与归档链接,判据不同。
166
175
  * 返回 false 才叫非法。 */
167
176
  export function isAllowedTodoLine(l) {
@@ -169,12 +178,12 @@ export function isAllowedTodoLine(l) {
169
178
  if (!t) return true; // 空行
170
179
  if (/^#{1,3}\s/.test(t)) return true; // `## Todo` / `### …` 标题
171
180
  if (isTaskLine(l)) return true; // 任务行
172
- if (isChildLine(l)) return true; // ↳ 断点附属行
181
+ if (isChildLine(l)) return true; // 缩进子项
173
182
  return false;
174
183
  }
175
184
 
176
185
  /** 把 Done 区文本(bodyLines,不含 `## Done` 标题)解析成任务单元 [{ date, lines:[main,...children] }]。
177
- * 剥 `### 日期` 分组标题与空行;任务行下紧跟的 ↳ 行并入该单元。 */
186
+ * 剥 `### 日期` 分组标题与空行;任务行下紧跟的缩进子项并入该单元。 */
178
187
 
179
188
  export function daysAgo(n, from) {
180
189
  const [y, m, d] = String(from).split('-').map(Number);
@@ -196,9 +205,9 @@ export function isPlaceholder(line) {
196
205
 
197
206
  export function extractId(text) {
198
207
  // 先剥行首状态标记(`[进行中] <id>`),否则 id 会取成 "[进行中]"(2026-09-13 实测)。
199
- // 再剥具体的 `- [ ] ` / `↳ [ ] ` 前缀(CLI 传完整行时)。
208
+ // 再剥具体的 `- [ ] ` 前缀(CLI 传完整行时)。
200
209
  const t = String(text)
201
- .replace(/^\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*/, '')
210
+ .replace(/^\s*-\s*\[[ x]\]\s*/, '')
202
211
  .replace(/^\[[^\]]+\]\s+/, '');
203
212
  // id = 到第一个空白 / `[` 为止的连续字符(id 不允许含空格或方括号)。
204
213
  // ★ 必须支持**中文 id**(2026-10-16 实测):原正则 `^[A-Za-z][\w-]*` 要求英文开头,
package/src/todo/spec.js CHANGED
@@ -55,8 +55,6 @@ export const INDEX_LINK_SECTIONS = ['Concepts', 'Entities', 'Sources', 'Synthese
55
55
 
56
56
  export const LOG_KINDS = ['note', 'dev', 'concept', 'ingest'];
57
57
 
58
- /** 条目形状规则表:文件 → 每行的预期形状。
59
- * 校验器只报告「不像这个形状」的行,不自动改写内容(机器不猜语义)。 */
60
58
  /** 条目形状规则表:文件 → 每行的预期形状。
61
59
  * 校验器只报告「不像这个形状」的行,不自动改写内容(机器不猜语义)。 */
62
60
 
@@ -156,7 +154,7 @@ export const ENTRY_SHAPES = {
156
154
  // 不是事后逐行猜(猜不出哪行是新的、哪行是旧的)。
157
155
  test: (l) => {
158
156
  if (/^#{1,3}\s/.test(l)) return null; // 标题行(## Done / ### 日期)不是条目
159
- if (!/^-\s/.test(l)) return null; // 非条目行(如缩进的 ↳ 断点)由下面单独判
157
+ if (!/^-\s/.test(l)) return null; // 非条目行(如缩进子项)由下面单独判
160
158
  // Done 区的归档行:`- [[log-日期]] 完成任务 N 条`。
161
159
  // ★ 历史命名不统一:还有 `[[2026-09-12-todo归档]]` 这种旧写法(实测),
162
160
  // 只认 `log-` 前缀会把它误报为「缺状态标记」。状归档行与任务行的
@@ -172,8 +170,8 @@ export const ENTRY_SHAPES = {
172
170
  if (m[1] === ' ' && !/^\[[^\]]+\]\s*\S/.test(rest)) {
173
171
  return '未完成任务应为 `- [ ] [状态] …`(状态:进行中/讨论中/滞留中)';
174
172
  }
175
- // 断点附属行不能挤在同一行(应另起 ` ↳ [ ] 断点: `)
176
- if (/↳\s*(?:\[[ x]\]\s*)?断点[::]/.test(rest)) return '断点要另起一行写 ` ↳ [ ] 断点: …`';
173
+ // 子项不能挤在同一行(应另起 ` - [ ] `)
174
+ if (/↳/.test(rest) || /\[\s*\]\s*断点/.test(rest)) return '子项要另起一行写 ` - [ ] …`';
177
175
  return null;
178
176
  },
179
177
  },