@fanchao8609/agent_brain_sync 1.28.6 → 1.28.7

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
@@ -340,10 +340,10 @@ function parseArgv(args) {
340
340
  }
341
341
  const usage = `abs — 跨会话记忆(.brain/ 图谱)
342
342
 
343
- 怎么用:按你要做的事找,不用记命令名。旧名都还能用,会自动提示新名。
343
+ 怎么用:按你要做的事找。相近命令已折叠,旧别名已删(index/status/summon/db search → show/search/ingest)。
344
344
 
345
345
  【开工】读状态,接上次的活
346
- abs load 必跑。Rules + 图谱计数 + 任务板 + 最近流水
346
+ abs load 必跑。未完成 todo + 用法 + 最近日志
347
347
  abs recall [词…] 卡住时用:按当前处境推相关页(不给词也行)
348
348
 
349
349
  【找东西】
@@ -380,9 +380,7 @@ const usage = `abs — 跨会话记忆(.brain/ 图谱)
380
380
  abs update 升级并刷新 hook/skill
381
381
  abs config set user <名字> 使用者姓名(写操作需先设置)
382
382
 
383
- 旧名(仍可用,会提示新名):
384
- index/log/status → show | capture → concept | db search → search
385
- summon → ingest --list | db add → init
383
+ 视图折叠: show index|log|status 写知识: note / concept / rule
386
384
 
387
385
  注: wrapup / teardown-check 是 hook 内部命令。
388
386
  `;
@@ -390,7 +388,7 @@ const usage = `abs — 跨会话记忆(.brain/ 图谱)
390
388
  /** 子命令级用法(abs <cmd> --help 时打印)。 */
391
389
  const subUsage = {
392
390
  load: [
393
- 'abs load — 开机读状态(Rules 全量 + 图谱计数 + todo 看板 + 最近 log)',
391
+ 'abs load — 开机读状态(未完成 todo + 用法 + 最近 log)',
394
392
  '',
395
393
  '用法:',
396
394
  ' abs load [--dir <项目根>]',
@@ -643,28 +641,12 @@ async function main() {
643
641
  else console.log(`✗ 未知视图 "${v}"。可用: index | log | status`);
644
642
  break;
645
643
  }
646
- case 'index':
647
- rejectExtra(opts._, 'abs index');
648
- console.log('(abs index 已改名 → abs show index)');
649
- console.log(await cmdShow({ dir: opts.dir, view: 'index' }));
650
- break;
651
644
  case 'log': {
652
- // 无参=查看 log.md;带参=追加一行 (用户/AI 主动记; hook 生命周期事件走技术日志 hooks.log, 不经这里)
653
- // ★ log 有双职责,不能并进 show:带参时它是**写**操作。
654
- // 所以只把「无参查看」这一半指向 show,写的那半保持不变、也不提示改名。
655
- if (opts._.length) {
656
- console.log(await cmdLog({ dir: opts.dir, title: opts._.join(' '), kind: opts.kind }));
657
- } else {
658
- console.log('(查看流水已改名 → abs show log;abs log "完成 X" 仍是记成果)');
659
- console.log(await cmdShow({ dir: opts.dir, view: 'log' }));
660
- }
645
+ // 只剩写:abs log "结论"。查看流水 = abs show log(2026-11-08 去掉无参查看的别名)。
646
+ if (!opts._.length) throw new Error('✗ 用法: abs log "一句话结论" [--kind dev|note|concept|ingest]\n 查看流水: abs show log');
647
+ console.log(await cmdLog({ dir: opts.dir, title: opts._.join(' '), kind: opts.kind }));
661
648
  break;
662
649
  }
663
- case 'status':
664
- rejectExtra(opts._, 'abs status');
665
- console.log('(abs status 已改名 → abs show status)');
666
- console.log(await cmdStatus({ dir: opts.dir }));
667
- break;
668
650
  // abs todo —— 无子命令=看板;带子命令=任务写操作
669
651
  case 'todo': {
670
652
  const [sub, id, ...rest2] = opts._;
@@ -721,13 +703,6 @@ async function main() {
721
703
  console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note, as: opts.as, session: opts.session }));
722
704
  break;
723
705
  }
724
- case 'summon': {
725
- // 别名(2026-11-06):并进 ingest --list —— 两者是同一件事的两半
726
- // (summon 说「哪些没落页」,ingest 负责落)。分两个顶层命令只是多记一个词。
727
- console.log('(abs summon 已改名 → abs ingest --list)');
728
- console.log(await cmdSummon({ dir: opts.dir, mark: !!opts.mark, full: !!opts.full, status: !!opts.status, limit: Number(opts.limit) || 15 }));
729
- break;
730
- }
731
706
  case 'ingest': {
732
707
  // --list:只看「还有哪些会话没落页」,不写入(原来的 abs summon)
733
708
  if (opts.list) {
@@ -784,15 +759,7 @@ async function main() {
784
759
  }
785
760
  case 'db': {
786
761
  const sub = opts._[0];
787
- const rest = opts._.slice(1);
788
762
  if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
789
- if (sub === 'search') {
790
- // 别名(2026-11-06):顶层已改叫 abs search。留着是为了不打断现有习惯与脚本,
791
- // 但每次提示一次 —— 不提示的话旧名会永远活着,命令面又变宽。
792
- console.log('(abs db search 已改名 → abs search)');
793
- console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8, full: !!opts.full, noRules: !!opts['no-rules'] }));
794
- break;
795
- }
796
763
  if (sub === 'status') { console.log(await cmdDbStatus()); break; }
797
764
  if (sub === 'evolve') { console.log(await cmdDbEvolve({ staleDays: Number(opts.staleDays) || 90 })); break; }
798
765
  if (sub === 'add') {
@@ -800,7 +767,7 @@ async function main() {
800
767
  console.log(`✓ 已登记项目: ${r}\n 建库: abs db rebuild`);
801
768
  break;
802
769
  }
803
- console.log('用法: abs db rebuild | search <词> | status | evolve | add [--dir <项目根>]');
770
+ console.log('用法: abs db rebuild | status | evolve | add [--dir <项目根>]');
804
771
  break;
805
772
  }
806
773
  case 'query': {
@@ -72,31 +72,24 @@ function dropCols(s: string, n: number): string {
72
72
  }
73
73
 
74
74
  /**
75
- * 推理强度 → 面板与输入框的主色(2026-11-07 用户需求)。
76
- * 色值起于用户 2026-11-07 给的六档灰蓝,之后用户要求「xhigh/high 再亮一点,往下都稍亮」,
77
- * 按**同色相**(H≈219°)提亮度得到现在这组:
78
- * max #ACB8CD(不动) · xhigh #9CABC7 · high #8D97AA · medium #747E93
79
- * low #656F83 · minimal #586171
80
- * 提亮前用户给的原值:xhigh #95A5C3 · high #768298 · medium #697387 · low #5C6577 · minimal #505867
81
- * 色相是用户挑的灰蓝(不是绿),换色只许按用户给的值改,别拿主题色或自己的审美去替换。
82
- * off / 未知 → 空串,表示「不上色,沿用默认色」—— 用户 2026-11-06 定过:推理关着就保持原样。
75
+ * 推理强度 → 主题色 token。空串表示「不上色,沿用默认色」。
83
76
  *
84
- * 为何自己拼 ANSI 而不是走 theme.fg:fg() 只认主题里的 token 名,主题那七档色相跟这组不一样。
85
- * 终端支持 24 位色就给 38;2,否则退到 256 色板里的一档(见 ansi256)。
86
- * ★ 256 色板里没有这种灰蓝:只能退到灰阶档(250/249/247/244/243/241),保住“一档比一档暗”、
87
- * 丢掉蓝调 —— 24 位色终端才是标称效果。
77
+ * 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
78
+ * 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
79
+ * (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
80
+ * 「off 时保持默认色」。
88
81
  *
89
- * ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortAnsi`)。不抽公共模块是故意的:
82
+ * ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortColorToken`)。不抽公共模块是故意的:
90
83
  * 安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,抽成共享文件会让
91
84
  * 「删掉输入框样式目录」连带把 todo 面板搞崩。
92
85
  */
93
86
  export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
94
- max: { rgb: '172;184;205', ansi256: 250 },
95
- xhigh: { rgb: '156;171;199', ansi256: 249 },
96
- high: { rgb: '141;151;170', ansi256: 247 },
97
- medium: { rgb: '116;126;147', ansi256: 244 },
98
- low: { rgb: '101;111;131', ansi256: 243 },
99
- minimal: { rgb: '88;97;113', ansi256: 241 },
87
+ max: { rgb: '197;205;220', ansi256: 189 },
88
+ xhigh: { rgb: '168;182;204', ansi256: 152 },
89
+ high: { rgb: '150;166;198', ansi256: 110 },
90
+ medium: { rgb: '122;138;160', ansi256: 110 },
91
+ low: { rgb: '94;108;132', ansi256: 103 },
92
+ minimal: { rgb: '70;82;106', ansi256: 240 },
100
93
  }
101
94
 
102
95
  export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
@@ -46,18 +46,12 @@ export class RoundedEditor extends CustomEditor {
46
46
  if (lines.length < 2) return lines
47
47
 
48
48
  const borderColor = (this as any).borderColor?.bind(this)
49
- // 文字色 + 边框色 = 推理强度的主色;off(或取不到)时为空 → 文字不上色、边框交回 pi 自己的边框色。
50
- // 选 24 位色还是 256 色,问主题自己的 getColorMode(),与 pi 其他文字一致。
51
- const themeNow = themeGetter?.()
52
- let colorMode = 'truecolor'
53
- try { colorMode = String(themeNow?.getColorMode?.() || 'truecolor') } catch {}
54
- const ansi = effortAnsi(effortLevel, colorMode)
55
- const effortFg = ansi ? (s: string) => paintAnsi(ansi, s) : null
56
- // ★ 边框也跟着上主色(2026-11-07 用户补提:「输入框也是要改颜色」)。
57
- // 前一轮只染了文字,结果输入框空着时框里没字、屏幕上只剩 pi 那圈紫/橙色边框,
58
- // 看起来就像完全没改 —— 边框不跟着变,这个功能肉眼就看不见。
59
- // roundBorder 本来就是「剥掉原色 → 整行重新上色」,故把上色函数传进 wrap 即可。
60
- const wrap = effortFg ?? ((s: string) => (borderColor ? borderColor(s) : s))
49
+ // 现代暗色方案(2026-11-08):文字 #F8FAFC,边框 #444C56,不再跟推理强度变色。
50
+ // 光标色 / 输入框背景 / 聚焦外发光是 pi 编辑器内部画的,扩展接管不了,先不动。
51
+ const colorMode = (() => { try { return String(themeGetter?.().getColorMode()) } catch { return 'truecolor' } })()
52
+ const textFg = (s: string) => paintAnsi(effortAnsi(effortLevel, colorMode), s)
53
+ const lineFg = textFg
54
+ const wrap = lineFg
61
55
  const out: string[] = []
62
56
 
63
57
  // 上边框:原本是整行 `─`(含 working 图标与滚动提示),两端换圆角,右端贴状态。
@@ -67,11 +61,9 @@ export class RoundedEditor extends CustomEditor {
67
61
  const bottomIdx = lines.length - 1 - autocompleteHeight
68
62
 
69
63
  for (let i = 1; i < bottomIdx; i++) {
70
- const body = lines[i]
71
- // 推理强度主色染**输入文字**(2026-11-06 用户需求,2026-11-07 换成用户指定的六档色)。
72
- // 边框同色(见上面 wrap)。整行包裹而不逐字:光标由 \x1b[7m 反转标记,与前景色互不干扰;
73
- // 输入框内容是用户手打的纯文本,行内不会有 reset 把后半行冲掉。
74
- out.push(wrap(SIDE) + (effortFg ? effortFg(body) : body) + wrap(SIDE))
64
+ // 剥掉 pi 原生文字色,再包 #F8FAFC —— 否则内层色码赢。
65
+ const body = lines[i].replace(/\x1b\[(?!7m)[0-9;]*m/g, '')
66
+ out.push(wrap(SIDE) + textFg(body) + wrap(SIDE))
75
67
  }
76
68
  // 下边框(2026-11-07 还原:左下角的 fanchao·昵称已去掉 —— 用户嫌难看,
77
69
  // 回 todo 面板标题;roundBorder 的 leftLabel 参数保留备用)。
package/hooks/abs.omp.ts CHANGED
@@ -542,8 +542,7 @@ export function todoAnimFrame(phase: number): string {
542
542
  * 规则:`thinking` + 首字母大写(thinkingMinimal…thinkingMax)。
543
543
  * off 返回空串而不是 `thinkingOff`:那个色接近背景,染文字会看不清(2026-11-06 用户定)。
544
544
  *
545
- * ★ 与 pi 版不再字面对应:pi 版 2026-11-07 已改成绿色阶(`effortAnsi`,用户要求),
546
- * omp 这边仍跟主题的 thinkingXxx 七色 —— 用户只提了 pi 面板。两个模板分别装到不同宿主,
545
+ * ★ 与 pi 版(hooks/abs.pi.ts 的 effortToken)逐字同义,但不共享:两个模板分别装到不同宿主,
547
546
  * 独立成文件才能各自删除/升级。
548
547
  */
549
548
  export function effortToken(level: string | null | undefined): string {
package/hooks/abs.pi.ts CHANGED
@@ -936,9 +936,6 @@ function clipToWidth(s: string, width: number): string {
936
936
  // 为何用盲文点阵而不是 ⠋⠙⠹ 之外的 ASCII 旋转符:
937
937
  // ⠿ 系符号在等宽字体里都是 1 列宽,且视觉上是个转动的方框,比 | / - \ 均匀。
938
938
  // 为何帧宽必须恒定:面板每 250ms 重绘,宽度一变整行就会左右抖。盲文恰好全是 1 列。
939
- /** 进行中那行的小动画(2026-11-07 用户换掉了原来的盲文十帧旋转 ⠋⠙⠹…,嫌难看,改要圆圈)。
940
- * 4 帧一圈 × 200ms ≈ 0.8 秒转一圈 —— 与旧动画(10 帧 × 100ms = 1 秒)节奏相当,
941
- * 帧数少了若不降速会转成一团花。 */
942
939
  const TODO_ANIM_FRAMES = ['◐', '◓', '◑', '◒']
943
940
  const TODO_ANIM_MS = 200
944
941
 
@@ -974,31 +971,25 @@ export function todoAnimFrame(phase: number): string {
974
971
  return TODO_ANIM_FRAMES[i]
975
972
  }
976
973
 
977
- /** 推理强度 → 面板与输入框的主色(2026-11-07 用户需求)。
978
- * 色值起于用户 2026-11-07 给的六档灰蓝,之后用户要求「xhigh/high 再亮一点,往下都稍亮」,
979
- * 按**同色相**(H≈219°)提亮度得到现在这组:
980
- * max #ACB8CD(不动) · xhigh #9CABC7 · high #8D97AA · medium #747E93
981
- * low #656F83 · minimal #586171
982
- * 提亮前用户给的原值:xhigh #95A5C3 · high #768298 · medium #697387 · low #5C6577 · minimal #505867
983
- * 色相是用户挑的灰蓝(不是绿),换色只许按用户给的值改,别拿主题色或自己的审美去替换。
984
- * off / 未知 → 空串,表示「不上色」:调用方用灰阶(用户 2026-11-06 定过,推理关着就别发光)。
974
+ /**
975
+ * 推理强度 → 主题色 token。空串表示「不上推理色,用调用方给的兼底色」。
985
976
  *
986
- * 为何自己拼 ANSI 而不是 theme.fg:fg() 只认主题里的 token 名,主题那七档色相跟这组不一样。
987
- * 终端支持 24 位色就给 38;2,否则退到 256 色板最近的一档(见 ansi256)。
988
- * ★ 256 色板里没有这种灰蓝:只能退到灰阶档(250/249/247/244/243/241),保住“一档比一档暗”、
989
- * 丢掉蓝调 —— 24 位色终端才是标称效果。
977
+ * 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
978
+ * 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
979
+ * (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
980
+ * 「off 时保持默认色」。
990
981
  *
991
- * ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`effortAnsi`)。
982
+ * ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`thinkingColorToken`)。
992
983
  * 不抽公共模块是故意的:安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,
993
984
  * 抽成共享文件会让「删掉输入框样式目录」连带把 todo 面板搞崩。
994
985
  */
995
986
  export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
996
- max: { rgb: '172;184;205', ansi256: 250 },
997
- xhigh: { rgb: '156;171;199', ansi256: 249 },
998
- high: { rgb: '141;151;170', ansi256: 247 },
999
- medium: { rgb: '116;126;147', ansi256: 244 },
1000
- low: { rgb: '101;111;131', ansi256: 243 },
1001
- minimal: { rgb: '88;97;113', ansi256: 241 },
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 },
1002
993
  }
1003
994
 
1004
995
  export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
@@ -1020,23 +1011,31 @@ export function renderPanelLines(
1020
1011
  animPhase = -1,
1021
1012
  nickname = '',
1022
1013
  effort = '',
1023
- colorMode = 'truecolor',
1024
1014
  ): string[] {
1025
1015
  // ★ 空看板不再直接返回空数组(2026-10-06 用户要求:「即便 todo 没有列表,也要在 pi 上显示」)。
1026
1016
  // 理由与看板段一致:面板是「机制在不在」的常驻指示,进去就看不见 = 不知道机制活着。
1027
1017
  // 下面仍要画框和标题,所以这里只是不再提前 return。
1028
1018
  //
1029
1019
  // ★ 主色跟推理强度走(2026-11-06 用户需求):标题、[状态]、树线 ├─└─、复选框 [ ]、
1030
- // 进行中动画同用一色,与 pi 输入框文字同色系。
1031
- // ★ 2026-11-07 用户把色子换成自己指定的六档主色:max 最亮 → minimal 最暗(见 effortAnsi)。
1032
- // ★ 2026-11-07 用户再改:面板里**所有文字都用这一色**(标题、状态、id、描述、子项、+N more),
1033
- // 不再给描述/子项降成灰 —— 拿“颜色深浅”区分主次的写法作废了,层次只靠符号与缩进。
1034
- // 别把描述改回 muted:那是用户主动去掉的(原话「所有文字都一致吧」)。
1035
- // ★ off 时整块面板用灰阶(2026-11-06 用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
1020
+ // 进行中动画同用一色,与 pi 输入框描边/文字同色系。
1021
+ // ★ off 时整块面板用灰阶(同日用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
1036
1022
  // 高亮色只在真的开着推理时给,否则「推理关了面板还在发光」让人误以为还在深度思考。
1037
- // 既然要求“一致”,off 也用同一个 muted,不再分两阶灰。
1038
- const mainAnsi = effortAnsi(effort, colorMode)
1039
- const main = mainAnsi ? (s: string) => paintAnsi(mainAnsi, s) : (s: string) => fg('muted', s)
1023
+ // 主色用 muted(中灰),描述与子项降一阶到 dim(深灰),两阶灰保留层次,不一整片平。
1024
+ // 现代暗色方案(2026-11-08 用户给定),不再跟推理强度走
1025
+ const C = {
1026
+ title: '38;2;255;215;0',
1027
+ task: '38;2;226;232;240',
1028
+ desc: '38;2;148;163;184',
1029
+ tree: '38;2;150;170;198',
1030
+ doing: '38;2;16;185;129',
1031
+ talk: '38;2;59;130;246',
1032
+ hold: '38;2;245;158;11',
1033
+ stuck: '38;2;139;92;246',
1034
+ done: '38;2;100;116;139',
1035
+ }
1036
+ const stateAnsi = (st: string): string =>
1037
+ st === '进行中' ? C.doing : st === '讨论中' ? C.talk : st === '搁置' ? C.hold : st === '滞留中' ? C.stuck : C.task
1038
+ const paint = (ansi: string, t: string) => paintAnsi(ansi, t)
1040
1039
  const lines: string[] = []
1041
1040
  // 上描边行已删除(2026-11-04 用户要求「去掉上边框」)。
1042
1041
  // 历史:那行曾用 `thinkingOff` 对齐 pi 输入框描边色(排查三轮才定色)。
@@ -1048,7 +1047,7 @@ export function renderPanelLines(
1048
1047
  const title = whoPart ? `Todo (${data.total}) — ${whoPart}` : `Todo (${data.total})`
1049
1048
  // 标题也顶最左(2026-11-04 用户)。旧代码曾在空看板时多缩 1 格,2026-11-06 空看板
1050
1049
  // 已整块不显示(见 refreshTodoPanel),那个分支没意义了。
1051
- lines.push(clipToWidth(main(title), width))
1050
+ lines.push(clipToWidth(paint(C.title, title), width))
1052
1051
 
1053
1052
  const lastIdx = data.rows.length - 1
1054
1053
  data.rows.forEach((t, i) => {
@@ -1060,33 +1059,36 @@ export function renderPanelLines(
1060
1059
  // 进行中的行带一个小动画(其余行不加,避免满屏都在跳)
1061
1060
  // 单字符 spinner 紧跟 [进行中]:靠前放才能一眼看到(放行尾会被长描述挤到前面去看不见)。
1062
1061
  // 2026-11-06 用户要求:「放在 [进行中] 后面」。
1063
- const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + main(todoAnimFrame(animPhase)) : ''
1062
+ const hasState = ['进行中', '讨论中', '搁置', '滞留中'].includes(t.state)
1063
+ const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + paint(C.doing, todoAnimFrame(animPhase)) : ''
1064
1064
  // 复选框 `[ ]`(2026-10-16 用户要求):面板是 todo.md 的只读视图,
1065
1065
  // 就该长成 todo 原貌 —— 摆成与文件里 `- [ ]` 同形,读到就认得出对应哪一行。
1066
- const box = main('[ ]')
1067
- // 状态标签、id、描述全跟主色(2026-11-06 定:靠符号和文字区分状态,不靠颜色;
1068
- // 2026-11-07 再定:描述也不单独降灰)。
1069
- const head = `${main(branch)} ${box} ${main(`[${t.state}]`)}${anim} ${main(t.id)}`
1070
- const desc = t.desc ? ' ' + main(`— ${t.desc}`) : ''
1066
+ const eff = effortAnsi(effort, 'truecolor') || C.desc
1067
+ const box = hasState ? '' : paint(eff, '[ ] ')
1068
+ const stPart = hasState
1069
+ ? paint(stateAnsi(t.state), `[${t.state}]`)
1070
+ : paint(eff, '[待办]')
1071
+ const head = `${paint(C.tree, branch)} ${box}${stPart}${anim} ${paint(eff, t.id)}`
1072
+ const desc = t.desc ? ' ' + paint(C.desc, `— ${t.desc}`) : ''
1071
1073
  lines.push(clipToWidth(head + desc, width))
1072
1074
  if (t.kids && t.kids.length) {
1073
1075
  // 子项:标准 md 嵌套列表 → 画成树
1074
1076
  // ├─ [ ] 子项一
1075
1077
  // └─ [x] 子项二
1076
1078
  // 树线前缀 = 父任务的 │(非末条)或空格(末条),与父任务分支对齐。
1077
- // 2026-11-07 用户要求全面板同色:勾过的子项也不再压暗,完成与否靠 [x] 区分。
1078
- const stem = isLast ? ' ' : main('│ ')
1079
+ // 勾过的用 dim 压暗(一眼看出哪些已完成),未勾用正常色。
1080
+ const stem = isLast ? ' ' : paint(C.tree, '│ ')
1079
1081
  const n = t.kids.length
1080
1082
  t.kids.forEach((k, ki) => {
1081
1083
  const kb = ki === n - 1 ? '└─' : '├─'
1082
- const kbox = main(k.done ? '[x]' : '[ ]')
1083
- const ktext = main(k.text)
1084
- lines.push(clipToWidth(`${stem} ${main(kb)} ${kbox} ${ktext}`, width))
1084
+ const kbox = paint(eff, k.done ? '[x]' : '[ ]')
1085
+ const ktext = k.done ? `\x1b[9m${paint(C.done, k.text)}\x1b[29m` : paint(C.desc, k.text)
1086
+ lines.push(clipToWidth(`${stem} ${paint(C.tree, kb)} ${kbox} ${ktext}`, width))
1085
1087
  })
1086
1088
  }
1087
1089
  })
1088
1090
 
1089
- if (data.hidden > 0) lines.push(clipToWidth(main(` +${data.hidden} more`), width))
1091
+ if (data.hidden > 0) lines.push(clipToWidth(paint(C.title, ` +${data.hidden} more`), width))
1090
1092
  return lines
1091
1093
  }
1092
1094
  /** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
@@ -1125,11 +1127,6 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
1125
1127
  (tui: any, theme: any) => {
1126
1128
  tuiRef = tui
1127
1129
  lastTui = tui // 供 hidePanel 在"无动画"时也能请求重绘
1128
- // 主色要按终端的色深选编码(24 位色 / 256 色)—— getColorMode() 是 pi 主题自己
1129
- // 算好的那一个;取不到就当支持 24 位色(真拿不准时宁可亮一点也别一格都不出)。
1130
- const colorMode = (() => {
1131
- try { return String(theme?.getColorMode?.() || 'truecolor') } catch { return 'truecolor' }
1132
- })()
1133
1130
  return {
1134
1131
  render: (width: number) =>
1135
1132
  renderPanelLines(
@@ -1140,7 +1137,6 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
1140
1137
  animPhase,
1141
1138
  sessionNickname,
1142
1139
  effortLevel,
1143
- colorMode,
1144
1140
  ),
1145
1141
  invalidate: () => {},
1146
1142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.28.6",
3
+ "version": "1.28.7",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,107 +1,107 @@
1
1
  ---
2
2
  name: abs-agent-brain-sync
3
- description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。触发词:用户直接输入 abs(单独一个词或 abs + 命令,如 'abs 开工'、'abs 沉淀'),以及「开工续接状态」「沉淀会话收获」「任务登记不丢」「经验落成知识页」「图谱体检」等任务。"
3
+ description: "USE FOR abs / agent-brain-sync cross-session memory and task handoff. Triggers: the user types `abs` alone or `abs` + a command (e.g. 'abs 开工', 'abs 沉淀'), or asks to 开工续接状态 / 沉淀会话收获 / 任务登记不丢 / 经验落成知识页 / 图谱体检."
4
4
  ---
5
5
 
6
- # abs — 跨会话记忆 (agent-brain-sync)
6
+ # abs — cross-session memory
7
7
 
8
- `.brain/` 是**你自己的跨会话记忆库**:踩过的坑、做过的决定、验证过的做法都在里面。
8
+ `.brain/` is your memory across sessions: bugs, decisions, verified fixes.
9
9
 
10
- **查它是你的决定,不是别人提醒你才做。** 干活中一旦心里形成关键词、对某件事有疑问
11
- (这个坑以前踩过吗?这个约定是谁定的?),那一刻就主动查 —— 不要等报错、不要等用户提醒。
10
+ **Querying it is your decision, not something you wait to be reminded of.** The moment a
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
13
 
13
- ## 开工:跑一句就够
14
+ ## Startup: one command
14
15
 
15
16
  ```bash
16
17
  abs load
17
18
  ```
18
19
 
19
- 它给:Rules(铁律,最先读)+ 图谱计数 + 任务板 + 最近流水 + 未填会话页提醒。
20
- 顶部若有 `⏳ 上会话滞留`,先对账再开工 —— 做完的 `abs todo done <id>`,
21
- 没做完的 `abs todo note <id> --note "接到哪"`。
20
+ Returns: unfinished todos + usage + last 3 log lines. If the top shows `⏳ 上会话滞留`,
21
+ reconcile first — finished items get `abs todo done <id> --as 落地|否决|仅方案 "结语"`,
22
+ unfinished ones get `abs todo note <id> --note "where it stopped"`.
22
23
 
23
- **绝不 `cat .brain/` 通读**。要主题用检索,要单页用 `abs resolve` 后只读那一页。
24
+ **Never `cat .brain/` end-to-end.** Search by topic; `abs resolve <page>` then read that one page.
24
25
 
25
- ## 找东西:三档,按精确度选
26
+ ## Format gates (reject, do not guess)
26
27
 
27
- | 你在干什么 | 用哪个 |
28
- |---|---|
29
- | 不知道找什么,卡住了 | `abs recall [词…]` —— 按当前处境推(不给词也行) |
30
- | 知道关键词,要最准 | `abs search <词>` —— 亮度排序 + 邻居扩散 + 跨项目 |
31
- | 要字面精确、输出短 | `abs query <词…>` —— 多词 OR 命中 |
28
+ - `todo start/add` without `--note "what"` → rejected. One sentence, verb-first, ≤40 chars.
29
+ - `todo done` without a conclusion → rejected. Every Done line must say what happened.
30
+ - `abs log` without a one-line conclusion → rejected.
31
+ - index rows without a real description (date-template only) → rejected by lint.
32
+ - Sub-items are one sentence each. Reports go to `abs log` / `sources/`, not the board.
32
33
 
33
- `abs search` 与 `abs query` 是**详略两档,不是重复**:前者带亮度和扩散更准,
34
- 后者只给页名+摘要更省 token。MCP 侧分别对应 `abs_search` / `abs_query`。
34
+ ## Find knowledge: three tiers
35
35
 
36
- 拿到页名后 `abs resolve <页名>` 反查路径,只读命中的那几页。答时用 `[[页面名]]` 标来源。
36
+ | Situation | Command |
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 |
37
41
 
38
- ## 写知识:三个落点,别混
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 `[[页面名]]`.
39
44
 
40
- | 你手上的东西 | 落点 | 命令 |
41
- |---|---|---|
42
- | 一句经验,还没想清 | `sources/` 待提炼 | `abs note "一句话"` |
43
- | 要成页(有骨架好填) | `concepts/` | `abs concept <slug> --title "…"` |
44
- | 一句话记下,日期即时间线 | `concepts/` | `abs capture --layer 2/3/4 "…"` |
45
- | 违反就必须停下的硬约束 | index 的 `## Rules` | `abs rule add "≤42 字"` |
45
+ ## Write knowledge: three sinks, do not mix
46
46
 
47
- `concept` 与 `capture` 都写 `concepts/`,但产出不同:前者带四段骨架(触发场景/表现/解法/验证),
48
- 后者只落一句话 + 日期前缀。要「写细」用 concept,要「快记」用 capture。
47
+ | What you have | Sink | Command |
48
+ |---|---|---|
49
+ | One experience, not yet clear | `sources/` | `abs note "一句话"` |
50
+ | A real page (4-section skeleton) | `concepts/` | `abs concept <slug> --title "…"` |
51
+ | One line, date as timeline | `concepts/` | `abs capture --layer 2/3/4 "…"` |
52
+ | Hard rule that must stop the agent | index `## Rules` | `abs rule add "≤42 字"` |
49
53
 
50
- **写完跑 `abs lint`**,它报死链、超限、无尾(`NO-TAIL`:页要有「做完怎么确认」那段)。
54
+ `concept` = four sections (trigger / symptom / fix / verify). `capture` = one line + date.
55
+ Then run `abs lint` (dead links, over-size, NO-TAIL).
51
56
 
52
- ## 写任务:todo 是活看板
57
+ ## Tasks: the board is alive
53
58
 
54
59
  ```bash
55
- abs todo add <id> --note "做什么" # 登记
56
- abs todo note <id> --note "到哪一步" # 追加子项(一句话)
60
+ abs todo start <id> --note "做什么" # register (note required)
61
+ abs todo note <id> --note "到哪一步" # append child (one sentence)
57
62
  abs todo done <id> --as 落地|否决|仅方案 "结语"
58
63
  abs todo state <id> --note 进行中|讨论中|滞留中|搁置
59
- abs todo remove <id> # 删除(连子项,不可恢复)
64
+ abs todo remove <id> # delete incl. children, no undo
60
65
  ```
61
66
 
62
- - **开工前先登记**:要动文件/跑命令就先 `start`,名字粗糙没关系(事后可 `rename`)。
63
- - **一段活干完立刻 done**:别攒着等大需求收尾。
64
- - `搁置` = 先不做、还留在看板上;`remove` = 当作没登记过。**两者别混**。
65
- - 随手记多句发现用 `abs todo memo <id> "…"`,不占看板。
66
-
67
- ## 触发器:意图,不是关键词
67
+ - Register before touching files. A rough id is fine (`rename` later).
68
+ - `done` immediately when a unit of work finishes; do not batch at session end.
69
+ - One `[进行中]` at a time. `搁置` = parked on the board; `remove` = never existed.
70
+ - `memo` for extra notes; it does not occupy the board.
68
71
 
69
- 句子里出现 todo / 沉淀 / load / note 这些词,若是**谈论、提问、定规则**,只回应不执行。
72
+ ## Triggers are intent, not keywords
70
73
 
71
- 唯一例外:用户直接输入 `abs`(单独一个词或 `abs` + 命令,如 `abs 开工`)—— 明确要用 abs,
72
- 直接执行;无子命令时走开工流程。
74
+ `todo` / `沉淀` / `load` / `note` in a sentence that is discussion, a question, or
75
+ rule-making → respond only, do not execute.
73
76
 
74
- ## 文档精简(写 todo / 知识库文档必须遵守)
77
+ Exception: the user types `abs` alone or `abs` + command → execute; no subcommand = startup.
75
78
 
76
- 1. 所有内容服务于主题目标,无关的直接去掉。
77
- 2. 不写没验证过的经验和总结。
78
- 3. 层次分明,按前因后果组织,不用自造词。
79
- 4. 禁止反问、猜测。
80
- 5. 拒绝重复,已有文档写过的优先引用。
81
- 6. 禁令直接了当,不唠叨。
79
+ ## Document diet (todo / knowledge pages)
82
80
 
83
- ## ⛔ 禁止做
81
+ 1. Everything serves the topic; cut the rest.
82
+ 2. No unverified summaries. Only what was done, run, tested.
83
+ 3. Ordered, layered, plain declarative sentences. No coined words.
84
+ 4. No rhetorical questions, no guessing.
85
+ 5. No duplication — cite the other page instead.
86
+ 6. Bans are direct, not chatty.
84
87
 
85
- 1. 禁止手工建骨架 / 手工登记任务 —— 用 `abs init` / `abs todo add`。
86
- 2. 禁止通读 `.brain/` —— 要状态用 `abs load`,要主题用检索,要单页用 `abs resolve`。
87
- 3. 禁止删知识页表达"已失效" —— 用 `abs supersede <页> --by <新页>`。
88
- 4. 禁止直写 Rules —— 先提 `[Rules 提议]` 问用户,确认后 `abs rule add`。
89
- 5. 禁止用宿主原生 todo 承载跨会话任务 —— 用 `abs todo`。
90
- 6. 禁止编造没跑过的验证结论。
88
+ ## ⛔ Never
91
89
 
92
- ## 需要时再查的细节
90
+ 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.
92
+ 3. Delete a knowledge page to mark it obsolete — `abs supersede <页> --by <新页>`.
93
+ 4. Write Rules directly — propose `[Rules 提议]`, wait for the user, then `abs rule add`.
94
+ 5. Host-native todo for cross-session work — use `abs todo`.
95
+ 6. Invent verification results that were not run.
93
96
 
94
- 以下内容已移到图谱页,**不要凭记忆猜**,用到时读它:
97
+ ## Details live in the graph, not in memory
95
98
 
96
- | 要做什么 | 读哪页 |
99
+ | Need | Read |
97
100
  |---|---|
98
- | 七层怎么判、`--kind` 怎么选、收尾深提炼全流程 | `abs resolve abs-skill-details` |
99
- | 会话页怎么落、三种骨架、与会话的挂钩 | `abs resolve session-digest-design` |
100
- | 新需求受理协议(先方案→登记→问开工) | `abs resolve abs-skill-details` |
101
- | `.brain/` 目录结构、知识页 frontmatter 格式 | `abs resolve abs-skill-details` |
102
- | 体检报错码含义(NO-INBOUND / NO-TAIL / …) | `abs resolve abs-skill-details` |
103
-
104
- ## 自我约束
101
+ | Seven layers, `--kind`, deep wrap-up | `abs resolve abs-skill-details` |
102
+ | Session page shapes, ingest hooks | `abs resolve session-digest-design` |
103
+ | New-request protocol (plan → register → start) | `abs resolve abs-skill-details` |
104
+ | `.brain/` layout, frontmatter | `abs resolve abs-skill-details` |
105
+ | Lint codes (NO-INBOUND / NO-TAIL / …) | `abs resolve abs-skill-details` |
105
106
 
106
- 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外);
107
- 双链、frontmatter、index 必须自洽 —— 坏链等于掰断接力棒。
107
+ Only read/write `.brain/` and target code. Broken dual-links = broken handoff.
package/src/brain/load.js CHANGED
@@ -17,9 +17,18 @@ import { digestFilled } from './layers.js';
17
17
  * 为何要限:会话页数量只增不减,全列会让 load 首屏随规模膨胀
18
18
  * —— 违反本仓「读取侧输出不得随规模增长」。5 个够 agent 判断先填哪张。 */
19
19
  const SESSION_UNFILLED_SHOW = 5;
20
+ /** 开机屏的使用方法(2026-11-08 用户定 load 三件套之一)。一行一个动作,别写成手册。 */
21
+ const LOAD_USAGE = [
22
+ '--- 用法 ---',
23
+ '接经验 abs search <词> / abs recall 记一句 abs note "…"',
24
+ '立概念 abs concept <id> --title "…" 登记活 abs todo start <id> --note "做什么"',
25
+ '收尾 abs todo done <id> --as 落地|否决|仅方案 "结论"',
26
+ '体检 abs lint 看全量 abs show index|log|status',
27
+ '纪律: 绝不通读 .brain/;检索词一冒出来就查,别等报错。',
28
+ ].join('\n');
20
29
  import { checkBrainShape } from './shape.js';
21
- import { rulesSection } from './rules.js';
22
- import { collapseIndex } from '../page.js';
30
+ import { readRules } from './rules.js';
31
+
23
32
 
24
33
  // ---------- board: 看板 ----------
25
34
  export async function cmdBoard({ dir, full }) {
@@ -95,19 +104,27 @@ export async function cmdLoad({ dir }) {
95
104
  ''
96
105
  );
97
106
  }
107
+ // 2026-11-08 用户定:load 只留三样 —— 未完成 todo、使用方法、最近日志。
108
+ // Rules 全文 / 图谱计数 / Done 折叠明细 / 查询提示 / 热点页 撤出开机屏;
109
+ // 逃生口还在:abs rule / abs show index / abs todo --full / abs lint。
110
+ const board = collapseDone(todo);
111
+ const todoHead = String(board.text || '').split(/\n## Done/)[0].trim();
112
+ const { items: ruleItems } = readRules(index);
113
+ const blocks = extractBlocks(todo);
98
114
  sections.push(
99
- // Rules 单独成段且放在最前(仅次于项目行/滞留):它是硬规则,不是普通清单。
100
- // 坑: 曾在 index 段内与页面清单平铺 —— AI 会当普通清单划过,而它每条都是付过代价的。
101
- ...(rulesSection(index) ? [rulesSection(index), ''] : []),
102
- collapseIndex(index) || '(index.md 为空)',
115
+ `📖 Rules ${ruleItems.length} 条(违反会丢数据/静默失效;全文: abs rule)`,
103
116
  '',
104
- // 两区制:Todo(未完成,行首带状态标记)+ Done。
105
- '--- Todo Board (todo.md) ---',
106
- collapseDone(todo).text || '(todo.md 为空)',
107
- '\n(Done 已按日期折叠计数;明细: abs todo --full)',
117
+ '--- 未完成 (todo.md) ---',
118
+ todoHead || '(无未完成任务)',
119
+ ...(blocks.length
120
+ ? [` ⛔ 卡点 ${blocks.length} 条: ${blocks.map((b) => `${b.id}(${b.why})`).join('; ')}`]
121
+ : []),
122
+ ` Done ${board.doneCount} 条已折叠(明细: abs todo --full)`,
108
123
  '',
109
- '--- 最近动作 (log.md, 最新 5 条) ---',
110
- recentLogLines(log, 5) || '(log.md 为空)',
124
+ '--- 最近动作 (log.md, 最新 3 条) ---',
125
+ recentLogLines(log, 3, 140) || '(log.md 为空)',
126
+ '',
127
+ LOAD_USAGE,
111
128
  );
112
129
  if (stranded.length) {
113
130
  const rows = stranded.map((t) => {
@@ -123,55 +140,21 @@ export async function cmdLoad({ dir }) {
123
140
  ''
124
141
  );
125
142
  }
126
- // 卡点提取(2026-09-16,定位 = 看板可见性):
127
- // 从 todo 里抽出带 `阻塞:` / `等待:` 的断点行,集中展示。
128
- // 为何需要:断点行在任务下方,一屏扫过去看不出"哪些事在等人/等外部"。
129
- // 只提取不求全 —— 只有明确写了前缀的才被抽出,没写的不猜。
130
- const blocks = extractBlocks(todo);
131
- if (blocks.length) {
132
- const at = sections.findIndex((s) => String(s).startsWith('--- Rules'));
133
- sections.splice(at === -1 ? 1 : at, 0,
134
- `⛔ 卡点 ${blocks.length} 条(在等外部条件)`,
135
- ...blocks.map((b) => ` - ${b.id}: ${b.why}`),
136
- '');
137
- }
138
- // 相关页提示(2026-09-16):实测 abs_query 几乎不被调用(MCP 日志 task 234 / note 54 / load 53 vs query 2)。
139
- // 根因:load 只列目录 → 模型觉得"我记过了" → 真需要时靠印象。模型不知道自己不知道什么。
140
- // 解法(用户定):不替它查 —— 而是【把该查的词直接给出来】,降低发起查询的成本。
141
- // 自动带出正文是错的方向:那会让 query 更没人用,且无法按需求变化调整词。
142
- // 失败静默:这只是 additive 提示,出任何错都不应弄坏 load。
143
- try {
144
- const rel = await queryHintSection(root, todo);
145
- if (rel) sections.push('', rel);
146
- } catch { /* 提示失败不影响 load */ }
147
- // 最相关几页(2026-11-02):queryHint 给「该查什么词」,这里给「该读哪几页」。
148
- // 放最后 —— 它是「再深入一层」的东西,前面先给状态与待办。
149
- try {
150
- const hot = await hotPagesSection(root, todo);
151
- if (hot) sections.push('', hot);
152
- } catch { /* 同上,失败静默 */ }
153
- // 未填会话页提醒(2026-11-05):ingest/hook 落的是空骨架,内容要人填。
154
- // 若不在开工时摆出来,「落了没填」就永远没人知道 —— 等于采集了不消化。
155
- // 与 sources 提醒同一个位置、同一种语气:只给线索,判断仍归 agent。
143
+
144
+ // 未填会话页 / sources 堆积:只给一行计数,不列页名(细节走 abs ingest --list / abs lint)。
145
+ // 当年列页名是为送达;现在按总量预算收口,一线足够让人决定要不要深查。
156
146
  try {
157
- const sessWarn = await unfilledSessionSection(root);
158
- if (sessWarn) {
159
- const at = sections.findIndex((s) => String(s).startsWith('--- Rules'));
160
- sections.splice(at === -1 ? 1 : at, 0, ...sessWarn);
161
- }
147
+ const sessWarn = await unfilledSessionSection(root, { oneLine: true });
148
+ if (sessWarn) sections.push('', sessWarn);
162
149
  } catch { /* 提醒失败不影响 load */ }
163
- // sources 堆积提醒:与「滞留」同构 —— 放在每次开工必经的顶部,而不是等人跑 lint。
164
- // 只数目录条目(不读文件),零成本。提炼仍手工:这里只负责送达,不替判断。
165
150
  try {
166
- const srcWarn = await sourcesCountSection(root);
167
- if (srcWarn) {
168
- const at = sections.findIndex((s) => String(s).startsWith('--- Rules'));
169
- sections.splice(at === -1 ? 1 : at, 0, ...srcWarn);
170
- }
151
+ const srcWarn = await sourcesCountSection(root, { oneLine: true });
152
+ if (srcWarn) sections.push('', srcWarn);
171
153
  } catch { /* 提醒失败不影响 load */ }
172
154
  return sections.join('\n');
173
155
  }
174
156
 
157
+
175
158
  /**
176
159
  * 从 todo 正文抽出"卡点"行,供 load 集中展示。
177
160
  *
@@ -306,7 +289,7 @@ export async function hotPagesSection(root, todoText, { limit = 3 } = {}) {
306
289
  * @param {string} todoText
307
290
  * @param {{name:string,body:string}[]} [pages] 已读好的页(省一次磁盘扫描)
308
291
  */
309
- async function queryHint(root, todoText, pages) {
292
+ export async function queryHint(root, todoText, pages) {
310
293
  const { kws, files } = await contextKeywords(root, todoText);
311
294
  if (!kws.length) return '';
312
295
  const all = pages || await listPages(brainPath(root)).catch(() => []);
@@ -335,10 +318,7 @@ async function countSources(root) {
335
318
  } catch { return 0; }
336
319
  }
337
320
 
338
- /** queryHint 的 load 端封装:返回提示文本(含标题行),无强词则空串。失败静默。 */
339
- async function queryHintSection(root, todo) {
340
- return queryHint(root, todo);
341
- }
321
+
342
322
 
343
323
  /**
344
324
  * recall:卡住时主动取「跟当前处境相关的几页」,不需要先想到检索词。
@@ -457,7 +437,7 @@ export async function cmdRecall({ dir, terms, limit = 5 } = {}) {
457
437
  *
458
438
  * 守「读取侧输出不随规模增长」:只列表头最多 SESSION_UNFILLED_SHOW 个页名,
459
439
  * 其余折叠成条数。页名够用 —— agent 要读哪张再 abs resolve 那一张,别在这贴正文。 */
460
- async function unfilledSessionSection(root) {
440
+ async function unfilledSessionSection(root, { oneLine = false } = {}) {
461
441
  let files;
462
442
  try {
463
443
  files = await fs.readdir(brainPath(root, 'sessions'));
@@ -472,6 +452,8 @@ async function unfilledSessionSection(root) {
472
452
  if (!digestFilled(body)) unfilled.push(f.replace(/\.md$/, ''));
473
453
  }
474
454
  if (!unfilled.length) return null;
455
+ // 2026-11-08 load 三件套收口:只回一行计数,不列页名(列名那套留给 --list / 以后要用再传 oneLine:false)。
456
+ if (oneLine) return `📝 未填会话页 ${unfilled.length} 张(abs ingest --list 看哪几张;不进检索,不急着全填)`;
475
457
  // 按日期倒序(2026-11-05 实测修正):原来按 readdir 顺序(≈文件名升序),
476
458
  // 60 张时列出来的是 18/27/36/45/54 —— 随机五张,既不是最近的也不是最该填的。
477
459
  // 页名形如 session-<日期>-s<id8>,日期就在里面,直接按名字倒排即可。
@@ -494,9 +476,10 @@ async function unfilledSessionSection(root) {
494
476
  }
495
477
 
496
478
  /** sources 堆积提醒:返回 [标题行, ...内容行] 或 null。失败静默。 */
497
- async function sourcesCountSection(root) {
479
+ async function sourcesCountSection(root, { oneLine = false } = {}) {
498
480
  const nsrc = await countSources(root);
499
481
  if (nsrc <= SOURCES_MAX) return null;
482
+ if (oneLine) return `♻ 待消化: sources/ 有 ${nsrc} 条 > ${SOURCES_MAX}(abs lint 看明细)`;
500
483
  return [
501
484
  `♻ 待消化: sources/ 有 ${nsrc} 条 > ${SOURCES_MAX}(采集了没提炼)`,
502
485
  '→ abs lint 看明细;提炼成 concepts/ 后删 source 并清引用',
@@ -5,7 +5,7 @@ import { join, dirname } from 'node:path';
5
5
  import { brainPath, requireBrain, absLogDir } from '../index.js';
6
6
  import { editFile, SKIP } from '../lock.js';
7
7
  import { appendWrapup } from '../wrapup.js';
8
- import { readIfExists } from '../text.js';
8
+ import { clip, readIfExists } from '../text.js';
9
9
  import { getUser } from '../userconfig.js';
10
10
  import { today, localStamp, archiveDoneInText, upsertArchiveSection, TODO_MAX_LINES } from '../todo.js';
11
11
  export async function cmdWrapup({ dir }) {
@@ -100,7 +100,20 @@ export async function cmdTodoArchive({ dir, keepDays = 3, dryRun = false } = {})
100
100
  const sIdx = index.indexOf('## Sessions');
101
101
  if (sIdx === -1) return SKIP;
102
102
  const after = index.indexOf('\n## ', sIdx + 1);
103
- const add = missing.map((g) => `- [[${g.slug}]] — ${g.date}(会话快照 + 任务归档)`).join('\n');
103
+ // index 行要能回头查(2026-11-08 用户要求):光写「日期(会话快照 + 任务归档)」
104
+ // 查不到那天干了什么。描述 = archiveBrief 的任务首段(截 110 字);
105
+ // 老数据/无描述时退回原样 —— 不造假描述。
106
+ // 行是覆盖式登记(registerInIndex 同族),这里只在**首次登记**写,之后不追改。
107
+ const add = missing.map((g) => {
108
+ const b = String(g.brief || '').trim();
109
+ // 无首段时用任务 id 充当描述(是真的,不是编的)—— 光写日期模板会被 INDEX-ENTRY 打回。
110
+ const ids = (g.lines || []).filter((l) => /^- \[x\]/.test(l))
111
+ .map((l) => (String(l).match(/\s(\S+)[^\n]*$/) || [])[1])
112
+ .filter(Boolean).slice(0, 3);
113
+ const fallback = `完成任务 ${g.count} 条${ids.length ? ':' + ids.join('、') : ''}`;
114
+ const desc = `${g.date}(会话快照 + 任务归档):${b ? clip(b, 110) : fallback}`;
115
+ return `- [[${g.slug}]] — ${desc}`;
116
+ }).join('\n');
104
117
  const next = after === -1
105
118
  ? `${index.replace(/\s*$/, '')}\n${add}\n`
106
119
  : index.slice(0, after) + `\n${add}` + index.slice(after);
package/src/lint.js CHANGED
@@ -193,6 +193,13 @@ async function checkRootFiles(vault) {
193
193
  `总结/报告/清单请分流:流水 → abs log,经验 → abs note,长设计 → sources/)`,
194
194
  );
195
195
  }
196
+ // 格式闸(2026-11-08 用户定):Todo 区任务行必须有 ` — 描述`,只有 id 打回。
197
+ if (todoSectionOf(lines, i) === 'Todo' && /^- \[ \]/.test(l) && !l.includes(' — ')) {
198
+ issues.push(
199
+ `TODO-NO-DESC: ${BRAIN_DIR}/${file}:${i + 1} (任务行没有描述;` +
200
+ `只有 id 查不到、接不上 —— abs todo start <id> --note "一句话" 补上,或 state 搁置/删除)`,
201
+ );
202
+ }
196
203
  // 断点行(新旧格式同一个解析器 —— 不再自己切前缀)
197
204
  const bpLine = parseChild(l);
198
205
  if (!bpLine) continue;
package/src/note.js CHANGED
@@ -273,6 +273,13 @@ export async function cmdLog({ dir, title, kind = 'dev', summon = true }) {
273
273
  ` note=经验/踩坑 / dev=完成的工作 / concept=新建概念页 / ingest=沉淀资料`,
274
274
  );
275
275
  }
276
+ // 格式闸(2026-11-08 用户定):空标题/纯空白的 log 打回。
277
+ if (!String(title || '').trim()) {
278
+ throw new Error(
279
+ `✗ log 必须有一句话结论 —— 空标题打回。\n` +
280
+ ` 用法: abs log "做完什么 + 怎么验证的" --kind dev`,
281
+ );
282
+ }
276
283
  await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
277
284
  const p = brainPath(root, 'log.md');
278
285
  const stamp = localStamp();
package/src/store.js CHANGED
@@ -19,7 +19,7 @@ export { BRAIN_SHAPE, LEGACY_MARKS, checkBrainShape } from './brain/shape.js';
19
19
  import './brain/gate.js';
20
20
 
21
21
  // ---- 只读呈现 ----
22
- export { cmdBoard, cmdStatus, cmdLoad, cmdShow, cmdRecall, extractBlocks } from './brain/load.js';
22
+ export { cmdBoard, cmdStatus, cmdLoad, cmdShow, cmdRecall, extractBlocks, queryHint } from './brain/load.js';
23
23
 
24
24
  // ---- Rules ----
25
25
  export { RULES_HEADING, RULES_MAX, RULE_BODY_MAX, readRules, cmdRule } from './brain/rules.js';
package/src/text.js CHANGED
@@ -52,13 +52,13 @@ export async function readIfExists(p) {
52
52
  /** log.md 是"新在上"(cmdLog 把新行插在标题后)——取**最新** n 条,只认 `## [` 条目行。
53
53
  * 坑: 曾用 tailLines 取末尾 → 拿到的永远是最旧几条,而标签写着"最近动作",
54
54
  * 于是"开机读状态"最该看的一节长期显示两天前的旧记录(今天的新条目从未显示)。 */
55
- export function recentLogLines(text, n) {
55
+ export function recentLogLines(text, n = 5, clipLen = 220) {
56
56
  return String(text || '')
57
57
  .split('\n')
58
58
  .filter((l) => /^##\s*\[/.test(l))
59
59
  .slice(0, n)
60
60
  // 每条按语义边界收口:load 是开机读状态,只求"上会话做到哪"的线索,不须全文。
61
61
  // 坑: 不收口时 5 条能占 2.9KB(单条 800B),又是一处随日志变长而膨胀的上下文。
62
- .map((l) => clip(l, 220))
62
+ .map((l) => clip(l, clipLen))
63
63
  .join('\n');
64
64
  }
@@ -231,6 +231,11 @@ export function archiveDoneInText(text, { keepDays = 3, from = today(), maxLines
231
231
  // 会把子项一起计数(实测:1 任务 + 1 子项 → 报 "完成任务 2 条")。
232
232
  // 归档标记的语义是「完成了多少条任务」,子项不算。
233
233
  count: gLines.filter((l) => /^- \[x\]/.test(l)).length,
234
+ // index 行描述(2026-11-08 用户要求):归档页在 index 里只有
235
+ // 「日期(会话快照 + 任务归档)」这种零信息量行,回头找某天的活查不到。
236
+ // 取每个任务的**第一段**(登记时 `--note` 的那句话)拼成摘要,给
237
+ // maintain.js 写 index 行用。纯函数放在这,cli/store 测试可直接断言。
238
+ brief: archiveBrief(gLines),
234
239
  });
235
240
  }
236
241
 
@@ -242,6 +247,22 @@ export function archiveDoneInText(text, { keepDays = 3, from = today(), maxLines
242
247
  return { text: rebuilt.join('\n').replace(/\n+$/, '\n'), archived, skipped, count };
243
248
  }
244
249
 
250
+ /** 从任务行抽第一段描述:`<id> [[who]] — 第一段 — 结语 【kind】 (完成 …)`。
251
+ * 第一段 = 第一个 `—` 到下一个 `—`/`【`/`(完成` 之间(即登记时的 --note)。
252
+ * 没有的行(如老格式 `- [x] KEEP (完成 …)`)跳过;一行都没有返回空串。
253
+ * 给 index 行用 —— 只取每条任务的首段、截 60 字,别把结语也卷进来。 */
254
+ export function archiveBrief(lines) {
255
+ const parts = [];
256
+ for (const raw of lines) {
257
+ if (!/^- \[x\]/.test(raw)) continue; // 断点/子项行不参与
258
+ const s = raw.replace(/^\s*-\s*\[x\]\s*/, '');
259
+ const m = s.match(/—\s*([\s\S]*?)(?=\s*—|\s*【|\s*\(完成|$)/);
260
+ const t = m ? m[1].trim().replace(/\s+/g, ' ') : '';
261
+ if (t) parts.push(t);
262
+ }
263
+ return parts.join(';');
264
+ }
265
+
245
266
  /** 归档页正文(按日期分组,原文保留)。供同日追写复用。 */
246
267
 
247
268
  export function renderArchiveBody(groups) {
package/src/todo/spec.js CHANGED
@@ -79,6 +79,12 @@ export const ENTRY_SHAPES = {
79
79
  if (!/^-\s/.test(l)) return '链接区只能放 `- [[页名]] — 一句话`(正文段落/编号列表/代码块都不行)';
80
80
  if (!/^-\s*\[\[[^\]]+\]\]/.test(l)) return '应以 `- [[页名]] …` 开头';
81
81
  if (!/^-\s*\[\[[^\]]+\]\]\s*[—-]\s*\S/.test(l)) return '`[[页名]]` 后要跟 `— 一句话` 描述';
82
+ // 格式闸(2026-11-08 用户定):机器模板行(只有日期+归档字样)不算描述,打回。
83
+ // 归档登记时必须带任务首段(见 maintain.js / archiveBrief)。
84
+ const dTpl = l.replace(/^\s*-\s*\[\[[^\]]+\]\]\s*[—-]\s*/, '').trim();
85
+ if (/^\d{4}-\d{2}-\d{2}((?:会话快照 \+ )?任务归档)?$/.test(dTpl)) {
86
+ return '描述不能只是日期模板 —— 要写那天干了什么(任务首段,≥4 字)';
87
+ }
82
88
  // 摘要里不许带 Markdown 结构符(实测污染就是摘要取了页面正文开头)
83
89
  if (/^\s*-\s*\[\[[^\]]+\]\]\s*[—-]\s*#+\s/.test(l)) return '摘要不能带 `## ` 标题前缀(取页面首行标题了)';
84
90
  // ★ 2026-10-18 补:摘要正文本身必须是**纯文本一句话**。
@@ -33,6 +33,15 @@ export async function cmdTask({ dir, action, id, section, note, as, session, n }
33
33
  const claim = `(认领 ${today()}${sid ? ' ' + sid : ''})`;
34
34
  await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
35
35
  if (action === 'start') {
36
+ // 格式闸(2026-11-08 用户定:不符合直接打回):只有 id 的看板行查不到、接不上。
37
+ // 实测:slim-skill-load-cmd 只登记了 id,面板上一行代号,隔天自己都不认识。
38
+ if (!String(note || '').trim()) {
39
+ throw new Error(
40
+ `✗ 任务必须有描述 —— 只有 id 的行查不到、接不上,打回。\n` +
41
+ ` 用法: abs todo start <id> --note "一句话:做什么"\n` +
42
+ ` 格式: 动词开头、一句话、≤40 字;进度用 abs todo note 逐条加,别塞进标题。`,
43
+ );
44
+ }
36
45
  // 两区制:未完成一律进 Todo,行首带状态标记(默认 进行中)。
37
46
  const st = section && TASK_STATES.includes(section) ? section : '进行中';
38
47
  // 唯一进行中约束(2026-10-05 照 rpiv-todo):看板要能回答"现在在做什么",
@@ -205,6 +214,15 @@ export async function cmdTask({ dir, action, id, section, note, as, session, n }
205
214
  if (as && !DONE_KINDS.includes(as)) {
206
215
  throw new Error(`✗ --as 只接受: ${DONE_KINDS.join(' | ')}(收到 "${as}")`);
207
216
  }
217
+ // 格式闸(2026-11-08 用户定):done 必须带结语,否则 Done 区只剩一个勾和 id,
218
+ // 下个会话不知道“做成了什么”。打回,让人把结论写完。
219
+ if (!String(note || '').trim() && !as) {
220
+ throw new Error(
221
+ `✗ done 必须带结语 —— 打回。只有勾没有结论的 Done 不可信。\n` +
222
+ ` 用法: abs todo done <id> --as 落地|否决|仅方案 "一句话结论"\n` +
223
+ ` 例: abs todo done <id> --as 落地 "821 测试全绿,已装到 ~/.pi"`,
224
+ );
225
+ }
208
226
  // 结语 = note(MCP 路径)或位置参数(CLI 路径)。坑(2026-09-16 实测):
209
227
  // MCP abs_task schema 无 as,AI 按旧 SKILL.md 传 note 会被静默丢弃、行上默认盖【落地】
210
228
  // —— 状态失真且无声。现在 note 也当结语;note 里自带【落地/否决/仅方案】时它就是 kind。
package/src/todo.js CHANGED
@@ -6,6 +6,6 @@ export { TODO_MAX_LINES, LOG_MAX_LINES, INDEX_MAX_LINES, today, localStamp, SEC,
6
6
  export { LEGACY_SECTION_RENAMES, TODO_SECTIONS, TODO_SUBSECTIONS, isAllowedTodoSub, INDEX_SECTIONS, INDEX_LINK_SECTIONS, LOG_KINDS, H1_TO_FILE, ENTRY_SHAPES, TASK_STATES, RETIRED_SECTIONS, BREAKPOINT_MAX, TASK_LINE_MAX, DONE_KINDS, ARCHIVE_SECTION } from './todo/spec.js';
7
7
  export { todoTemplate, rebuildStructure, dropRetiredSections, enforceBrainFormat, normalizeTodo, ensureStateMark, stateOfTaskLine, stripStateMark, checkTaskBox } from './todo/structure.js';
8
8
  export { checkFileShape, violationSignatures, assertNoStrayBlank, assertTodoContent } from './todo/validate.js';
9
- export { doneDateOf, doneKindOf, withDoneKind, collapseDone, archiveDoneInText, renderArchiveBody, upsertArchiveSection, groupDoneSection, insertDoneGrouped } from './todo/archive.js';
9
+ export { doneDateOf, doneKindOf, withDoneKind, collapseDone, archiveDoneInText, renderArchiveBody, upsertArchiveSection, groupDoneSection, insertDoneGrouped, archiveBrief } from './todo/archive.js';
10
10
  export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setBreakpointDone, setMemo, readMemo, boardText } from './todo/tasks.js';
11
11
  export { migrateLegacyChild, isTaskLine, isChildLine, CHILD_INDENT } from './todo/common.js';