@fanchao8609/agent_brain_sync 1.8.7 → 1.8.8
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/hooks/abs.opencode.ts +3 -6
- package/hooks/abs.pi.ts +4 -7
- package/package.json +2 -2
- package/skill/abs-agent-brain-sync/SKILL.md +27 -4
- package/skill/abs-think-tree/SKILL.md +194 -0
- package/skill/abs-think-tree/check.js +119 -0
- package/skill/abs-think-tree/check.test.js +161 -0
- package/src/store.js +5 -9
package/hooks/abs.opencode.ts
CHANGED
|
@@ -54,12 +54,9 @@ const server = async ({ client, directory }) => {
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
const TEARDOWN_MSG =
|
|
57
|
-
"[abs
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"3) 值得留的经验 abs note \"...\"(宁少勿滥,能从代码 grep 到的不记);\n" +
|
|
61
|
-
"4) abs log \"完成 X:...\" 记一行工作成果,新页同步进 index。\n" +
|
|
62
|
-
"简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\"无可沉淀\"即可。"
|
|
57
|
+
"[abs] 本会话改过文件,.brain/ 今日无记录。\n" +
|
|
58
|
+
"这条是信息不是命令:该沉淀就沉淀,没有可沉淀的直接回一句「无可沉淀」,不用凑。\n" +
|
|
59
|
+
"需要时:abs todo / abs todo done <id> / abs note \"...\" / abs log \"...\""
|
|
63
60
|
|
|
64
61
|
// bash 里只跑查询类命令不算改文件 (与 pi 侧 READONLY_CMD 同义, 但生成代码里要写进模板串)
|
|
65
62
|
const READONLY_CMD = /^\s*(ls|cat|grep|rg|find|head|tail|wc|git\s+(status|log|diff|show|branch)|pwd|which|echo|node\s+-v|npm\s+(ls|view)|curl)\b/
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -176,13 +176,10 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
176
176
|
const notes = sessionNotes.splice(0, sessionNotes.length)
|
|
177
177
|
try {
|
|
178
178
|
pi.sendUserMessage(
|
|
179
|
-
"[abs
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
|
|
183
|
-
"4) abs log \"完成 X:...\" 记一行工作成果,新页同步进 index。\n" +
|
|
184
|
-
notesBlock(notes) +
|
|
185
|
-
"简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\"无可沉淀\"即可。",
|
|
179
|
+
"[abs] 本会话改过文件,.brain/ 今日无记录。\n" +
|
|
180
|
+
"这条是信息不是命令:该沉淀就沉淀,没有可沉淀的直接回一句「无可沉淀」,不用凑。\n" +
|
|
181
|
+
"需要时:abs todo / abs todo done <id> / abs note \"...\" / abs log \"...\"" +
|
|
182
|
+
notesBlock(notes),
|
|
186
183
|
{ deliverAs: "followUp" },
|
|
187
184
|
)
|
|
188
185
|
} catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanchao8609/agent_brain_sync",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.8",
|
|
4
4
|
"description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"abs": "node bin/abs.js",
|
|
36
36
|
"mcp": "node bin/mcp.js",
|
|
37
|
-
"test": "node --test 'test/*.test.js'",
|
|
37
|
+
"test": "node --test 'test/*.test.js' 'skill/**/*.test.js'",
|
|
38
38
|
"prepack": "node -e \"require('fs').chmodSync('bin/abs.js',0o755);require('fs').chmodSync('bin/mcp.js',0o755)\"",
|
|
39
39
|
"pack:check": "npm pack --dry-run"
|
|
40
40
|
}
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abs-agent-brain-sync
|
|
3
|
-
description: abs (agent-brain-sync) 跨会话 AI 编码记忆与任务续接。开场续接状态(abs load/MCP abs_load),干活中任务/经验实时落盘(abs_task/abs_note)
|
|
3
|
+
description: abs (agent-brain-sync) 跨会话 AI 编码记忆与任务续接。开场续接状态(abs load/MCP abs_load),干活中任务/经验实时落盘(abs_task/abs_note),需要收尾时才走收尾循环。解决会话无状态:经验/进度/坑碎片化、重开失忆。遇 bug 排查时配合挂载 bug-hunter skill。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# abs — 跨会话记忆 (agent-brain-sync)
|
|
7
7
|
|
|
8
|
+
## 最高优先:触发总则(凌驾本文所有流程)
|
|
9
|
+
|
|
10
|
+
**关键词不是触发器,意图才是。**
|
|
11
|
+
|
|
12
|
+
句子里出现 abs 词(收尾/todo/沉淀/提示/load/log/note)**不等于**要执行 abs 动作。
|
|
13
|
+
|
|
14
|
+
| 用户的意思 | 做什么 |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 谈论、提问、吐槽、定规则("太啰嗦""要简短""这个设计怎样") | **只回应,不执行动作** |
|
|
17
|
+
| 明确让我做事("收尾""记一下""去做") | 执行 |
|
|
18
|
+
|
|
19
|
+
**判断依据是整段话的意图,不是里面出现过哪个词。**
|
|
20
|
+
|
|
21
|
+
反例(真实发生):用户说"尽量简短的汇报" —— 那是在**定规则**,不是在**下命令**;
|
|
22
|
+
被误当成指令跑了一次收尾。
|
|
23
|
+
|
|
24
|
+
推论:
|
|
25
|
+
- 拿不准时**先问**,不要靠关键词直接动手。
|
|
26
|
+
- 规则变更("以后简短点")写入本文,**不执行动作**。
|
|
27
|
+
- 输出简短:收尾/提示/todo 汇报尽量一两句,不写小作文。
|
|
28
|
+
- 本总则适用于所有工具,不限 abs。
|
|
29
|
+
|
|
8
30
|
把 AI 编码经验从会话沙盒里救出来。每个会话都是无状态的——Claude、OpenCode、Cursor
|
|
9
31
|
各开一堆会话,经验/进度/踩坑全碎片化,重开像失忆。本技能用一个放**项目根目录**、
|
|
10
32
|
Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
@@ -256,7 +278,7 @@ abs todo archive # 一条命令搞定,无需手工搬
|
|
|
256
278
|
> OpenCode todowrite / pi `/list`)—— 那些多是会话内临时,不写 `.brain/todo.md`,
|
|
257
279
|
> 下会话接不上、收尾没影。原生 todo 顶多记“本会话不跨断点的临时拆解”。
|
|
258
280
|
|
|
259
|
-
##
|
|
281
|
+
## 收尾循环(用户明确要求收尾时才走)
|
|
260
282
|
|
|
261
283
|
**每个任务边界、被 Stop/打断、告一段落时,别停半空。** 这是“开场接上状态、结束落回状态”的闭环。
|
|
262
284
|
|
|
@@ -264,10 +286,11 @@ abs todo archive # 一条命令搞定,无需手工搬
|
|
|
264
286
|
> 下会话 `abs load` 会自动把滞留顶到顶部(`⏳ 上会话滞留`)—— 所以收尾不靠自觉,是开场被强制接上。
|
|
265
287
|
>
|
|
266
288
|
> **主动注入**:pi 扩展在 `agent_end` 检测「本会话真改过文件」且「log.md 今日无记录」时注入
|
|
267
|
-
> `[abs 收尾提醒]
|
|
289
|
+
> `[abs 收尾提醒]`(每会话最多一次)。**它是一条信息,不是命令** ——
|
|
290
|
+
> 自己判断该不该沉淀;没有可沉淀的就回一句「无可沉淀」,不用强行凑。
|
|
268
291
|
> Claude/Codex 靠 `Stop` 事件达成同样效果。
|
|
269
292
|
|
|
270
|
-
|
|
293
|
+
当用户**明确说要收尾/结束/切别的事**时,按下面走(不是每个词都触发,见开头总则):
|
|
271
294
|
|
|
272
295
|
1. **读 todo** → `abs load`,看 Todo 还有哪些没完成。
|
|
273
296
|
2. **判有没有做完没登记** → 实际完成了漏登记的 `abs todo done <id>`;做到一半补
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: abs-think-tree
|
|
3
|
+
description: 回答前的固定思考骨架 —— 三层下坠式自问, 每层出多个候选互相竞争, 最后一个问题可给多个答案。用于减少"急于表达、只给一个答案、把无答案伪装成有答案"。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 三层思考骨架
|
|
7
|
+
|
|
8
|
+
## 为什么需要它
|
|
9
|
+
|
|
10
|
+
默认失败模式有三个,且都不是"不够聪明"造成的:
|
|
11
|
+
|
|
12
|
+
| 失败模式 | 表现 |
|
|
13
|
+
|---------|------|
|
|
14
|
+
| 急于表达 | 还没想清就写答案,答案是第一个想到的 |
|
|
15
|
+
| 只给一个答案 | 明明有几种合理解读,只交付一种,另外几种当没想过 |
|
|
16
|
+
| 无答案装成有答案 | 问题本身没有确定答案,硬凑一个自圆其说的 |
|
|
17
|
+
|
|
18
|
+
**不是让模型变聪明,是让它的偷懒可见。** 骨架填不满 = 露怯,比给个像样的答案更有价值。
|
|
19
|
+
|
|
20
|
+
## 核心:下坠 + 竞争
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
根: 用户原话(外部给定,不可改)
|
|
24
|
+
└ L1 他为什么问 出 n 个候选,留最贴合原话的 ← 防答非所问
|
|
25
|
+
└ L2 边界在哪 三条轴上定位,留最贴合 L1 的 ← 防答错方向(默认留 2)
|
|
26
|
+
└ L3 他要什么 写出可验收产出形态,留最贴合 L2 的 ← 防给不出东西
|
|
27
|
+
↓
|
|
28
|
+
回答:沿存活路径给,有几条活路径就给几个答案
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**三层职能各自独立,不可合并**:L1 定动机 / L2 定边界 / L3 定产出。
|
|
32
|
+
少了 L2,答案容易落在错的轴上(问主观答客观、问当下答长期)。
|
|
33
|
+
|
|
34
|
+
**竞争的关键:每层的裁判是父节点,不是答案。** 兄弟候选共享同一个父,所以能横向比较——
|
|
35
|
+
"这两条哪个更贴合上面的问题",这是相对判断,不是自评,也不是造证据。
|
|
36
|
+
|
|
37
|
+
**父节点始终是外部给定的**(L1 的父是用户原话),所以裁判链是外部的,模型无法自己定标准。
|
|
38
|
+
|
|
39
|
+
## 留几个(关键)
|
|
40
|
+
|
|
41
|
+
留几个**不看你想要几个,看有没有依据**:
|
|
42
|
+
|
|
43
|
+
| 同层候选之间的差距 | 留 k | 依据 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| 有明显差距(有候选明显不贴合父节点) | 可以留 1 | 竞争胜出,有裁判 |
|
|
46
|
+
| **无差距(都同样贴合)** | **必须留 2** | 竞争无法裁决,砍任何一个都是无依据 |
|
|
47
|
+
|
|
48
|
+
**默认留 2。** 因为大多数情况下同时存在几个同样合理的视角,而**同样合理时砍掉一个,是拿单一视角冒充完整回答**——这正是这套骨架要防的病。
|
|
49
|
+
|
|
50
|
+
**无差距时强行留 1 的后果**(实测):问"要不要迁移到新框架",两个 L1 候选(评估成本收益 / 遇瓶颈想换)都贴合原话,
|
|
51
|
+
它们的 L2 边界差别只在"当下/长期"一条轴。留 1 就必须砍掉一边——
|
|
52
|
+
**但这里没有胜者,砍谁都是丢掉一个合法视角。**
|
|
53
|
+
|
|
54
|
+
**竞争结构的边界**:它只能淘汰差的,不能淘汰同样好的。兄弟差距为零时,没有裁判。
|
|
55
|
+
|
|
56
|
+
## 流程
|
|
57
|
+
|
|
58
|
+
### L1 他为什么问 —— 出 3 个,留 2
|
|
59
|
+
|
|
60
|
+
写 3 个**具体的后续动作**,不是心情。
|
|
61
|
+
|
|
62
|
+
- ✅ "想快速定位原因"、"想确认是不是自己改错了"、"想让测试通过"
|
|
63
|
+
- ❌ "想了解这个技术"、"想学习一下"(空泛,等于没写)
|
|
64
|
+
|
|
65
|
+
**基准**:用户原话。写不出的直接标"原话不足以判断"。
|
|
66
|
+
|
|
67
|
+
### L2 他在问什么 —— 每支出 2 个,留 1~2(同上:无差距时留 2)
|
|
68
|
+
|
|
69
|
+
**L2 的职能是划定问题边界,不是复述问题。** 复述不产生信息;划边界产生信息。
|
|
70
|
+
|
|
71
|
+
对每个存活的 L1,在三条轴上定位这个问题的边界:
|
|
72
|
+
|
|
73
|
+
| 轴 | 两端 | 划错的后果 |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| 主观 / 客观 | "好不好吃" vs "营养成分" | 答成客观数据 |
|
|
76
|
+
| 当下 / 长期 | "这顿吃啥" vs "长期吃水果好不好" | 答成养生建议 |
|
|
77
|
+
| 判断 / 操作 | "要不要买" vs "怎么挑" | 给方法但没给结论 |
|
|
78
|
+
|
|
79
|
+
- **每条轴必须落到一端**,不许写"两者都有"。
|
|
80
|
+
- 必须能跟 L1 对上。
|
|
81
|
+
|
|
82
|
+
**落不下去时,区分两种原因(路径生死相同,输出描述不同):**
|
|
83
|
+
|
|
84
|
+
| 原因 | 含义 | 输出里怎么说 |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| 落不下去,因为候选本身空泛 | 动机没说清(如"想了解苹果") | "剪掉:动机不具体" |
|
|
87
|
+
| 落不下去,因为信息不够 | 动机成立但原话没有线索 | "悬置:需要你补充 X" |
|
|
88
|
+
|
|
89
|
+
两者都使这一支不往下走,**但前者是判断,后者是缺信息**。不要把缺信息写成判断。
|
|
90
|
+
|
|
91
|
+
- 不同 L1 可能落到同一个边界 → **汇聚 = 强信号**,记录下来。
|
|
92
|
+
|
|
93
|
+
**示例**:"苹果好吃吗"
|
|
94
|
+
- L1 = 想选水果 → L2 = 主观 + 当下 + 判断
|
|
95
|
+
- 这个边界直接挡住"富含维 C"(客观)和"长期吃水果的好处"(长期)—— 那是在答另一个问题
|
|
96
|
+
|
|
97
|
+
**L2 是没它就容易答错方向的一层。** 跳过 L2 直接由 L1 生成答案,最常见的失败是答案落在错的轴上。
|
|
98
|
+
|
|
99
|
+
### L3 他要什么 —— 写出可验收的产出形态
|
|
100
|
+
|
|
101
|
+
这一层是**全流程的地基**。
|
|
102
|
+
|
|
103
|
+
- ✅ "一段能跑的代码"、"一个明确结论"、"2-3 个选项让我选"、"一个追问"
|
|
104
|
+
- ❌ "一个全面的回答"、"一些建议"(不可验收)
|
|
105
|
+
|
|
106
|
+
**写不出可验收形态时,不许硬写。** 直接说明:
|
|
107
|
+
> 这个问题没有可验收的标准,我给的是参考,不是答案。
|
|
108
|
+
|
|
109
|
+
这一条是本骨架最重要的产物——它把"无答案"从失败变成一个合法输出。
|
|
110
|
+
|
|
111
|
+
### 回答
|
|
112
|
+
|
|
113
|
+
沿存活路径写。四条规矩:
|
|
114
|
+
|
|
115
|
+
1. **每条不同的 L2/L3 路径对应一个答案,不合并。** 有 3 条活路径就给 3 个答案。
|
|
116
|
+
2. **汇聚优先,分歧附为例外**(不是并列):
|
|
117
|
+
- 多条路径汇聚 → 当作**主结论**,可标"多条路径汇聚于此"
|
|
118
|
+
- 少数路径分歧 → 当作**例外**附在后面,写明它在哪条轴上不同
|
|
119
|
+
- ❌ 不要写成两个平行答案 —— 那会让用户以为两者同等重要
|
|
120
|
+
|
|
121
|
+
**示例**(三选一汇聚、一条分歧):
|
|
122
|
+
> 主:问的是**主观口味**,苹果甜脆多汁,这几个维度自己判断。
|
|
123
|
+
> 例外:**如果你要给小孩吃,问题就变成安全性(客观轴)**,那是另一个问题,得单独说。
|
|
124
|
+
|
|
125
|
+
3. **不选最优。** 骨架禁止输出"综合来看最佳答案是……"这种合并。
|
|
126
|
+
4. **不回头改上层。** L3 写不出可验收形态时,不许回头改 L2 的边界。
|
|
127
|
+
**理由:下级不能改上级。** L3 没有比 L2 更高的裁判,让它改 L2 = 自证。
|
|
128
|
+
正确做法:直接在 L3 露怯("这题我给参考不是答案")。
|
|
129
|
+
|
|
130
|
+
## 能力边界(不是 bug,无法靠本骨架解决)
|
|
131
|
+
|
|
132
|
+
竞争结构的裁判链是"父节点",而父节点有它看不到的地方:
|
|
133
|
+
|
|
134
|
+
| 边界 | 说明 |
|
|
135
|
+
|---|---|
|
|
136
|
+
| **只能淘汰差的,不能淘汰同样好的** | 兄弟候选同样贴合父节点时,没有裁判 |
|
|
137
|
+
| **只能一致地错,不能发现自己错了** | 父节点错了(如 L1 动机判断错),子节点会沿着它一致错下去;竞争范围只在兄弟之间,淘汰不了父 |
|
|
138
|
+
| **无外部信号** | 要发现"父错了",只能靠用户纠正或可运行的验证(编译/测试/算数) |
|
|
139
|
+
|
|
140
|
+
**本骨架保证一致性,不保证方向。** 它防的是"同一套逻辑内部自相矛盾";
|
|
141
|
+
不防"整套逻辑从根上就错了"——那需要外部裁判。
|
|
142
|
+
|
|
143
|
+
## 自检(答完前扫一眼)
|
|
144
|
+
|
|
145
|
+
| 检查项 | 不合格的样子 |
|
|
146
|
+
|-------|-------------|
|
|
147
|
+
| L1 三条是否互斥? | 三条其实是同一件事的换说法 |
|
|
148
|
+
| L1 是否具体到动作? | "想了解一下" |
|
|
149
|
+
| L2 三条轴是否都落到一端? | 写"两者都有"、没划边界就进 L3 |
|
|
150
|
+
| L2 能否对回 L1? | 对不上还留着 |
|
|
151
|
+
| L3 是否可验收? | "给出好的回答" |
|
|
152
|
+
| 是否偷偷合并了多解? | 说了三种可能,最后只答一种 |
|
|
153
|
+
| 分歧是否被藏起来了? | 两条路径结论冲突,装作没看见 |
|
|
154
|
+
| 无答案时是否露怯? | 硬凑一个"综合"答案 |
|
|
155
|
+
|
|
156
|
+
## 陷阱
|
|
157
|
+
|
|
158
|
+
| 陷阱 | 正确做法 |
|
|
159
|
+
|------|---------|
|
|
160
|
+
| 把 L1 写成用户心情 | 写成具体后续动作 |
|
|
161
|
+
| L2 复述问题(废层) | L2 必须划边界:主观/客观 · 当下/长期 · 判断/操作 |
|
|
162
|
+
| L2 写"两者都有" | 每条轴必须落到一端,落不下去就作废这一支 |
|
|
163
|
+
| 所有层都填满还答错 | 填满是必要不充分;基准是父节点,不是自评 |
|
|
164
|
+
| 层层筛选只剩一条 | k 至少留 2 条到终点,否则退回"急于表达" |
|
|
165
|
+
| 同层候选无差距却留 1 | 无差距必须留 2;只有有明显差距时才能靠竞争留 1 |
|
|
166
|
+
| 为了填满而编候选 | 编不出来时写"原话不足以判断",这本身是结论 |
|
|
167
|
+
| 用"综合来看"合并多解 | 禁止。分歧就是分歧,列出来 |
|
|
168
|
+
| 无答案时硬凑 | 明确说"这题没有可验收标准" |
|
|
169
|
+
| 把骨架当形式走一遍 | 填完还是答第一个想到的 = 没走 |
|
|
170
|
+
|
|
171
|
+
## 边界(这套骨架不适用的时候)
|
|
172
|
+
|
|
173
|
+
- **问题本身有唯一正确答案时**(算术、编译):不需要分叉,直接答 + 验证。
|
|
174
|
+
- **用户明确要一个答案时**:给一个,别列 3 个让用户挑。
|
|
175
|
+
- **纯闲聊**:走这套会显得像官僚流程。
|
|
176
|
+
|
|
177
|
+
**本骨架真正的适用面:需求模糊、可能有多种合理解读、或根本没有确定答案的问题。**
|
|
178
|
+
|
|
179
|
+
## 成本
|
|
180
|
+
|
|
181
|
+
一次思考内完成,不是 n 次调用。**n 条线是同一个 forward pass 里的 n 个位置**,
|
|
182
|
+
上下文共享,token 只比直接回答多约 30%。不烧 token 的关键是**不来回**——
|
|
183
|
+
不在生成和外部判定之间反复起调用。
|
|
184
|
+
|
|
185
|
+
## 收尾:结论必须明确
|
|
186
|
+
|
|
187
|
+
答完必须让用户能判断:
|
|
188
|
+
|
|
189
|
+
1. **给了几个答案**(一条路径就一个,多条就列全)
|
|
190
|
+
2. **哪几条汇聚了**(置信较高处)
|
|
191
|
+
3. **哪几条分歧了**(需要用户定夺处)
|
|
192
|
+
4. **是否无答案**(若 L3 写不出,明确说)
|
|
193
|
+
|
|
194
|
+
不许用"综合来看建议……"把分歧抹平。
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* think-tree 自证器 —— 检查一次三层思考的填写质量。
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* node skill/abs-think-tree/check.js <填好的答案文件.md>
|
|
6
|
+
*
|
|
7
|
+
* 输入格式(答案文件):
|
|
8
|
+
* ## L1
|
|
9
|
+
* - [动作] 想快速定位原因
|
|
10
|
+
* - [动作] 想确认是不是自己改错了
|
|
11
|
+
* ## L2
|
|
12
|
+
* - [客观][当下][判断] 在问因果
|
|
13
|
+
* ## L3
|
|
14
|
+
* - [产出] 一个嫌疑点 + 证据 + 验证方法
|
|
15
|
+
*
|
|
16
|
+
* 各类标签是硬判据: 不写 = 报错, 不是"提醒"。
|
|
17
|
+
* 目的: 把"贴合用户原话"这句软话, 拆成能失败的检查。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const ACTION_VERBS = /(定位|确认|判断|决定|选择|选定|挑选|修改|查找|比较|评估|验证|交付|得到|知道|排除|复现|区分|排查|了解|收尾|继续|停止|给出|输出|回答|说明|解释|分析|检查|拆|推|排|定|学)/;
|
|
21
|
+
|
|
22
|
+
/** L2 三轴标签。必须三轴齐全, 缺一轴 = 边界没划全。 */
|
|
23
|
+
const AXES = ["主观|客观", "当下|长期", "判断|操作"];
|
|
24
|
+
|
|
25
|
+
/** L3 可验收产出类型的白名单。 */
|
|
26
|
+
const OUTPUT_KINDS = ["代码", "结论", "选项", "追问", "证据", "复现条件", "命令"];
|
|
27
|
+
|
|
28
|
+
function fail(list, msg) {
|
|
29
|
+
list.push(msg);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** L1: 每个候选必须是"动作", 不能是"心情"。 */
|
|
33
|
+
function checkL1(lines, errs) {
|
|
34
|
+
if (!lines.length) return fail(errs, "L1 为空: 至少写 1 个动机候选");
|
|
35
|
+
if (lines.length < 2) fail(errs, "L1 只有 1 个候选: 无兄弟则无法竞争");
|
|
36
|
+
lines.forEach((l, i) => {
|
|
37
|
+
const m = l.match(/^-\s*\[动作\]\s*(.+)$/);
|
|
38
|
+
if (!m) return fail(errs, `L1[${i}] 缺 [动作] 标签: "${l}"`);
|
|
39
|
+
const body = m[1].trim();
|
|
40
|
+
if (!body) return fail(errs, `L1[${i}] 标签后为空`);
|
|
41
|
+
if (!ACTION_VERBS.test(body))
|
|
42
|
+
fail(errs, `L1[${i}] 不像动作(无动词): "${body}"`);
|
|
43
|
+
// 心情词黑名单
|
|
44
|
+
if (/(了解一下|学习一下|感兴趣|好奇|随便看看)/.test(body))
|
|
45
|
+
fail(errs, `L1[${i}] 是心情不是动作: "${body}"`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** L2: 三轴必须齐全, 且每轴只能落一端。 */
|
|
50
|
+
function checkL2(lines, errs) {
|
|
51
|
+
if (!lines.length) return fail(errs, "L2 为空: 边界没划");
|
|
52
|
+
lines.forEach((l, i) => {
|
|
53
|
+
const m = l.match(/^-\s*((?:\[[^\]]+\])+)\s*(.+)$/);
|
|
54
|
+
if (!m) return fail(errs, `L2[${i}] 缺轴标签: "${l}"`);
|
|
55
|
+
const tags = [...m[1].matchAll(/\[([^\]]+)\]/g)].map((x) => x[1]);
|
|
56
|
+
AXES.forEach((ax) => {
|
|
57
|
+
const ends = ax.split("|");
|
|
58
|
+
const hit = tags.filter((t) => ends.includes(t));
|
|
59
|
+
if (!hit.length) fail(errs, `L2[${i}] 缺轴 [${ax}]: 边界没划全`);
|
|
60
|
+
if (hit.length > 1) fail(errs, `L2[${i}] 轴 [${ax}] 落了两端: 必须只落一端`);
|
|
61
|
+
});
|
|
62
|
+
if (/(两者都有|都算|不确定|看情况)/.test(m[2]))
|
|
63
|
+
fail(errs, `L2[${i}] 边界含糊(写了"两者都有"之类): "${m[2]}"`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** L3: 必须能指出可验收产出类型, 或明确露怯。 */
|
|
68
|
+
function checkL3(lines, errs) {
|
|
69
|
+
if (!lines.length) return fail(errs, "L3 为空: 既没给产出也没露怯");
|
|
70
|
+
lines.forEach((l, i) => {
|
|
71
|
+
// 露怯分支: 明确说没有可验收标准 —— 合法
|
|
72
|
+
if (/\[无验收标准\]/.test(l)) {
|
|
73
|
+
if (!/给(的)?是参考|不是答案|无法验收/.test(l))
|
|
74
|
+
fail(errs, `L3[${i}] 标了无验收标准但没明说给的是参考: "${l}"`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const m = l.match(/^-\s*\[产出\]\s*(.+)$/);
|
|
78
|
+
if (!m) return fail(errs, `L3[${i}] 缺 [产出] 或 [无验收标准] 标签: "${l}"`);
|
|
79
|
+
if (!OUTPUT_KINDS.some((k) => m[1].includes(k)))
|
|
80
|
+
fail(errs, `L3[${i}] 产出不可验收(不含白名单类型 ${OUTPUT_KINDS.join("/")}): "${m[1]}"`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** L2 是否逐条能对回 L1 的存活支 —— 需要显式写的 [父:N], 且不得越界。 */
|
|
85
|
+
function checkTrace(lines, l1count, errs) {
|
|
86
|
+
lines.forEach((l, i) => {
|
|
87
|
+
const m = l.match(/\[父:(\d+)\]/);
|
|
88
|
+
if (!m) return fail(errs, `L2[${i}] 缺 [父:N] 标注: 无法证明它来自哪个 L1 支`);
|
|
89
|
+
const n = Number(m[1]);
|
|
90
|
+
if (n >= l1count)
|
|
91
|
+
fail(errs, `L2[${i}] [父:${n}] 越界: L1 只有 ${l1count} 条, 指向不存在的父`);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function check(text) {
|
|
96
|
+
const errs = [];
|
|
97
|
+
const sec = { L1: [], L2: [], L3: [] };
|
|
98
|
+
let cur = null;
|
|
99
|
+
for (const raw of text.split("\n")) {
|
|
100
|
+
const h = raw.match(/^##\s*(L[123])\s*$/);
|
|
101
|
+
if (h) { cur = h[1]; continue; }
|
|
102
|
+
if (cur && raw.trim().startsWith("-")) sec[cur].push(raw.trim());
|
|
103
|
+
}
|
|
104
|
+
checkL1(sec.L1, errs);
|
|
105
|
+
checkL2(sec.L2, errs);
|
|
106
|
+
checkTrace(sec.L2, sec.L1.length, errs);
|
|
107
|
+
checkL3(sec.L3, errs);
|
|
108
|
+
return { ok: errs.length === 0, errors: errs, counts: { L1: sec.L1.length, L2: sec.L2.length, L3: sec.L3.length } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
112
|
+
const fs = await import("node:fs");
|
|
113
|
+
const p = process.argv[2];
|
|
114
|
+
if (!p) { console.log("用法: node check.js <答案文件.md>"); process.exit(2); }
|
|
115
|
+
const r = check(fs.readFileSync(p, "utf8"));
|
|
116
|
+
console.log(`L1=${r.counts.L1} L2=${r.counts.L2} L3=${r.counts.L3}`);
|
|
117
|
+
if (r.ok) console.log("PASS");
|
|
118
|
+
else { console.log(`FAIL (${r.errors.length})`); r.errors.forEach((e) => console.log(" ✗ " + e)); process.exit(1); }
|
|
119
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { check } from "./check.js";
|
|
4
|
+
|
|
5
|
+
/** 合规样本: 苹果选水果 */
|
|
6
|
+
const GOOD = `
|
|
7
|
+
## L1
|
|
8
|
+
- [动作] 想快速判断买哪个品种
|
|
9
|
+
- [动作] 想决定手上这个吃不吃
|
|
10
|
+
## L2
|
|
11
|
+
- [父:0][主观][当下][判断] 在问口味好不好
|
|
12
|
+
- [父:1][主观][当下][判断] 在问能不能吃
|
|
13
|
+
## L3
|
|
14
|
+
- [产出] 几个口味维度的横向结论
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
test("合规样本 PASS", () => {
|
|
18
|
+
const r = check(GOOD);
|
|
19
|
+
assert.ok(r.ok, r.errors.join("; "));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("L1 空泛失败: '想了解一下'", () => {
|
|
23
|
+
const r = check(`
|
|
24
|
+
## L1
|
|
25
|
+
- [动作] 想了解一下这个技术
|
|
26
|
+
- [动作] 想快速定位原因
|
|
27
|
+
## L2
|
|
28
|
+
- [父:1][主观][当下][判断] x
|
|
29
|
+
## L3
|
|
30
|
+
- [产出] 一个结论
|
|
31
|
+
`);
|
|
32
|
+
assert.ok(!r.ok);
|
|
33
|
+
assert.ok(r.errors.some((e) => /心情不是动作/.test(e)), r.errors.join(";"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("L1 只有一个候选失败: 无兄弟则无法竞争", () => {
|
|
37
|
+
const r = check(`
|
|
38
|
+
## L1
|
|
39
|
+
- [动作] 想快速定位原因
|
|
40
|
+
## L2
|
|
41
|
+
- [父:0][主观][当下][判断] x
|
|
42
|
+
## L3
|
|
43
|
+
- [产出] 一个结论
|
|
44
|
+
`);
|
|
45
|
+
assert.ok(r.errors.some((e) => /只有 1 个候选/.test(e)), r.errors.join(";"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("L2 缺轴失败: 边界没划全", () => {
|
|
49
|
+
const r = check(`
|
|
50
|
+
## L1
|
|
51
|
+
- [动作] 想快速定位原因
|
|
52
|
+
- [动作] 想确认改动影响
|
|
53
|
+
## L2
|
|
54
|
+
- [父:0][主观][当下] x
|
|
55
|
+
## L3
|
|
56
|
+
- [产出] 一个结论
|
|
57
|
+
`);
|
|
58
|
+
assert.ok(r.errors.some((e) => /缺轴 \[判断\|操作\]/.test(e)), r.errors.join(";"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("L2 一轴落两端失败", () => {
|
|
62
|
+
const r = check(`
|
|
63
|
+
## L1
|
|
64
|
+
- [动作] 想快速定位原因
|
|
65
|
+
- [动作] 想确认改动影响
|
|
66
|
+
## L2
|
|
67
|
+
- [父:0][主观][客观][当下][判断] x
|
|
68
|
+
## L3
|
|
69
|
+
- [产出] 一个结论
|
|
70
|
+
`);
|
|
71
|
+
assert.ok(r.errors.some((e) => /落了两端/.test(e)), r.errors.join(";"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("L2 无父标注失败: 追溯不到来源", () => {
|
|
75
|
+
const r = check(`
|
|
76
|
+
## L1
|
|
77
|
+
- [动作] 想快速定位原因
|
|
78
|
+
- [动作] 想确认改动影响
|
|
79
|
+
## L2
|
|
80
|
+
- [主观][当下][判断] x
|
|
81
|
+
## L3
|
|
82
|
+
- [产出] 一个结论
|
|
83
|
+
`);
|
|
84
|
+
assert.ok(r.errors.some((e) => /缺 \[父:N\]/.test(e)), r.errors.join(";"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("L3 不可验收失败: '一些建议'", () => {
|
|
88
|
+
const r = check(`
|
|
89
|
+
## L1
|
|
90
|
+
- [动作] 想快速定位原因
|
|
91
|
+
- [动作] 想确认改动影响
|
|
92
|
+
## L2
|
|
93
|
+
- [父:0][主观][当下][判断] x
|
|
94
|
+
## L3
|
|
95
|
+
- [产出] 一些建议
|
|
96
|
+
`);
|
|
97
|
+
assert.ok(r.errors.some((e) => /不可验收/.test(e)), r.errors.join(";"));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("L3 露怯合法: 无验收标准 + 明说是参考", () => {
|
|
101
|
+
const r = check(`
|
|
102
|
+
## L1
|
|
103
|
+
- [动作] 想快速定位原因
|
|
104
|
+
- [动作] 想确认改动影响
|
|
105
|
+
## L2
|
|
106
|
+
- [父:0][主观][当下][判断] x
|
|
107
|
+
## L3
|
|
108
|
+
- [无验收标准] 此题给的是参考, 不是答案
|
|
109
|
+
`);
|
|
110
|
+
assert.ok(r.ok, r.errors.join(";"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("L3 标了露怯但没说'是参考' -> 失败", () => {
|
|
114
|
+
const r = check(`
|
|
115
|
+
## L1
|
|
116
|
+
- [动作] 想快速定位原因
|
|
117
|
+
- [动作] 想确认改动影响
|
|
118
|
+
## L2
|
|
119
|
+
- [父:0][主观][当下][判断] x
|
|
120
|
+
## L3
|
|
121
|
+
- [无验收标准] 这题很难
|
|
122
|
+
`);
|
|
123
|
+
assert.ok(!r.ok);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 核心用例: 编造动机。
|
|
128
|
+
* 骨架的已知边界是"拦不住 L1 编一个说得通的动机"。
|
|
129
|
+
* 这里试: 一个句子里没有任何线索, 却写了具体动作 —— 检查器能抓到吗?
|
|
130
|
+
*/
|
|
131
|
+
test("边界用例: 编造的动机能通过全部格式检查 —— 这是检查器的能力边界", () => {
|
|
132
|
+
// 用户只说"你觉得呢", 原话里没有任何动作线索。
|
|
133
|
+
// 但编出带动词的动机很容易 —— 动词白名单挡不住编造。
|
|
134
|
+
const fabricated = `
|
|
135
|
+
## L1
|
|
136
|
+
- [动作] 想确认我的判断是否可靠
|
|
137
|
+
- [动作] 想知道我是否认真思考过
|
|
138
|
+
## L2
|
|
139
|
+
- [父:0][主观][当下][判断] 在问结论
|
|
140
|
+
- [父:1][主观][当下][判断] 在问过程
|
|
141
|
+
## L3
|
|
142
|
+
- [产出] 一个明确结论
|
|
143
|
+
`;
|
|
144
|
+
const r = check(fabricated);
|
|
145
|
+
// 断言: 它能通过 —— 这是能力边界, 必须被记录, 不能被假装解决
|
|
146
|
+
assert.ok(r.ok, "格式检查无法发现'动机是编的': " + r.errors.join(";"));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("[父:N] 越界能被发现 (曾漏, 已修)", () => {
|
|
150
|
+
const r = check(`
|
|
151
|
+
## L1
|
|
152
|
+
- [动作] 想快速定位原因
|
|
153
|
+
- [动作] 想确认改动影响
|
|
154
|
+
## L2
|
|
155
|
+
- [父:9][主观][当下][判断] x
|
|
156
|
+
## L3
|
|
157
|
+
- [产出] 一个结论
|
|
158
|
+
`);
|
|
159
|
+
assert.ok(!r.ok, "应发现越界");
|
|
160
|
+
assert.ok(r.errors.some((e) => /越界/.test(e)), r.errors.join(";"));
|
|
161
|
+
});
|
package/src/store.js
CHANGED
|
@@ -778,17 +778,13 @@ export async function cmdTeardownCheck({ dir, payload }) {
|
|
|
778
778
|
await fs.writeFile(mark, stamp).catch(() => {});
|
|
779
779
|
|
|
780
780
|
const msg = [
|
|
781
|
-
// 未设姓名时把设置指令插到第0条 —— 否则后续 todo add/log/note
|
|
782
|
-
// 而收尾提醒本身不提这事,使用者只会看到一连串报错。
|
|
781
|
+
// 未设姓名时把设置指令插到第0条 —— 否则后续 todo add/log/note 全会被守卫拦下。
|
|
783
782
|
(await getUser() ? [] : [
|
|
784
|
-
'0) 本机尚未设置使用者姓名 —— 先跑 abs config set user
|
|
783
|
+
'0) 本机尚未设置使用者姓名 —— 先跑 abs config set user <你的名字>,否则 todo/log/note 都会被拦下;',
|
|
785
784
|
]),
|
|
786
|
-
'[abs
|
|
787
|
-
'
|
|
788
|
-
'
|
|
789
|
-
'3) 值得留的经验 abs note "..."(宁少勿滥,能从代码 grep 到的不记);',
|
|
790
|
-
'4) abs log "完成 X:..." 记一行工作成果,新页同步进 index。',
|
|
791
|
-
'简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句"无可沉淀"即可。',
|
|
785
|
+
'[abs] 本会话改过文件,.brain/ 今日无记录。',
|
|
786
|
+
'这条是信息不是命令:该沉淀就沉淀,没有可沉淀的直接回一句「无可沉淀」,不用凑。',
|
|
787
|
+
'需要时:abs todo / abs todo done <id> / abs note "..." / abs log "..."',
|
|
792
788
|
].join('\n');
|
|
793
789
|
|
|
794
790
|
// Claude Code Stop hook 契约: {"decision":"block","reason":"..."} = 阻止结束并把 reason 回灌给 agent
|