@fanchao8609/agent_brain_sync 1.19.1 → 1.19.3

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.19.1",
3
+ "version": "1.19.3",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/store.js CHANGED
@@ -1041,20 +1041,27 @@ async function markDone(file, id, kind = '落地', conclusion, bySid = '') {
1041
1041
  break;
1042
1042
  }
1043
1043
  moved = [head, ...bp];
1044
- // 未勾子项升为独立任务留在 Todo 区:原地留着不行 —— 子项行一旦失去父任务,
1045
- // 重建分区时就是无主杂行(实测被清掉,待办静默消失)。
1044
+ // 还有未勾子项就**不许 done**(2026-10-18 用户定)。
1046
1045
  //
1047
- // ★ 2026-10-18 补:升格的行必须有**合法 id** 与**父任务线索**。
1048
- // 原来直接写 `- [ ] ${txt}`,实测 idOfTaskLine 会把正文开头的几个字当成 id
1049
- // (`- [ ] [进行中] 子任务带 id ABC-1` → id="子任务带"),后续 `abs todo done`
1050
- // 靠 id 全等定位,于是变成靠句子前几个字匹配 —— 换个措辞就断,两条子项
1051
- // 开头相同时互相误命中。作者标记沿用父任务的,不另取名(拿不到就留空)。
1052
- const author = (l.match(/\[\[([^\]]+)\]\]/) || [])[1];
1053
- leftover.forEach((txt, k) => {
1054
- const restId = `${wantId}-rest${k + 1}`;
1055
- kept.push(`- [ ] [进行中] ${restId}${author ? ` [[${author}]]` : ''} — ${txt}(原 ${wantId} 遗留)`);
1056
- });
1057
- leftBehind.push(...leftover);
1046
+ // 规则:主任务与子任务都是任务,处理方式一样 —— 子任务也是看板上一行,
1047
+ // 也要自己完成。只有主子全部完成,主任务才能经 done。
1048
+ //
1049
+ // 为何不自己搬:上四版的四种做法全被否定——
1050
+ // ① 随父任务进 Done(原实现)→ Done 区被 collapseDone 折叠成计数,
1051
+ // 未勾子项进去即消失(实测 factory-docs-slim)
1052
+ // ② 升为顶层任务(<id>-restN)→ 用户:子任务不能升格
1053
+ // ③ 建占位主任务挂住 → 仍是无中生有,与 ② 同病
1054
+ // ④ 就这么留着不管 → 无主杂行,重建分区时被清掉
1055
+ // 正确做法是**不替用户决定**:告诉他还有哪些没完成,让他先勾或先删。
1056
+ if (leftover.length) {
1057
+ throw new Error(
1058
+ `✗ 任务 "${wantId}" 还有 ${leftover.length} 条子任务未完成,不能 done:\n` +
1059
+ leftover.map((p) => ` - [ ] ${p}`).join('\n') +
1060
+ `\n子任务也是任务,主子全部完成才能 done。先二选一:\n` +
1061
+ ` • 做完了 → abs todo bp-done ${wantId} (勾最后一条;指定第 N 条:bp-done ${wantId} --n N)\n` +
1062
+ ` • 不做了 → 删掉那几行子项,再 abs todo done ${wantId}`,
1063
+ );
1064
+ }
1058
1065
  continue;
1059
1066
  }
1060
1067
  kept.push(l);
@@ -77,6 +77,36 @@ export function checkFileShape(file, text, { lintMode = false } = {}) {
77
77
  }
78
78
  }
79
79
  }
80
+ // 孤儿缩进行:缩进的任务行(子任务)上方必须紧跟着一条任务行。
81
+ //
82
+ // 为何要这个(2026-10-18 实测):我手动清理 Done 区时只删了父任务行、
83
+ // 留下了它的 ` - [x] 测试子项`,于是看板上出现一条“没有主项的子项”
84
+ // —— 而当时 checkFileShape 报 0 违规,lint 也说健康,没人拦得住。
85
+ // 现实中它会来自:手改文件、父任务行被删、分区重建时父任务被搬走。
86
+ // 子任务离开父任务就没有意义(它靠缩进表达归属),所以这是硬错。
87
+ if (file === 'todo.md') {
88
+ for (let i = 0; i < lines.length; i++) {
89
+ if (!/^\s+- \[[ x]\]/.test(lines[i])) continue;
90
+ // 往上找最近的非空行
91
+ let p = i - 1;
92
+ while (p >= 0 && !lines[p].trim()) p--;
93
+ const prev = p >= 0 ? lines[p] : null;
94
+ // 父行可以是:顶层任务 `- [x]`、子任务 ` - [x]`、或旧格式一等任务 ` ↳ [ ] …`
95
+ // (后者缩进是历史形态,它本身是任务不是子项 —— 实测漏认会把合法数据误判成孤儿)。
96
+ const ok = prev !== null && (
97
+ /^- \[[ x]\]/.test(prev) ||
98
+ /^\s+- \[[ x]\]/.test(prev) ||
99
+ /^\s*↳\s*\[[ x]\]/.test(prev)
100
+ );
101
+ if (!ok) {
102
+ issues.push({
103
+ code: 'TODO-ORPHAN-CHILD',
104
+ line: i + 1,
105
+ msg: `todo.md:${i + 1} 孤儿子任务(「${lines[i].trim().slice(0, 40)}」上方没有主任务)—— 子任务靠缩进表达归属,删父任务时要连子项一起删`,
106
+ });
107
+ }
108
+ }
109
+ }
80
110
  // 条目之间的空行(与写入闸门 assertNoStrayBlank 同判据)。
81
111
  // lint 只能「报」,不能抛 —— 体检不能因格式坏而挂。
82
112
  const isEntryLine = (l) => /^-\s/.test(l) || /^#{1,3}\s+\[/.test(l);