@fanchao8609/agent_brain_sync 1.15.0 → 1.15.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/bin/abs.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // bin/abs.js — abs CLI 入口。
3
3
  // abs <cmd> [args]
4
4
  // 命令: init / board / status / load / task / install / uninstall / help
5
- import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdShow, cmdRepair, cmdWrapup, cmdRule, cmdTeardownCheck, cmdTodoArchive, cmdResolve, cmdSupersede, cmdReview, cmdAutoTask, cmdAutoTaskSweep } from '../src/store.js';
5
+ import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdShow, cmdRepair, cmdWrapup, cmdRule, cmdTeardownCheck, cmdTodoArchive, cmdResolve, cmdSupersede, cmdReview } from '../src/store.js';
6
6
  import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
7
7
  import { runInstall, runUninstall } from '../src/install.js';
8
8
  import { readFileSync } from 'node:fs';
@@ -92,10 +92,6 @@ const FLAG_SPEC = {
92
92
  // ★ 漏声明就是真 bug(2026-10-05 本机实测):不在 FLAG_SPEC 的选项会被静默丢掉,
93
93
  // 于是 `abs log "x" --kind note` 全写成默认的 dev,而 cmdLog 的枚举校验看不见它。
94
94
  'kind': { type: 'string' },
95
- // auto-task 用(hook 调):--session 作幂等键(一会话一条),--prompt 抽任务关键词。
96
- // ★ 同上:不在这声明就会被 parseArgv 静默丢掉,登记变成"永远首次"、闸门形同虚设。
97
- 'session': { type: 'string' },
98
- 'prompt': { type: 'string' },
99
95
  'by': { type: 'string' },
100
96
  'all': { type: 'boolean' },
101
97
  'keep-days': { type: 'string' },
@@ -533,16 +529,7 @@ async function main() {
533
529
  // 内部命令(hook 专用,不出现在 help):Stop 时机械快照未完成任务
534
530
  case 'wrapup': {
535
531
  rejectExtra(opts._, 'abs wrapup(内部命令, 供 hook 调用)');
536
- const r = await cmdWrapup({ dir: opts.dir });
537
- // 收尾时顺带清理 hook 自动登记的条目(它们不代表人工接管的成果)
538
- const swept = await cmdAutoTaskSweep({ dir: opts.dir }).catch(() => null);
539
- console.log([r, swept].filter(Boolean).join('\n') || '(无滞留任务)');
540
- break;
541
- }
542
- // hook 自动登记(pi/opencode 侧调): 一会话一条,同会话只更新断点
543
- case 'auto-task': {
544
- const r = await cmdAutoTask({ dir: opts.dir, session: opts.session, prompt: opts.prompt, note: opts.note });
545
- if (r) console.log(r); // 无事发生则静默(不制造空动作噪音)
532
+ console.log(await cmdWrapup({ dir: opts.dir }));
546
533
  break;
547
534
  }
548
535
  // Stop hook 用: 判定是否注入收尾指令。stdout 给 shell (`push:<json>` / `{}`), 无额外输出。
package/hooks/abs.pi.ts CHANGED
@@ -44,19 +44,6 @@ function sessionIdFor(ctx: any): string {
44
44
  } catch { return '' }
45
45
  }
46
46
 
47
- /** 调 abs auto-task(hook 自动登记)。detached fire-and-forget,不阻塞 turn。
48
- *
49
- * 为何走 spawn 而非 import:与 snapshotWrapup 同源 —— 扩展进程里不能阻塞用户 turn,
50
- * 且 abs 的写入要经过它自己的文件锁(跨进程锁才有效)。 */
51
- function spawnAutoTask(cwd: string, args: string[]): void {
52
- try {
53
- const child = spawn(process.execPath, [ABS_BIN, "auto-task", ...args], {
54
- cwd: cwd || process.cwd(), stdio: "ignore", detached: true,
55
- })
56
- child.unref()
57
- } catch {} // fire-and-forget: 登记失败不得影响宿主
58
- }
59
-
60
47
  /** 会改文件的工具名。用于判定「真的开始干活了」—— 排查类会话先跑一堆读命令,
61
48
  * 若用「第一次 tool_call」当判据,断点会被写在与任务无关的文件上。
62
49
  *
@@ -112,13 +99,6 @@ let pendingTodoTool = false
112
99
  * 只记第一次写文件,不是每次都记:断点是「记到哪」不是流水账。 */
113
100
  let autoTaskSeen = false
114
101
 
115
- /** 本会话最后被写到的文件(agent_end 落断点用)。
116
- * 只存文件名,不存内容 —— 断点是"记到哪",不是快照。 */
117
- let lastTouchedFile = ''
118
-
119
- /** 最近一轮的用户指令原文。只用于「第一次真改文件时」抽任务关键词 ——
120
- * 登记时机已从 before_agent_start 挪到 tool_call(用户实报:一按回车就登记太早)。 */
121
- let pendingPrompt = ''
122
102
 
123
103
  /** 最近一次渲染拿到的 tui 引用。hidePanel 用它请求重绘 —— 面板消失时动画
124
104
  * 定时器已停,没有别的重绘通道(见 hidePanel 的坑注解)。 */
@@ -626,8 +606,6 @@ export default function absPiHook(pi: ExtensionAPI): void {
626
606
  pi.on("session_start", (event: any, ctx: any) => {
627
607
  // 新会话 = 重新登记:不重置则同进程的第二个会话永远不再落断点
628
608
  autoTaskSeen = false
629
- lastTouchedFile = ''
630
- pendingPrompt = ''
631
609
  resetThrottle()
632
610
  // 本会话的随机昵称 —— 在这里抽一次(不是每帧抽,否则面板会疯狂闪)。
633
611
  // 每次 session_start(startup/reload/new/resume/fork)重抽 → “每次打开 pi 都是随机的”。
@@ -665,21 +643,11 @@ export default function absPiHook(pi: ExtensionAPI): void {
665
643
  }
666
644
  // 自动登记断点(2026-10-05):真的动文件时,把「改到哪个」记进本会话的自动任务。
667
645
  // 为何不用「第一次 tool_call」:排查类会话先跑一堆读命令,断点会落在无关文件上。
646
+ // 写类工具只留一行痕(可观测),不 spawn、不写看板 —— 见上面的删除说明。
668
647
  if (!autoTaskSeen && isWriteCall(name, input)) {
669
648
  autoTaskSeen = true
670
- const cwd = (ctx && ctx.cwd) || process.cwd()
671
- const file = String(input?.path ?? input?.file_path ?? input?.filePath ?? '')
672
- const bp = file ? `改了 ${file.slice(0, 120)}` : '开始改文件'
673
- if (file) lastTouchedFile = file.slice(0, 120)
674
649
  const sid = sessionIdFor(ctx)
675
- // 首次真改文件 = "真开工了" → 此刻才登记(带用户指令抽关键词)+ 落断点。
676
- // 这是唯一登记入口:光说/说问/讨论都不登记(2026-10-05 用户实报时机太早)。
677
- if (sid) {
678
- const args = ['--session', sid, '--note', bp]
679
- if (pendingPrompt) args.push('--prompt', pendingPrompt)
680
- spawnAutoTask(cwd, args)
681
- }
682
- logHook(`tool_call auto_task name=${name} sid=${sid || '-'}`).catch(() => {})
650
+ logHook(`tool_call first_write name=${name} sid=${sid || '-'}`).catch(() => {})
683
651
  }
684
652
  })
685
653
 
@@ -711,10 +679,11 @@ export default function absPiHook(pi: ExtensionAPI): void {
711
679
  const ok = injectTodoGuidelines(event?.systemPromptOptions)
712
680
  logHook(`before_agent_start todo_guide=${ok ? 'on' : 'off'} cwd=${cwd}`).catch(() => {})
713
681
 
714
- // 只【记住】本轮用户指令,不在这里登记(2026-10-05 用户实报时机太早)。
715
- // 旧行为: 一按回车就登记 → 用户还在说/聊、需求没成型,看板上就已经开出了任务。
716
- // 现在: 留到第一次真改文件时(tool_call)才登记,那时才算"真开工了"。
717
- if (event?.prompt) pendingPrompt = String(event.prompt).slice(0, 200)
682
+ // 自动登记已整体移除(2026-10-05 用户实报两点):
683
+ // ① 卡顿 —— agent_end 每轮 spawn 一个 node 进程
684
+ // ② 看板冒出大量 `auto-TBD-xxx — 待命名` 占位
685
+ // 结论: hook 只留【可观测痕迹】,不写看板、不起进程。登记回到 agent 主动调 abs task。
686
+ // 教训见 concept auto-todo-register-design 的"为什么最后删掉自动登记"一节。
718
687
  } catch (e: any) {
719
688
  logHook(`before_agent_start error=${e?.message || 'unknown'}`).catch(() => {})
720
689
  }
@@ -736,16 +705,9 @@ export default function absPiHook(pi: ExtensionAPI): void {
736
705
  agentEndSeen = true
737
706
  await logHook(`agent_end:seen cwd=${cwd} brain=${brain || 'none'}`).catch(() => {})
738
707
  }
739
- // 每轮结束落一次断点(2026-10-05): 本会话若真动过文件, 把"干到哪"推进到最新。
740
- // 为何只在动过文件后: 纯对话/查询轮不需要推进断点, 否则白 spawn 一堆进程。
741
- // 为何不放在 session_shutdown: 那是进程退出, 而"一轮干完"才是自然的落盘点 ——
742
- // 用户可能一直开着 pi 跨很多轮, 断点必须活着的时候就已经是最新的。
743
- if (!brain || !autoTaskSeen) return
744
- const sid = sessionIdFor(ctx)
745
- if (!sid) return
746
- const last = lastTouchedFile
747
- spawnAutoTask(cwd, ['--session', sid, '--note', last ? `本轮到 ${last}` : '本轮有改动'])
748
- logHook(`agent_end auto_task breakpoint sid=${sid}`).catch(() => {})
708
+ // 每轮 spawn 已删除(2026-10-05 用户实报"卡顿"):agentEndSeen 一旦置位,
709
+ // 下面那个条件就恒真 → 每轮结束都起一个 node 进程,纯属白烧。
710
+ // 断点改由 agent 在用 abs_task 时自己写(那时它本来就在跑,不额外起进程)。
749
711
  })
750
712
 
751
713
  pi.on("session_shutdown", (_e: any, ctx: any) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -116,6 +116,12 @@ abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs t
116
116
  **硬规则(开工前触发器,别被状态表漏掉):**
117
117
 
118
118
  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>"`(描述同步换掉)。
119
125
  2. **一段活儿干完立刻 done** —— 不是等整个需求收尾。宁可拆成 5 条小的,别攒成 1 条大的。
120
126
  3. **动手超过两三轮还没登记 = 已在失控路上** —— 立刻补 `start`。
121
127
 
package/src/store.js CHANGED
@@ -1187,148 +1187,6 @@ export async function cmdTask({ dir, action, id, section, note, as }) {
1187
1187
  throw new Error(`unknown task action: ${action}`);
1188
1188
  }
1189
1189
 
1190
- // ---------- 自动登记(hook 专用,2026-10-05) ----------
1191
- // 背景: pi 的 promptGuidelines 常驻指引已注入(实检 414 次有痕), 但行为没变 ——
1192
- // 指引假设 agent 已决定"要开一个任务", 而排查类工作没有清晰起点("我先看看怎么回事"
1193
- // 看明白了活儿已经干完一半)。故改为 hook 侧自动登记, 不靠 agent 自觉。
1194
- //
1195
- // 粒度: **一会话一条**(幂等键 = 会话 id), 后续改动只更新断点 —— 不是一文件一条。
1196
- // 否则一个会话改 20 个文件就塞 20 条, 看板被垃圾淹没(违"读取侧不得随规模增长")。
1197
- const AUTO_TASK_PREFIX = 'auto-';
1198
-
1199
- /** 从用户指令抽任务关键词。抽不到返回 null —— 宁可不登记, 也不脏看板。
1200
- *
1201
- * 为何不抽不到就用机器 id: 看板是给人看的, `auto-c3f1` 认不出是什么;
1202
- * "在吗"/"继续"/"abs" 这类纯对话开头抽出的是噪声, 登了反而增加负担。
1203
- */
1204
- export function autoTaskId(text) {
1205
- const s = String(text || '').replace(/[\u200b\u200c\u200d\ufeff]/g, '').trim();
1206
- if (!s) return null;
1207
- // 纯对话/寒暄不算指令
1208
- if (/^(在吗|在不在|你好|hi|hello|ok|好的|收到|继续|谢了|谢谢|abs|嗯|对|是的|行)[\s!!。.~]*$/i.test(s)) return null;
1209
- // 疑问句不算开工指令(2026-10-05 实报): 用户问"为什么任务是这个啊"被登记成了任务 ——
1210
- // 提问/质疑/求助不是"让我动手",登记它等于把用户的疑问变成待办,很荒唐。
1211
- // 判据用两档: ①句中有疑问词 ②句尾是问号(含全角)。
1212
- if (/[??]\s*$/.test(s)) return null;
1213
- if (/(为什么|为啥|怎么会|怎么|如何|什么是|是什么|哪里|哪个|能不能|可不可以|是否|吗\s*[??。!!]?$|呢\s*[??。!!]?$)/.test(s)) return null;
1214
- // 取首个意群(标点/换行切)作为 id 来源
1215
- const head = s.split(/[\n。!?!?;;,,]/)[0].trim();
1216
- if (head.length < 4) return null;
1217
- // 中文取字,英文/数字取词;保留可读性,限长
1218
- const words = head.match(/[A-Za-z0-9_.-]+/g) || [];
1219
- const cjk = head.replace(/[A-Za-z0-9_.\-\s]+/g, '').slice(0, 12);
1220
- const slug = (words.length && cjk.length < 2 ? words.slice(0, 4).join('-') : cjk)
1221
- .replace(/[^\w\u4e00-\u9fa5-]/g, '')
1222
- .slice(0, 24);
1223
- return slug.length >= 2 ? slug : null;
1224
- }
1225
-
1226
- /** 从断点文本提一个像样的 slug。
1227
- *
1228
- * 断点形如 `改了 src/store.js` / `本轮到 hooks/abs.pi.ts`;直接当 slug 会得到
1229
- * `改了`(首词)这种废名。这里取【文件名】当 slug —— 「改了哪个文件」比「改了」有意义。
1230
- */
1231
- export function autoTaskSlugFromNote(note) {
1232
- const s = String(note || '').replace(/[\u200b\u200c\u200d\ufeff]/g, '');
1233
- // 文件路径优先(含 / 或 . 扩展名)
1234
- const m = s.match(/[\w./-]*[\w-]+\.[A-Za-z0-9]{1,8}/);
1235
- if (m) {
1236
- const base = m[0].split('/').pop().replace(/\.[A-Za-z0-9]{1,8}$/, '');
1237
- if (base && base.length >= 2) return `改 ${base}`;
1238
- }
1239
- // 退而求其次:抽到的关键词(可能为空)
1240
- return autoTaskId(s);
1241
- }
1242
-
1243
- /** hook 自动登记: 一会话一条 + 断点更新。
1244
- *
1245
- * @param {object} o
1246
- * @param {string} o.dir 项目目录
1247
- * @param {string} o.session 会话 id(幂等键)
1248
- * @param {string} o.prompt 用户指令原文(抽 id 用,仅首次需要)
1249
- * @param {string} o.note 断点内容(后续调用时更新到同一条)
1250
- * @returns {Promise<string|null>} 动作说明;无事发生返回 null
1251
- */
1252
- export async function cmdAutoTask({ dir, session, prompt, note }) {
1253
- if (String(process.env.ABS_AUTO_TASK || '') === '0') return null;
1254
- const root = await requireBrain(dir || process.cwd());
1255
- const sid = String(session || '').replace(/[^\w-]/g, '').slice(0, 16);
1256
- if (!sid) return null;
1257
-
1258
- // 幂等键藏在标记里而不是 id 里(2026-10-05 实报): 原先 id = `auto-<会话uuid前16位>`,
1259
- // 用户看到 `auto-01a10b96-372d-71` 完全认不出是什么。id 是给人看的,改用关键词;
1260
- // 会话 id 退成机器用的隐藏键(`(自动登记 <sid>)`),sweep 靠它认领自动条目。
1261
- const MARK = `自动登记 ${sid}`;
1262
- const todoPath = brainPath(root, 'todo.md');
1263
-
1264
- // 已登记过 → 只更新断点(一会话一条)
1265
- let existingId = null;
1266
- await editFile(todoPath, (cur) => {
1267
- if (cur === null) return SKIP;
1268
- for (const l of cur.split('\n')) {
1269
- const id = idOfTaskLine(l);
1270
- if (id && id.startsWith(AUTO_TASK_PREFIX) && l.includes(MARK)) { existingId = id; break; }
1271
- }
1272
- return SKIP;
1273
- });
1274
- if (existingId) {
1275
- if (!note) return null;
1276
- const r = await setBreakpoint(root, { id: existingId, text: note });
1277
- return r?.changed ? `↳ ${existingId} 断点更新` : null;
1278
- }
1279
-
1280
- // 占位登记(2026-10-05 用户实报后重设计)。
1281
- //
1282
- // 为何不再抽用户话当名字:机器抽词天生不可靠 —— 实测抽出过
1283
- // `我已经重启测试一下`(用户的开场白)、`本轮有改动`(更糟:那是 hook 自己
1284
- // 生成的断点文本,等于把机器的话当成用户意图)。
1285
- // 而用户想要的名字长这样:`fix-skill-trigger` / `fix-opencode-v2-plugin` ——
1286
- // 那**不是从用户的字面上抽出来的**,是理解"在干什么"之后起的名字。
1287
- //
1288
- // 故分工改为:hook 只开一个 `TBD-<会话>` 占位(明确标记"未命名"),
1289
- // 名字留给理解任务的一方改(`abs todo rename` 或直接编辑 todo.md)。
1290
- // 占位名机器化、一眼可辨,不冒充真任务名。
1291
- // 开占位的前置条件:**真的动过文件**(hook 传了 --note)。
1292
- // 只有 prompt 而无 note = 用户说了句话但没动手 → 不开(这是"时机"那条坑的守门,
1293
- // 改成固定占位后一度丢掉此判断,导致纯寒暄也开条目 —— 单测抓到的)。
1294
- if (!note) return null;
1295
- const id = `${AUTO_TASK_PREFIX}TBD-${sid}`;
1296
- const who = await requireUser();
1297
- await ensurePersonPage(root, who);
1298
- await upsertTask(root, {
1299
- section: SEC.todo,
1300
- text: `[进行中] ${id} ${atTag(who)} — 待命名 (${MARK})`,
1301
- });
1302
- if (note) await setBreakpoint(root, { id, text: note });
1303
- return `✓ 自动登记占位 ${id}(待命名,稍后改名)`;
1304
- }
1305
-
1306
- /** 收尾: 把自动登记的未完成任务标 done(用户没显式接管它们)。
1307
- * 结语用【仅方案】—— 自动条目本身不代表"成果落地",不该冒充【落地】。 */
1308
- export async function cmdAutoTaskSweep({ dir }) {
1309
- const root = await requireBrain(dir || process.cwd());
1310
- const todoPath = brainPath(root, 'todo.md');
1311
- const ids = [];
1312
- await editFile(todoPath, (cur) => {
1313
- if (cur === null) return SKIP;
1314
- for (const l of cur.split('\n')) {
1315
- const id = idOfTaskLine(l);
1316
- // 自动条目的判据是「自动登记」标记, 不是 id 前缀 —— id 现在是给人看的关键词,
1317
- // `auto-fix-panel` 这种名字人工也可能起得出来, 靠前缀认领会误杀真任务。
1318
- if (id && /自动登记\s+\S+/.test(l)) ids.push(id);
1319
- }
1320
- return SKIP;
1321
- });
1322
- let n = 0;
1323
- for (const id of ids) {
1324
- try {
1325
- await markDone(todoPath, id, '仅方案', '自动登记未接管,收尾清理');
1326
- n++;
1327
- } catch { /* 单条失败不阻塞其余 */ }
1328
- }
1329
- return n ? `✓ 自动任务收尾 ${n} 条` : null;
1330
- }
1331
-
1332
1190
  async function markDone(file, id, kind = '落地', conclusion) {
1333
1191
  const res = await editFile(file, (text) => {
1334
1192
  const lines = text.split('\n');