@fanchao8609/agent_brain_sync 1.8.1 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
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),每轮结束走收尾循环(读todo→判未登记→沉淀→更新index/log)。解决会话无状态:经验/进度/坑碎片化、重开失忆。
3
+ description: abs (agent-brain-sync) 跨会话 AI 编码记忆与任务续接。开场续接状态(abs load/MCP abs_load),干活中任务/经验实时落盘(abs_task/abs_note),每轮结束走收尾循环(读todo→判未登记→沉淀→更新index/log)。解决会话无状态:经验/进度/坑碎片化、重开失忆。遇 bug 排查时配合挂载 bug-hunter skill。
4
4
  ---
5
5
 
6
6
  # abs — 跨会话记忆 (agent-brain-sync)
@@ -313,3 +313,6 @@ abs supersede <页名> --by <取代它的新页> # 不写 --by 也行 = 单纯
313
313
  - 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外)。
314
314
  - 只写真实发生的事实;遵守容量纪律,宁缺毋滥。
315
315
  - 双链/frontmatter/index 必须自洽 —— 坏链 = 掰断接力棒。
316
+ - **排查 bug 时挂载 `bug-hunter` skill**(随 abs 一并安装,在 `skills/bug-hunter/`):
317
+ 完整排查流程(列假设 → 复现 → 定位改动 → 打印 → 顺藤摸瓜 → 根因 → 验证)
318
+ 与「给结论要明确」都在那份里。**排查方法不写在本文件**(两份维护必漂)。
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: bug-hunter
3
+ description: 按流程论排查 bug,先定位改动 → 日志打印 → 顺藤摸瓜逐层排查
4
+ ---
5
+
6
+ # Bug 排查方法论
7
+
8
+ ## 核心原则
9
+
10
+ **Bug 多数由新增或修改的功能引起。不凭空猜测,用数据说话。但数据不是终点——先复现,再定位,再理解根因。**
11
+
12
+ 常见的失败模式是把"能复现的问题"当成"已定位的 bug",跳过理解直接下结论。定位到错误位置 ≠ 找到根因。改对症状而不改根因,换一批数据又会复发。
13
+
14
+ ## 排查流程
15
+
16
+ ### 第 -1 步:列假设 —— 方向错了怎么办(不能跳过)
17
+
18
+ **触发**:用户报了症状(“打不开”“报错了”“不对”),但没给一手错误信息。
19
+
20
+ **最大坑(真实案例)**:只有一个假设时,它要么被证实要么思路断掉 —— 于是**自己造一个输入**去测:
21
+ 编了个接口名 `app.init` → curl 测出 404 → “我测出来的,是事实” → 推出“nginx 没配 PATH_INFO”
22
+ → 用户纠正后**重跑同一实验、得到同一 404** → 坚信自己没错。
23
+
24
+ > 假证据最毒之处:**下游全程都是真的**(404 真、命令真跑过)。唯一假的那环
25
+ > (输入名)在最上游,且已消失在过程里 → 所以“再验证一次”救不了,
26
+ > 他会重跑同假输入得同真输出。
27
+ > 下面第 0 步“先复现”在拿不到报错时反而会逼出造假 —— 所以本步在它之前。
28
+
29
+ 1. **开局列 ≥2 个假设**(代码层 / 配置层 / 构建产物 / 缓存 / 第三方后台)。
30
+ 只列一个 = 逼自己去证实它 = 造证据的动力。
31
+ 2. **每个假设配反证条件**:什么现象出现就认它错。
32
+ > 例:“假设 nginx 没配 PATH_INFO;反证条件:用**从代码里读到的真实接口名**
33
+ > 测出也是 404 → 接口本身不存在,与 nginx 无关 → 弃此假设。”
34
+ > 反证条件里写明“真实接口名”,`app.init` 这种编造的就自动不能用了。
35
+ 3. **卡住两轮就换假设**,不是在同一假设上加细节。
36
+ 4. **领域外的先声明无取证能力**(构建产物在不在 / 缓存 / 微信等第三方后台行为),
37
+ 要用户提供事实,**不要脑补**。
38
+
39
+ **结论所依据的每个具体名字(接口/字段/报错文本/路径)必须指得出处(文件:行)。**
40
+ 指不出的,只能标“推测”二字,**不得当证据用**。
41
+
42
+ > **为什么多假设能防造假**:单假设必须被证实,否则思路断 → 有动机造。
43
+ > 多假设下,“支持了 A” 不等于 “就是 A”(B/C/D 还没排除)→ **假证据推不出结论,造它就没意义了**。
44
+
45
+ ### 第 0 步:复现 — 先让 Bug 稳定出现
46
+
47
+ 没有稳定复现,后面全是猜。先确定:
48
+ - 复现的必要条件是什么?哪个输入/操作/环境触发?
49
+ - 是 100% 复现,还是偶发?偶发问题先找触发模式(特定数据、特定时机、特定顺序)。
50
+ - 日志里有没有现成的错误栈?错误信息 + 行号是最便宜的定位入口。
51
+
52
+ **给 Bug 起个名字**(一句话描述 + 触发条件)。排查卡住时,回去核对是否还在同一个 Bug 上。
53
+
54
+ ### 第一步:定位范围 — 改动了什么
55
+
56
+ Bug 不会凭空出现。先查最近的代码改动:
57
+
58
+ ```bash
59
+ git diff HEAD~3 --stat # 看改了哪些文件
60
+ git log --oneline -5 # 看最近提交
61
+ git diff <commit> -- <file> # 看具体改了什么
62
+ git log -p -S '可疑字段/函数名' # 谁改过这段代码
63
+ git blame <file> -L <行号,行号> # 定位到具体某行是谁写的
64
+ ```
65
+
66
+ 问自己:
67
+ - 这次新增/修改了什么功能?
68
+ - 改动涉及哪些文件?
69
+ - 哪个改动最可能影响到出问题的地方?
70
+
71
+ **不要排查无关代码,把精力集中在改动范围。**
72
+
73
+ ### 第二步:日志打印 — 让数据说话
74
+
75
+ 不要猜,打印出来看。根据技术栈选择打印方式:
76
+
77
+ | 技术栈 | 打印方式 | 日志位置 |
78
+ |--------|----------|----------|
79
+ | PHP (ThinkPHP) | `\think\Log::error($data)` 或 `trace($data)` | `runtime/log/` |
80
+ | PHP (Laravel) | `Log::info($data)` 或 `logger($data)` | `storage/logs/` |
81
+ | Vue / JS | `console.log(data)` | 浏览器控制台 |
82
+ | Node.js | `console.log(data)` 或 `logger.debug(data)` | 终端 / 日志文件 |
83
+ | SQL | `\think\Db::getLastSql()` 或开启 SQL 日志 | runtime/log 或控制台 |
84
+
85
+ **打印要打印关键数据,不要只打印 "到了这里",要打印实际的变量值。**
86
+
87
+ ```php
88
+ // ❌ 没用
89
+ \Log::error('进入方法');
90
+
91
+ // ✅ 有用 — 打印入参、中间态、出参
92
+ \Log::error('goods detail', ['id' => $id, 'goods' => $goods, 'type' => gettype($goods)]);
93
+ ```
94
+
95
+ **二分法打印**:在可疑链路上隔几层就插一个日志点,先跑一遍看哪段有数据、哪段没数据,把范围砍半,再往里面加日志。比一次打满所有层更快收敛。
96
+
97
+ **加分技巧**:
98
+ - 给每个日志点带唯一前缀(如 `[BUG-01]`、`[BUG-02]`),日志里 grep 一次就能看到全链路顺序。
99
+ - 打印耗时和调用来源:怀疑并发/时序时,打上时间戳与 `debug_backtrace()` 或请求 ID。
100
+
101
+ ### 第三步:顺藤摸瓜 — 按调用链逐层排查
102
+
103
+ 从用户操作出发,沿着调用链一层层往下查:
104
+
105
+ ```
106
+ 用户操作(点击/请求)
107
+ → 路由(routes / pages.json / url)
108
+ → 门面/中间件(facade / middleware)
109
+ → 控制器(controller)
110
+ → 服务层(service)
111
+ → 模型(model)
112
+ → 数据库(SQL)
113
+ ```
114
+
115
+ 每一层都打印关键数据,确认数据在哪一层开始出错:
116
+
117
+ ```php
118
+ // 控制器 — 打印接收到的参数
119
+ \Log::error('controller input', ['params' => $params]);
120
+
121
+ // 服务层 — 打印处理中的数据
122
+ \Log::error('service process', ['data' => $data, 'result' => $result]);
123
+
124
+ // 模型 — 打印查询结果
125
+ \Log::error('model query', ['sql' => $this->getLastSql(), 'result' => $result]);
126
+ ```
127
+
128
+ **找到数据从正确变为错误的那一层,bug 就在那一层。**
129
+
130
+ ### 第四步:找到根因,不只修症状
131
+
132
+ 改对症状 = 换一批数据又复发。修复前回答:
133
+ - 为什么数据在这一层变错了?是**逻辑错误**(判断写反/字段取错),**类型错误**(null/数组/对象混用),还是**数据本身脏**(上游写入时就错了)?
134
+ - 根因若在上游(写入方/历史数据),这里 patch 只是挡一下,应该去修上游或在入口统一兜底。
135
+
136
+ **修一层,不改所有调用点**:如果多个地方都调用同一函数,只在出问题的那个调用点打补丁,其他调用点照样坏。在共享函数里修一次,是所有调用点的最小修复。
137
+
138
+ ### 第五步:验证修复 — 逐行打印确认
139
+
140
+ 修复 bug 后,不要凭空验证。**先拿第 0 步的复现条件重跑**,确认错误消失、结果正确,再用打印验证每一步符合预期:
141
+
142
+ ```php
143
+ // 修复后验证
144
+ \Log::error('step1: fetch', ['data' => $data]);
145
+ $data = transform($data);
146
+ \Log::error('step2: transform', ['data' => $data]);
147
+ $result = save($data);
148
+ \Log::error('step3: save', ['result' => $result]);
149
+ ```
150
+
151
+ 验证包括三层:
152
+ - **修好了**:复现路径不再报错,输出正确。
153
+ - **没修坏**:相关正常路径仍正常(回归)。
154
+ - **边界还在**:尝试边缘输入(空值、超大值、并发、重复提交),确认修复没引入新洞。
155
+
156
+ 确认无误后,再删除调试日志。
157
+
158
+ ## 实战示例
159
+
160
+ **场景:** 商品详情页报 `Call to a member function toArray() on array`
161
+
162
+ **第 0 步:复现**
163
+ - 访问 `/shopro/goods/goods/detail/id/31` 稳定复现
164
+ - 日志:`[error] 致命错误: Call to a member function toArray() on array [/var/www/html/.../GoodsMemberPrice.php:50]`
165
+
166
+ **第一步:定位范围**
167
+ - 最近新增了会员系统,改了 Goods 控制器和新增了 GoodsMemberPrice 模型
168
+ - 怀疑新增代码有问题
169
+
170
+ **第二步:查看日志**
171
+ - 日志直接告诉出错文件和行号
172
+
173
+ **第三步:顺藤摸瓜**
174
+ - 路由:`/shopro/goods/goods/detail/id/31`
175
+ - 控制器:`Goods::detail()` → 调用了 `GoodsMemberPriceModel::getByGoods()`
176
+ - 模型:`getByGoods()` 第50行 `select()->toArray()` → `select()` 返回数组,不是 Collection
177
+
178
+ **第四步:找根因**
179
+ - 为什么 id=31 出错而别的正常?查数据:31 这件商品有会员价记录 → 触发 `select()` 返回多行场景。可能是 `find()`/`select()` 返回值混用的历史问题。若只是个别记录脏,应清理数据或给方法加统一返回类型。
180
+
181
+ **第五步:修复验证**
182
+ ```php
183
+ // 修复前
184
+ return self::where('goods_id', $goodsId)->select()->toArray();
185
+
186
+ // 修复后
187
+ $result = self::where('goods_id', $goodsId)->select();
188
+ return $result instanceof \think\Collection ? $result->toArray() : (array)$result;
189
+ ```
190
+ - 重跑 id=31(不再报错)+ 跑一个无会员价记录的 id(确认没回归)
191
+
192
+ ## 常见陷阱
193
+
194
+ | 陷阱 | 正确做法 |
195
+ |------|----------|
196
+ | 凭经验猜测 bug 位置 | 先复现 + 看日志/打印,用数据定位 |
197
+ | 只看代码不运行 | 打印实际运行时的数据 |
198
+ | 一次改很多地方再测试 | 改一处、验证一处 |
199
+ | 修症状不修根因 | 追问"为什么这层数据变了",确认根因 |
200
+ | 只验证出错的路径 | 重跑复现 + 回归正常路径 + 边界输入 |
201
+ | 忘记删除调试代码 | 修复确认后清理所有 `console.log` / `\Log::error` |
202
+ | 只在本地验证 | 确认服务器代码是否同步部署,数据是否一致 |
203
+ | 排查卡住还硬扛 | 回去核对第 0 步的 Bug 描述,确认是否在同一个问题上;必要时换数据/换触发角度重看 |
204
+ | 偶发当成必然 | 偶发先找触发模式,别用单个样本下结论 |
205
+
206
+ ## 兜底:卡住时做什么
207
+
208
+ 排查 >20 分钟没进展,不硬扛,回到流程检查:
209
+ 1. **Bug 描述还准吗?** 是否有新的观察改变了问题定义。
210
+ 2. **复现还稳定吗?** 换一个触发样本还出现吗。
211
+ 3. **有没有漏看的日志?** grep 全量日志(不只最近的),可能早期就报过错。
212
+ 4. **改动范围查全了吗?** 只看 HEAD~3 可能漏了分支合并、配置文件、部署差异。
213
+ 5. **要不要跟同事确认?** 这条功能最近谁改的、改动意图是什么,可能一句话点醒。
214
+ 6. **向上游看一层。** 数据/配置来源方是否也变了,不只是消费方的问题。
215
+
216
+ ## 收尾:给结论要明确,不要模糊
217
+
218
+ **不许**:"可能是 X,也可能是 Y,建议排查一下" —— 这是把判断推回给用户。
219
+
220
+ **必须**:明确给出
221
+ 1. **当前结论**(是什么 / 或者"未定位")
222
+ 2. **置信度与依据**(哪来的证据 / 还是只是推测)
223
+ 3. **下一步具体动作**(跑什么命令、看什么输出、要用户提供什么)
224
+
225
+ "未定位"是合法输出,但必须配一句"**需要你提供 X**"(具体到要什么都),
226
+ 不能只说"无法确定"就结束。
227
+
228
+ > 与第 -1 步同一根因:模糊化是逃避判断。要么给结论 + 依据,要么明确索取证据。
package/src/install.js CHANGED
@@ -16,6 +16,28 @@ const ABS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..');
16
16
  const HOOK_TEMPLATE = join(ABS_DIR, 'hooks', 'event.sh');
17
17
  const SKILL_SOURCE = join(ABS_DIR, 'skill', 'SKILL.md');
18
18
 
19
+ // 随 abs 一并安装的附带 skill(agent 排查 bug 时挂载)。
20
+ // 必须放在本包内随包发布 —— 不能假设宿主已装:实测 co​dex 只有 abs 自己装的 skill,
21
+ // 若引用外部 skill 会成悬空。ponytail: 只加这一个,需要更多时改成扫 skill/ 子目录。
22
+ const BUNDLED_SKILLS = [
23
+ { name: 'bug-hunter', src: join(ABS_DIR, 'skill', 'bug-hunter', 'SKILL.md') },
24
+ ];
25
+
26
+ /** 安装主 skill + 全部附带 skill 到该宿主。收口在此:四处安装点共用,
27
+ * 新增附带 skill 时只改本函数(避免「改一份不算改」)。返回步骤行。 */
28
+ async function installSkills(agentKey) {
29
+ const steps = [];
30
+ const base = hostSkillDir(agentKey);
31
+ await atomicWrite(join(base, 'SKILL.md'), await fs.readFile(SKILL_SOURCE, 'utf8'));
32
+ steps.push(`✓ skill → ${join(base, 'SKILL.md')}`);
33
+ for (const b of BUNDLED_SKILLS) {
34
+ const t = join(base, '..', b.name, 'SKILL.md');
35
+ await atomicWrite(t, await fs.readFile(b.src, 'utf8'));
36
+ steps.push(`✓ skill → ${t}`);
37
+ }
38
+ return steps;
39
+ }
40
+
19
41
  /**
20
42
  * 本包的稳定入口路径解析(mcp.js / abs.js 通用)。
21
43
  *
@@ -455,9 +477,7 @@ async function installClaudeCode({ withMcp, withSkill, log }) {
455
477
 
456
478
  // 3) skill → <configRoot>/skills/abs-agent-brain-sync/SKILL.md (config 根 = CLAUDE_CONFIG_DIR)
457
479
  if (withSkill) {
458
- const target = join(hostSkillDir('claude-code'), 'SKILL.md');
459
- await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
460
- steps.push(`✓ skill → ${target}`);
480
+ steps.push(...await installSkills('claude-code'));
461
481
  }
462
482
  return steps;
463
483
  }
@@ -494,7 +514,8 @@ async function uninstallClaudeCode() {
494
514
  // skill
495
515
  const skillDir = hostSkillDir('claude-code');
496
516
  await fs.rm(skillDir, { recursive: true, force: true });
497
- steps.push(`✓ skill 已删除`);
517
+ for (const b of BUNDLED_SKILLS) await fs.rm(join(skillDir, '..', b.name), { recursive: true, force: true });
518
+ steps.push(`✓ skill 已删除(含附带: ${BUNDLED_SKILLS.map((x) => x.name).join(", ")})`);
498
519
  return steps;
499
520
  }
500
521
 
@@ -574,9 +595,7 @@ async function installCodex({ withMcp, withSkill, log }) {
574
595
  }
575
596
  }
576
597
  if (withSkill) {
577
- const target = join(hostSkillDir('codex'), 'SKILL.md');
578
- await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
579
- steps.push(`✓ skill → ${target}`);
598
+ steps.push(...await installSkills('codex'));
580
599
  }
581
600
  return steps;
582
601
  }
@@ -726,9 +745,7 @@ async function installOpenCode({ withMcp, withSkill, log }) {
726
745
  steps.push(`✓ MCP → ${mcpP} (mcp.abs local)`);
727
746
  }
728
747
  if (withSkill) {
729
- const target = join(hostSkillDir('opencode'), 'SKILL.md');
730
- await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
731
- steps.push(`✓ skill → ${target}`);
748
+ steps.push(...await installSkills('opencode'));
732
749
  }
733
750
  return steps;
734
751
  }
@@ -775,9 +792,7 @@ async function installPi({ withMcp, withSkill, log }) {
775
792
  steps.push(`✓ MCP → ${mcpP} (mcpServers.abs, stdio)`);
776
793
  }
777
794
  if (withSkill) {
778
- const target = join(hostSkillDir('pi'), 'SKILL.md');
779
- await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
780
- steps.push(`✓ skill → ${target}`);
795
+ steps.push(...await installSkills('pi'));
781
796
  }
782
797
  return steps;
783
798
  }