@fanchao8609/agent_brain_sync 1.14.1 → 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' },
@@ -284,6 +280,7 @@ const subUsage = {
284
280
  '用法:',
285
281
  ' abs todo 看板',
286
282
  ' abs todo add <id> [--note "做什么"] [--section 讨论中|滞留中]',
283
+ ' abs todo rename <旧id> --note "<新id>" 给自动登记的占位条目起真名',
287
284
  ' abs todo note <id> --note "断点/进度"',
288
285
  ' abs todo state <id> --note 进行中|讨论中|滞留中',
289
286
  ' abs todo done <id> [--as 落地|否决|仅方案] [结语文字]',
@@ -351,6 +348,7 @@ const TODO_ACTIONS = {
351
348
  start: 'start', // add 的别名(老习惯保留)
352
349
  note: 'note',
353
350
  state: 'state', // 改行首状态标记:进行中|讨论中|滞留中(原地,不搬区)
351
+ rename: 'rename', // 改任务 id:hook 只开 auto-TBD-<会话> 占位,真名由此处补
354
352
  done: 'done',
355
353
  };
356
354
 
@@ -531,16 +529,7 @@ async function main() {
531
529
  // 内部命令(hook 专用,不出现在 help):Stop 时机械快照未完成任务
532
530
  case 'wrapup': {
533
531
  rejectExtra(opts._, 'abs wrapup(内部命令, 供 hook 调用)');
534
- const r = await cmdWrapup({ dir: opts.dir });
535
- // 收尾时顺带清理 hook 自动登记的条目(它们不代表人工接管的成果)
536
- const swept = await cmdAutoTaskSweep({ dir: opts.dir }).catch(() => null);
537
- console.log([r, swept].filter(Boolean).join('\n') || '(无滞留任务)');
538
- break;
539
- }
540
- // hook 自动登记(pi/opencode 侧调): 一会话一条,同会话只更新断点
541
- case 'auto-task': {
542
- const r = await cmdAutoTask({ dir: opts.dir, session: opts.session, prompt: opts.prompt, note: opts.note });
543
- if (r) console.log(r); // 无事发生则静默(不制造空动作噪音)
532
+ console.log(await cmdWrapup({ dir: opts.dir }));
544
533
  break;
545
534
  }
546
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.14.1",
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
@@ -1120,6 +1120,31 @@ export async function cmdTask({ dir, action, id, section, note, as }) {
1120
1120
  });
1121
1121
  return `✓ 任务${r.updated ? '更新(幂等)' : '登记'} → ${brainPath(root, 'todo.md')}\n [${st}] ${id} ${atTag(who)}${note ? ' — ' + note : ''}`;
1122
1122
  }
1123
+ if (action === 'rename') {
1124
+ // 给任务改名(2026-10-05):hook 只开 `auto-TBD-<会话>` 占位,真名由理解任务的一方起
1125
+ // (`fix-skill-trigger` 这种不是从用户字面抽出来的,是读懂在干什么之后起的)。
1126
+ // --note 传新 id(沿用既有 CLI 习惯,免得再加一个 flag)。
1127
+ if (!note) throw new Error('✗ 用法: abs todo rename <旧id> --note "<新id>"');
1128
+ const newId = String(note).replace(/[\u200b\u200c\u200d\ufeff]/g, '').trim();
1129
+ if (!newId || /\s/.test(newId)) throw new Error(`✗ 新 id 不能为空或含空白: "${newId}"`);
1130
+ const f = brainPath(root, 'todo.md');
1131
+ let done = false;
1132
+ await editFile(f, (cur) => {
1133
+ if (cur === null) return SKIP;
1134
+ const lines = cur.split('\n');
1135
+ const want = String(id).replace(/[\u200b\u200c\u200d\ufeff]/g, '');
1136
+ for (let i = 0; i < lines.length; i++) {
1137
+ if (idOfTaskLine(lines[i]) !== want) continue;
1138
+ lines[i] = lines[i].replace(want, newId);
1139
+ // 占位描述「待命名」随之换掉 —— 改完名还写着"待命名"会让人以为没改。
1140
+ lines[i] = lines[i].replace(/—\s*待命名\s*/, `— ${newId.replace(/^(auto-|fix-|feat-)/, '')} `);
1141
+ done = true;
1142
+ break;
1143
+ }
1144
+ return done ? { text: lines.join('\n') } : SKIP;
1145
+ });
1146
+ return done ? `✓ 任务改名 ${id} → ${newId}` : `[NO_MATCH] 未找到含 "${id}" 的未完成任务行`;
1147
+ }
1123
1148
  if (action === 'state') {
1124
1149
  // 改状态标记(原地,不搬区):进行中 / 讨论中 / 滞留中
1125
1150
  if (!note) throw new Error(`✗ 用法: abs todo state <id> --note "${TASK_STATES.join('|')}"`);
@@ -1162,150 +1187,6 @@ export async function cmdTask({ dir, action, id, section, note, as }) {
1162
1187
  throw new Error(`unknown task action: ${action}`);
1163
1188
  }
1164
1189
 
1165
- // ---------- 自动登记(hook 专用,2026-10-05) ----------
1166
- // 背景: pi 的 promptGuidelines 常驻指引已注入(实检 414 次有痕), 但行为没变 ——
1167
- // 指引假设 agent 已决定"要开一个任务", 而排查类工作没有清晰起点("我先看看怎么回事"
1168
- // 看明白了活儿已经干完一半)。故改为 hook 侧自动登记, 不靠 agent 自觉。
1169
- //
1170
- // 粒度: **一会话一条**(幂等键 = 会话 id), 后续改动只更新断点 —— 不是一文件一条。
1171
- // 否则一个会话改 20 个文件就塞 20 条, 看板被垃圾淹没(违"读取侧不得随规模增长")。
1172
- const AUTO_TASK_PREFIX = 'auto-';
1173
-
1174
- /** 从用户指令抽任务关键词。抽不到返回 null —— 宁可不登记, 也不脏看板。
1175
- *
1176
- * 为何不抽不到就用机器 id: 看板是给人看的, `auto-c3f1` 认不出是什么;
1177
- * "在吗"/"继续"/"abs" 这类纯对话开头抽出的是噪声, 登了反而增加负担。
1178
- */
1179
- export function autoTaskId(text) {
1180
- const s = String(text || '').replace(/[\u200b\u200c\u200d\ufeff]/g, '').trim();
1181
- if (!s) return null;
1182
- // 纯对话/寒暄不算指令
1183
- if (/^(在吗|在不在|你好|hi|hello|ok|好的|收到|继续|谢了|谢谢|abs|嗯|对|是的|行)[\s!!。.~]*$/i.test(s)) return null;
1184
- // 疑问句不算开工指令(2026-10-05 实报): 用户问"为什么任务是这个啊"被登记成了任务 ——
1185
- // 提问/质疑/求助不是"让我动手",登记它等于把用户的疑问变成待办,很荒唐。
1186
- // 判据用两档: ①句中有疑问词 ②句尾是问号(含全角)。
1187
- if (/[??]\s*$/.test(s)) return null;
1188
- if (/(为什么|为啥|怎么会|怎么|如何|什么是|是什么|哪里|哪个|能不能|可不可以|是否|吗\s*[??。!!]?$|呢\s*[??。!!]?$)/.test(s)) return null;
1189
- // 取首个意群(标点/换行切)作为 id 来源
1190
- const head = s.split(/[\n。!?!?;;,,]/)[0].trim();
1191
- if (head.length < 4) return null;
1192
- // 中文取字,英文/数字取词;保留可读性,限长
1193
- const words = head.match(/[A-Za-z0-9_.-]+/g) || [];
1194
- const cjk = head.replace(/[A-Za-z0-9_.\-\s]+/g, '').slice(0, 12);
1195
- const slug = (words.length && cjk.length < 2 ? words.slice(0, 4).join('-') : cjk)
1196
- .replace(/[^\w\u4e00-\u9fa5-]/g, '')
1197
- .slice(0, 24);
1198
- return slug.length >= 2 ? slug : null;
1199
- }
1200
-
1201
- /** 从断点文本提一个像样的 slug。
1202
- *
1203
- * 断点形如 `改了 src/store.js` / `本轮到 hooks/abs.pi.ts`;直接当 slug 会得到
1204
- * `改了`(首词)这种废名。这里取【文件名】当 slug —— 「改了哪个文件」比「改了」有意义。
1205
- */
1206
- export function autoTaskSlugFromNote(note) {
1207
- const s = String(note || '').replace(/[\u200b\u200c\u200d\ufeff]/g, '');
1208
- // 文件路径优先(含 / 或 . 扩展名)
1209
- const m = s.match(/[\w./-]*[\w-]+\.[A-Za-z0-9]{1,8}/);
1210
- if (m) {
1211
- const base = m[0].split('/').pop().replace(/\.[A-Za-z0-9]{1,8}$/, '');
1212
- if (base && base.length >= 2) return `改 ${base}`;
1213
- }
1214
- // 退而求其次:抽到的关键词(可能为空)
1215
- return autoTaskId(s);
1216
- }
1217
-
1218
- /** hook 自动登记: 一会话一条 + 断点更新。
1219
- *
1220
- * @param {object} o
1221
- * @param {string} o.dir 项目目录
1222
- * @param {string} o.session 会话 id(幂等键)
1223
- * @param {string} o.prompt 用户指令原文(抽 id 用,仅首次需要)
1224
- * @param {string} o.note 断点内容(后续调用时更新到同一条)
1225
- * @returns {Promise<string|null>} 动作说明;无事发生返回 null
1226
- */
1227
- export async function cmdAutoTask({ dir, session, prompt, note }) {
1228
- if (String(process.env.ABS_AUTO_TASK || '') === '0') return null;
1229
- const root = await requireBrain(dir || process.cwd());
1230
- const sid = String(session || '').replace(/[^\w-]/g, '').slice(0, 16);
1231
- if (!sid) return null;
1232
-
1233
- // 幂等键藏在标记里而不是 id 里(2026-10-05 实报): 原先 id = `auto-<会话uuid前16位>`,
1234
- // 用户看到 `auto-01a10b96-372d-71` 完全认不出是什么。id 是给人看的,改用关键词;
1235
- // 会话 id 退成机器用的隐藏键(`(自动登记 <sid>)`),sweep 靠它认领自动条目。
1236
- const MARK = `自动登记 ${sid}`;
1237
- const todoPath = brainPath(root, 'todo.md');
1238
-
1239
- // 已登记过 → 只更新断点(一会话一条)
1240
- let existingId = null;
1241
- await editFile(todoPath, (cur) => {
1242
- if (cur === null) return SKIP;
1243
- for (const l of cur.split('\n')) {
1244
- const id = idOfTaskLine(l);
1245
- if (id && id.startsWith(AUTO_TASK_PREFIX) && l.includes(MARK)) { existingId = id; break; }
1246
- }
1247
- return SKIP;
1248
- });
1249
- if (existingId) {
1250
- if (!note) return null;
1251
- const r = await setBreakpoint(root, { id: existingId, text: note });
1252
- return r?.changed ? `↳ ${existingId} 断点更新` : null;
1253
- }
1254
-
1255
- // 首次登记: 关键词优先从用户指令抽;抽不出(寒暄/提问/无指令)时用断点兜底。
1256
- //
1257
- // 为何 note 也能兜底(2026-10-05 时机改动后新增):登记时机已挪到"第一次真改文件",
1258
- // 那一刻【已经动过文件】就是最可靠的"这是件事"的证据 —— 此时因抽不出关键词而放弃,
1259
- // 等于用户寒暄两句后开始干活却什么都没记。用文件名当兜底 slug 比不登记有用。
1260
- // 兜底仅在【有断点】时启用 —— 断点的存在本身就意味着"刚动过文件"。
1261
- // 只有 prompt 而抽不出关键词(纯寒暄/提问、且没动文件)→ 依然不登记(用户实报的那个坑)。
1262
- const slug = autoTaskId(prompt) || (note ? autoTaskSlugFromNote(note) : null);
1263
- if (!slug) return null;
1264
- // id = 关键词(可读);同 slug 已存在则加序号,避免两条不同活撞同一个 id。
1265
- let id = `${AUTO_TASK_PREFIX}${slug}`;
1266
- let n = 1;
1267
- await editFile(todoPath, (cur) => {
1268
- if (cur === null) return SKIP;
1269
- const ids = new Set(cur.split('\n').map((l) => idOfTaskLine(l)).filter(Boolean));
1270
- while (ids.has(id)) id = `${AUTO_TASK_PREFIX}${slug}-${++n}`;
1271
- return SKIP;
1272
- });
1273
- const who = await requireUser();
1274
- await ensurePersonPage(root, who);
1275
- await upsertTask(root, {
1276
- section: SEC.todo,
1277
- text: `[进行中] ${id} ${atTag(who)} — ${slug} (${MARK})`,
1278
- });
1279
- if (note) await setBreakpoint(root, { id, text: note });
1280
- return `✓ 自动登记 ${id} — ${slug}`;
1281
- }
1282
-
1283
- /** 收尾: 把自动登记的未完成任务标 done(用户没显式接管它们)。
1284
- * 结语用【仅方案】—— 自动条目本身不代表"成果落地",不该冒充【落地】。 */
1285
- export async function cmdAutoTaskSweep({ dir }) {
1286
- const root = await requireBrain(dir || process.cwd());
1287
- const todoPath = brainPath(root, 'todo.md');
1288
- const ids = [];
1289
- await editFile(todoPath, (cur) => {
1290
- if (cur === null) return SKIP;
1291
- for (const l of cur.split('\n')) {
1292
- const id = idOfTaskLine(l);
1293
- // 自动条目的判据是「自动登记」标记, 不是 id 前缀 —— id 现在是给人看的关键词,
1294
- // `auto-fix-panel` 这种名字人工也可能起得出来, 靠前缀认领会误杀真任务。
1295
- if (id && /自动登记\s+\S+/.test(l)) ids.push(id);
1296
- }
1297
- return SKIP;
1298
- });
1299
- let n = 0;
1300
- for (const id of ids) {
1301
- try {
1302
- await markDone(todoPath, id, '仅方案', '自动登记未接管,收尾清理');
1303
- n++;
1304
- } catch { /* 单条失败不阻塞其余 */ }
1305
- }
1306
- return n ? `✓ 自动任务收尾 ${n} 条` : null;
1307
- }
1308
-
1309
1190
  async function markDone(file, id, kind = '落地', conclusion) {
1310
1191
  const res = await editFile(file, (text) => {
1311
1192
  const lines = text.split('\n');