@fanchao8609/agent_brain_sync 1.29.0 → 1.31.0

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.pi.ts CHANGED
@@ -780,13 +780,13 @@ export function pickNickname(rnd: () => number = Math.random): string {
780
780
  * 子项行(缩进的 `- [ ]`)归属到它上一条任务,不单独成条。 */
781
781
  export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
782
782
  total: number
783
- rows: { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; sid: string }[]
783
+ rows: { state: string; id: string; desc: string; kids: { done: boolean; text: string; state: string | null }[]; sid: string }[]
784
784
  hidden: number
785
785
  } {
786
786
  // 两阶段:先无过滤地按序解析(子项归属需要“上一条”是原文里真正的前一条),
787
787
  // 再按作者过滤。若边解析边过滤,被滤掉的条目下方的子项会挂到它上面那一条
788
788
  // —— 实测:别人任务的子项会显示在我的任务下面(2026-10-03 审查发现)。
789
- type Row = { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; authors: string[]; sid: string }
789
+ type Row = { state: string; id: string; desc: string; kids: { done: boolean; text: string; state: string | null }[]; authors: string[]; sid: string }
790
790
  const parsed: Row[] = []
791
791
  let inTodo = false
792
792
  for (const raw of String(md || '').split('\n')) {
@@ -861,12 +861,20 @@ async function currentUser(): Promise<string> {
861
861
  * 为何不 import src:面板是宿主扩展,单独装到 ~/.pi/extensions/,与包内 src/ 不同目录
862
862
  * (装完就断了)。故两端各一份 —— 规则简单(两步正则),且两边测试都盖。
863
863
  * 标准 md:缩进的 `- [ ] 内容`。 */
864
- function parseChildLine(l: string): { done: boolean; text: string; indent: number } | null {
864
+ /** 行首状态标记的四个词(任务行与子项共用;与 CLI 侧 common.js 的 TASK_STATES 同值)。
865
+ * 两个扩展各自独立安装,所以这里再拄一份,不跨包 import。 */
866
+ const STATE_WORDS = ['进行中', '讨论中', '滞留中', '搁置']
867
+
868
+ function parseChildLine(l: string): { done: boolean; text: string; indent: number; state: string | null } | null {
865
869
  // 只认标准 md 缩进子项(2026-10-20 旧箭头写法已废弃)。
866
870
  // 旧行由 CLI 侧 migrateLegacyChild 在读写时迁移掉,面板拿到的已是新格式。
867
871
  const m = String(l || '').match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/)
868
872
  if (!m) return null
869
- return { done: m[2] === 'x', text: m[3].trim(), indent: m[1].length }
873
+ // 子项也能带状态标记(2026-11-09 用户要求,与任务行同形):只认四个词,
874
+ // 别的方括号(如 `[WIP]`)是正文的一部分,原样留在 text 里(同 CLI 侧 parseChild)。
875
+ const s = m[3].match(/^\[([^\]]+)\]\s*(.*)$/)
876
+ const state = s && STATE_WORDS.includes(s[1]) ? s[1] : null
877
+ return { done: m[2] === 'x', text: (state ? s![2] : m[3]).trim(), indent: m[1].length, state }
870
878
  }
871
879
 
872
880
  function dispWidth(s: string): number {
@@ -935,110 +943,69 @@ function clipToWidth(s: string, width: number): string {
935
943
  //
936
944
  // 为何用盲文点阵而不是 ⠋⠙⠹ 之外的 ASCII 旋转符:
937
945
  // ⠿ 系符号在等宽字体里都是 1 列宽,且视觉上是个转动的方框,比 | / - \ 均匀。
938
- // 为何帧宽必须恒定:面板每 250ms 重绘,宽度一变整行就会左右抖。盲文恰好全是 1 列。
939
- const TODO_ANIM_FRAMES = ['◐', '◓', '◑', '◒']
940
- const TODO_ANIM_MS = 200
941
-
942
- /** 动画相位(每 tick +1)。模块级 —— 同 agentEndSeen,闭包 flag 在重复注册时不共享。 */
943
- let animPhase = 0
944
- let animTimer: ReturnType<typeof setInterval> | undefined
945
-
946
946
  /** 当前推理强度(off/minimal/low/…/max)。模块级,理由同上:widget factory 每帧重新调,
947
947
  * 拿不到 ctx,只能由事件回调先存好。2026-11-06 用户需求:面板主色跟推理强度走。 */
948
948
  let effortLevel = ''
949
949
 
950
- /** 启动/保持动画定时器。重复调用不会叠加(已存在则不动)。
951
- * getTui 是为了每 tick 拿**当前**的 tui 引用 —— widget 可能因刷新被重建,
952
- * 持旧的引用会调到一个已被丢弃的 renderer。 */
953
- function startAnimTimer(getTui: () => any): void {
954
- if (animTimer) return
955
- animTimer = setInterval(() => {
956
- animPhase++
957
- try { getTui()?.requestRender?.() } catch {}
958
- }, TODO_ANIM_MS)
959
- // 别阻止进程退出(扩展是宿主进程的一分子,定时器不该吊住事件循环)
960
- ;(animTimer as any)?.unref?.()
961
- }
962
-
963
- function stopAnimTimer(): void {
964
- if (animTimer) { clearInterval(animTimer); animTimer = undefined }
965
- }
966
-
967
- /** 取当前动画帧(phase 每 tick +1,由定时器驱动)。非进行中的行不传 phase(不显示)。 */
968
- export function todoAnimFrame(phase: number): string {
969
- const n = TODO_ANIM_FRAMES.length
970
- const i = ((Math.floor(phase) % n) + n) % n
971
- return TODO_ANIM_FRAMES[i]
972
- }
973
-
974
- /**
975
- * 推理强度 → 主题色 token。空串表示「不上推理色,用调用方给的兼底色」。
950
+ /** 档位 → pi 主题里那档 thinking 色(与输入框边框同一个来源)。
976
951
  *
977
- * 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
978
- * 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
979
- * (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
980
- * 「off 时保持默认色」。
981
- *
982
- * ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`thinkingColorToken`)。
983
- * 不抽公共模块是故意的:安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,
984
- * 抽成共享文件会让「删掉输入框样式目录」连带把 todo 面板搞崩。
985
- */
986
- export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
987
- max: { rgb: '197;205;220', ansi256: 189 },
988
- xhigh: { rgb: '168;182;204', ansi256: 152 },
989
- high: { rgb: '150;166;198', ansi256: 110 },
990
- medium: { rgb: '122;138;160', ansi256: 110 },
991
- low: { rgb: '94;108;132', ansi256: 103 },
992
- minimal: { rgb: '70;82;106', ansi256: 240 },
993
- }
994
-
995
- export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
996
- const g = EFFORT_COLOR[String(level || '').trim().toLowerCase()]
997
- if (!g) return ''
998
- return colorMode === 'truecolor' ? `38;2;${g.rgb}` : `38;5;${g.ansi256}`
952
+ * 为何要向主题要(2026-11-09 用户要求「todo 上的颜色也跟着 pi 输入框的颜色」):
953
+ * 那七档色写在主题里(dark.json 的 okhsl),自己拄一份就会跟着主题改而跑偏。
954
+ * 为何要抠一层正则:getFgAnsi() 返回的是整段 `\x1b[38;2;…m`,而面板内部用的是
955
+ * 「色码串 + paintAnsi 自己包转义」那套,只能取里面的 `38;2;…`。
956
+ * 空档位或主题不认这个 token → 返回空串,调用处回落到 C.desc。 */
957
+ export function thinkingAnsiOf(theme: any, level: string | null | undefined): string {
958
+ const lvl = String(level || '').trim().toLowerCase()
959
+ if (!lvl) return ''
960
+ const token = `thinking${lvl[0].toUpperCase()}${lvl.slice(1)}`
961
+ try {
962
+ const m = String(theme?.getFgAnsi?.(token) || '').match(/\x1b\[([0-9;]+)m/)
963
+ return m ? m[1] : ''
964
+ } catch {
965
+ return ''
966
+ }
999
967
  }
1000
968
 
1001
- /** 用 effortAnsi 的结果包一段文字;空串原样返回(off 时不着色)。 */
969
+ /** 用色码串包一段文字(如 `paint('38;2;16;185;129', 'x')`);空串原样返回。 */
1002
970
  export function paintAnsi(ansi: string, text: string): string {
1003
971
  return ansi ? `\x1b[${ansi}m${text}\x1b[39m` : text
1004
972
  }
1005
973
 
1006
974
  export function renderPanelLines(
1007
- data: { total: number; rows: { state: string; id: string; desc: string; kids?: { done: boolean; text: string }[] }[]; hidden: number },
975
+ data: { total: number; rows: { state: string; id: string; desc: string; kids?: { done: boolean; text: string; state?: string | null }[] }[]; hidden: number },
1008
976
  who: string,
1009
977
  width: number,
1010
978
  fg: (color: string, s: string) => string,
1011
- animPhase = -1,
1012
979
  nickname = '',
1013
- effort = '',
980
+ effortColor = '',
1014
981
  ): string[] {
1015
982
  // ★ 空看板不再直接返回空数组(2026-10-06 用户要求:「即便 todo 没有列表,也要在 pi 上显示」)。
1016
983
  // 理由与看板段一致:面板是「机制在不在」的常驻指示,进去就看不见 = 不知道机制活着。
1017
984
  // 下面仍要画框和标题,所以这里只是不再提前 return。
1018
985
  //
1019
- // ★ 主色跟推理强度走(2026-11-06 用户需求):标题、[状态]、树线 ├─└─、复选框 [ ]、
1020
- // 进行中动画同用一色,与 pi 输入框描边/文字同色系。
1021
- // ★ off 时整块面板用灰阶(同日用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
1022
- // 高亮色只在真的开着推理时给,否则「推理关了面板还在发光」让人误以为还在深度思考。
1023
- // 主色用 muted(中灰),描述与子项降一阶到 dim(深灰),两阶灰保留层次,不一整片平。
1024
- // 现代暗色方案(2026-11-08 用户给定),不再跟推理强度走。
1025
- // 标题色(2026-11-08 用户实测「太亮了」):从纯金 #FFD700 换成与「搁置」同一个橙 #F59E0B
1026
- // —— 纯金在暗底上抢眼,标题只是分区标签,不该比当天要做的事更跳。
1027
- // 代价:标题与 `[搁置]` 标签同色;两者一个在最左(标题)一个在行中(状态),位置分得开。
986
+ // ★ 文字色跟推理强度走(2026-11-09 用户要求「面板上的文字颜色要与输入框推理强度一致」):
987
+ // 标题、树线、任务 id、描述、子项、`+N more` 全用档位色(effortColor,由调用处从主题取)。
988
+ // ★ 四个状态标签(进行中/讨论中/滞留中/搁置)**保留各自语义色**,不跟档位走 ——
989
+ // 用户选的方案:否则四态只能靠文字看,一眼分不出。别再顺手把它们也统一了。
990
+ // 描述色 C.desc 降为「取不到档位色时的兼底」(主题没这个 token / 模型不支持推理)。
991
+ // 历史(勿重走):2026-11-08 曾定过「现代暗色方案:标题橙、树线蓝灰、描述次灰」,已作废。
1028
992
  const C = {
993
+ // 面板大标题回到擱置橙 #F59E0B(2026-11-09 用户试过灰白,看完要回橙;沿用 2026-11-08 的定)。
994
+ // 任务行的 id/描述**不用**这个色 —— 它们跟档位色跑。
1029
995
  title: '38;2;245;158;11',
1030
996
  task: '38;2;226;232;240',
1031
997
  desc: '38;2;148;163;184',
1032
- tree: '38;2;150;170;198',
1033
998
  doing: '38;2;16;185;129',
1034
999
  talk: '38;2;59;130;246',
1035
1000
  hold: '38;2;245;158;11',
1036
1001
  stuck: '38;2;139;92;246',
1037
- done: '38;2;100;116;139',
1038
1002
  }
1039
1003
  const stateAnsi = (st: string): string =>
1040
1004
  st === '进行中' ? C.doing : st === '讨论中' ? C.talk : st === '搁置' ? C.hold : st === '滞留中' ? C.stuck : C.task
1041
1005
  const paint = (ansi: string, t: string) => paintAnsi(ansi, t)
1006
+ // 面板文字色 = 调用处从主题取的档位色;取不到(主题没这个 token / 模型不支持推理)回落描述灰。
1007
+ // 提到循环外:标题与 `+N more` 也在用它。
1008
+ const eff = effortColor || C.desc
1042
1009
  const lines: string[] = []
1043
1010
  // 上描边行已删除(2026-11-04 用户要求「去掉上边框」)。
1044
1011
  // 历史:那行曾用 `thinkingOff` 对齐 pi 输入框描边色(排查三轮才定色)。
@@ -1059,51 +1026,52 @@ export function renderPanelLines(
1059
1026
  // 全篇没有 └─ 收尾,子项行的 │ 又延伸到视觉底部,看起来像列表被截断。)
1060
1027
  const isLast = i === lastIdx
1061
1028
  const branch = isLast ? '└─' : '├─'
1062
- // 进行中的行带一个小动画(其余行不加,避免满屏都在跳)
1063
- // 单字符 spinner 紧跟 [进行中]:靠前放才能一眼看到(放行尾会被长描述挤到前面去看不见)。
1064
- // 2026-11-06 用户要求:「放在 [进行中] 后面」。
1065
- const hasState = ['进行中', '讨论中', '搁置', '滞留中'].includes(t.state)
1066
- const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + paint(C.doing, todoAnimFrame(animPhase)) : ''
1029
+ // 进行中的行曾带圆圈动画(2026-11-08 用户定:「这个动画没啥用」,整个去掉)。
1030
+ // 去掉后 [进行中] 靠状态色区分,不再额外消耗一次 200ms 重绘。
1031
+ const hasState = STATE_WORDS.includes(t.state)
1067
1032
  // 复选框 `[ ]`(2026-10-16 用户要求):面板是 todo.md 的只读视图,
1068
1033
  // 就该长成 todo 原貌 —— 摆成与文件里 `- [ ]` 同形,读到就认得出对应哪一行。
1069
- const eff = effortAnsi(effort, 'truecolor') || C.desc
1070
1034
  const box = hasState ? '' : paint(eff, '[ ] ')
1071
1035
  const stPart = hasState
1072
1036
  ? paint(stateAnsi(t.state), `[${t.state}]`)
1073
1037
  : paint(eff, '[待办]')
1074
- const head = `${paint(C.tree, branch)} ${box}${stPart}${anim} ${paint(eff, t.id)}`
1075
- const desc = t.desc ? ' ' + paint(C.desc, `— ${t.desc}`) : ''
1038
+ const head = `${paint(eff, branch)} ${box}${stPart} ${paint(eff, t.id)}`
1039
+ const desc = t.desc ? ' ' + paint(eff, `— ${t.desc}`) : ''
1076
1040
  lines.push(clipToWidth(head + desc, width))
1077
1041
  if (t.kids && t.kids.length) {
1078
1042
  // 子项:标准 md 嵌套列表 → 画成树
1079
1043
  // ├─ [ ] 子项一
1080
1044
  // └─ [x] 子项二
1081
1045
  // 树线前缀 = 父任务的 │(非末条)或空格(末条),与父任务分支对齐。
1082
- // 勾过的用 dim 压暗(一眼看出哪些已完成),未勾用正常色。
1083
- const stem = isLast ? ' ' : paint(C.tree, '│ ')
1046
+ // 勾过的用删除线区分(颜色已统一为档位色),未勾不着修饰。
1047
+ const stem = isLast ? ' ' : paint(eff, '│ ')
1084
1048
  const n = t.kids.length
1085
1049
  t.kids.forEach((k, ki) => {
1086
1050
  const kb = ki === n - 1 ? '└─' : '├─'
1087
- const kbox = paint(eff, k.done ? '[x]' : '[ ]')
1088
- const ktext = k.done ? `\x1b[9m${paint(C.done, k.text)}\x1b[29m` : paint(C.desc, k.text)
1089
- lines.push(clipToWidth(`${stem} ${paint(C.tree, kb)} ${kbox} ${ktext}`, width))
1051
+ // 有状态的子项跟任务同形:不画 `[ ]`,改画状态标签(2026-11-09 用户要求子项也能标四态)。
1052
+ const kbox = k.state
1053
+ ? paint(stateAnsi(k.state), `[${k.state}]`)
1054
+ : paint(eff, k.done ? '[x]' : '[ ]')
1055
+ // 勾过的子项靠删除线区分(颜色已统一为档位色,不再降灰)
1056
+ const ktext = k.done ? `\x1b[9m${paint(eff, k.text)}\x1b[29m` : paint(eff, k.text)
1057
+ lines.push(clipToWidth(`${stem} ${paint(eff, kb)} ${kbox} ${ktext}`, width))
1090
1058
  })
1091
1059
  }
1092
1060
  })
1093
1061
 
1094
- if (data.hidden > 0) lines.push(clipToWidth(paint(C.title, ` +${data.hidden} more`), width))
1062
+ if (data.hidden > 0) lines.push(clipToWidth(paint(eff, ` +${data.hidden} more`), width))
1095
1063
  return lines
1096
1064
  }
1097
1065
  /** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
1098
1066
  *
1099
1067
  * 坑(2026-10-05 用户实报): 任务全标完后面板不消失, 按 ESC 或重启才正常。
1100
- * 根因: 唯一的重绘入口是动画定时器里的 requestRender, 而"任务全完成"恰好让
1101
- * needAnim=false → stopAnimTimer() → 从此没有任何人请求重绘 → 屏幕冻结在旧帧。
1102
- * 即"消失"这个动作本身把它自己的重绘通道关掉了。
1103
- * 故消失路径要自己 requestRender 一次。 */
1068
+ * 根因: 当时唯一的重绘入口是动画定时器里的 requestRender,而"任务全完成"恰好让
1069
+ * needAnim=false → 停定时器 → 从此没有任何人请求重绘 → 屏幕冻结在旧帧。
1070
+ * 动画已拆(2026-11-08),定时器不再存在,但这个坑改头换面仍然成立:
1071
+ * 重绘只能由事件主动请求,少一处就静默冻结。
1072
+ */
1104
1073
  function hidePanel(ui: any): void {
1105
1074
  try { ui.setWidget(PANEL_KEY, undefined) } catch {}
1106
- stopAnimTimer()
1107
1075
  try { lastTui?.requestRender?.() } catch {}
1108
1076
  }
1109
1077
 
@@ -1137,24 +1105,18 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
1137
1105
  who,
1138
1106
  width,
1139
1107
  (c: string, s: string) => theme.fg(c, s),
1140
- animPhase,
1141
1108
  sessionNickname,
1142
- effortLevel,
1109
+ // 面板主色跟 pi 输入框一致:从主题现取当前档位的 thinking 色(2026-11-09 用户要求)。
1110
+ thinkingAnsiOf(theme, effortLevel),
1143
1111
  ),
1144
1112
  invalidate: () => {},
1145
1113
  }
1146
1114
  },
1147
1115
  { placement: 'aboveEditor' },
1148
1116
  )
1149
- // 只在真有“进行中”任务时跑定时器 —— 否则白刷 CPU(无动画可播)。
1150
- const needAnim = data.rows.some((t) => t.state === '进行中')
1151
- if (needAnim) startAnimTimer(() => tuiRef)
1152
- else stopAnimTimer()
1153
- // ★ 无动画时得自己刷一帧。
1154
- // 若这里不主动刷,从“有任务”变成“无任务”时屏幕会冻结在旧帧
1155
- // (标题还写着旧计数)—— 与 2026-10-05 那个静默 bug 同一族。
1156
- // 仍在 needAnim 时不用管:定时器每帧会刷。
1157
- if (!needAnim) try { lastTui?.requestRender?.() } catch {}
1117
+ // 动画去掉后(2026-11-08),每次刷新都得自己请求重绘 ——
1118
+ // 没有定时器兜底了,漏这一句屏幕会冻结在旧帧(标题还写着旧计数)。
1119
+ try { lastTui?.requestRender?.() } catch {}
1158
1120
  } catch {
1159
1121
  hidePanel(ui)
1160
1122
  }
@@ -1337,7 +1299,6 @@ export default function absPiHook(pi: ExtensionAPI): void {
1337
1299
  })
1338
1300
 
1339
1301
  pi.on("session_shutdown", (_e: any, ctx: any) => {
1340
- stopAnimTimer() // 面板没了,动画定时器必须一起停(否则残留定时器白刷已丢弃的 tui)
1341
1302
  snapshotWrapup((ctx && ctx.cwd) || process.cwd())
1342
1303
  snapshotSessionPage((ctx && ctx.cwd) || process.cwd(), sessionIdFor(ctx))
1343
1304
  logHook("session_shutdown").catch(() => {})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@ description: "USE FOR abs / agent-brain-sync cross-session memory and task hando
9
9
 
10
10
  **Querying it is your decision, not something you wait to be reminded of.** The moment a
11
11
  keyword forms or a question appears ("was this bug solved before?", "who decided this?"),
12
- search — do not wait for an error or for the user to prompt you.
12
+ run `abs query <词>` — do not wait for an error or for the user to prompt you.
13
13
 
14
14
  ## Startup: one command
15
15
 
@@ -31,16 +31,17 @@ unfinished ones get `abs todo note <id> --note "where it stopped"`.
31
31
  - index rows without a real description (date-template only) → rejected by lint.
32
32
  - Sub-items are one sentence each. Reports go to `abs log` / `sources/`, not the board.
33
33
 
34
- ## Find knowledge: three tiers
34
+ ## Find knowledge: one entry
35
35
 
36
36
  | Situation | Command |
37
37
  |---|---|
38
- | Stuck, no keywords | `abs recall [词…]` — infers from current work |
39
- | Have keywords, want best recall | `abs search <词>` — luminance + neighbor spread, cross-project |
40
- | Want exact literal, short output | `abs query <词…>` — multi-word OR |
38
+ | Have keywords | `abs query <词…>` — multi-word OR over `.brain/`, page name + summary |
39
+ | Got a page name | `abs resolve <页名>` → read only that page |
41
40
 
42
- `search` and `query` are detailed vs short, not duplicates. MCP: `abs_search` / `abs_query`.
43
- Got a page name? `abs resolve <页名>` → read only that page. Cite sources as `[[页面名]]`.
41
+ One entry, no tiers: `query` is the only search (2026-11-08 — the SQLite graph库 with
42
+ `abs search` / `abs recall` / `abs db` was deleted; it was write-only in practice:
43
+ 881 sessions logged `search` 3 times, `recall` 9 times, while the todo board ran 1897).
44
+ Cite sources as `[[页面名]]`.
44
45
 
45
46
  ## Write knowledge: three sinks, do not mix
46
47
 
@@ -60,7 +61,8 @@ Then run `abs lint` (dead links, over-size, NO-TAIL).
60
61
  abs todo start <id> --note "做什么" # register (note required)
61
62
  abs todo note <id> --note "到哪一步" # append child (one sentence)
62
63
  abs todo done <id> --as 落地|否决|仅方案 "结语"
63
- abs todo state <id> --note 进行中|讨论中|滞留中|搁置
64
+ abs todo state <id> --note 进行中|讨论中|滞留中|搁置 # 任务本身
65
+ abs todo state <id> --note 滞留中 --n 2 # 第 2 条子项(子项也带四态)
64
66
  abs todo remove <id> # delete incl. children, no undo
65
67
  ```
66
68
 
@@ -88,7 +90,7 @@ Exception: the user types `abs` alone or `abs` + command → execute; no subcomm
88
90
  ## ⛔ Never
89
91
 
90
92
  1. Hand-build skeletons or hand-register tasks — `abs init` / `abs todo start`.
91
- 2. Read `.brain/` end-to-end — `abs load` for state, search for topics, resolve for one page.
93
+ 2. Read `.brain/` end-to-end — `abs load` for state, `abs query` for topics, `abs resolve` for one page.
92
94
  3. Delete a knowledge page to mark it obsolete — `abs supersede <页> --by <新页>`.
93
95
  4. Write Rules directly — propose `[Rules 提议]`, wait for the user, then `abs rule add`.
94
96
  5. Host-native todo for cross-session work — use `abs todo`.
@@ -18,7 +18,6 @@ import { registerInIndex } from '../note.js';
18
18
  import { cmdLog } from '../note.js';
19
19
  import { clip, slugOf } from '../text.js';
20
20
  import { LAYERS, layerOf, DEFAULT_LAYER } from './layers.js';
21
- import { autoIndex } from './db-cmd.js';
22
21
  import { summonTail } from './summon.js';
23
22
 
24
23
  /** 层 → 落点目录。L1/L6 不立页,由下面的分支单独处理。 */
@@ -197,10 +196,6 @@ export async function cmdCapture({ dir, text, layer, tags, title, kind }) {
197
196
  await registerInIndex(root, section, slug, head);
198
197
  await cmdLog({ dir: root, title: clean, kind: 'note', summon: false });
199
198
 
200
- // 落盘即入库 —— 否则用户以为记下了,检索却空手而归(静默失效)。
201
- // autoIndex 永不抛错也不阻塞:库坏了只是检索旧一点,不能让记笔记失败。
202
- const idx = await autoIndex();
203
-
204
199
  // 每次总结都顺手查一遍会话历史,避免漏下没总结的会话(2026-11-02 用户要求)。
205
200
  // 只给线索不写盘 —— 落哪一条仍是人的判断。
206
201
  const tail = await summonTail(root);
@@ -209,7 +204,6 @@ export async function cmdCapture({ dir, text, layer, tags, title, kind }) {
209
204
  `✓ 已落入 L${L} ${meta.name}层 → ${dirName}/${file}`,
210
205
  ` ${clip(clean, 60)} ${atTag(who)}`,
211
206
  ` 该层衰减 ${meta.decay}(${meta.note})`,
212
- idx.ok ? ' 已同步入库,abs db search 可查到' : ` (未入库:${idx.reason})`,
213
207
  ].join('\n') + tail;
214
208
  }
215
209