@fanchao8609/agent_brain_sync 1.28.5 → 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 +8 -41
- package/hooks/abs-rounded-editor/format.ts +12 -17
- package/hooks/abs-rounded-editor/index.ts +9 -17
- package/hooks/abs.omp.ts +1 -2
- package/hooks/abs.pi.ts +47 -48
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +68 -68
- package/src/brain/load.js +44 -61
- package/src/brain/maintain.js +15 -2
- package/src/lint.js +7 -0
- package/src/note.js +7 -0
- package/src/store.js +1 -1
- package/src/text.js +2 -2
- package/src/todo/archive.js +21 -0
- package/src/todo/spec.js +6 -0
- package/src/todo/task-cmd.js +18 -0
- package/src/todo.js +1 -1
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
|
|
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 —
|
|
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
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
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 |
|
|
770
|
+
console.log('用法: abs db rebuild | status | evolve | add [--dir <项目根>]');
|
|
804
771
|
break;
|
|
805
772
|
}
|
|
806
773
|
case 'query': {
|
|
@@ -72,29 +72,24 @@ function dropCols(s: string, n: number): string {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* 推理强度 →
|
|
76
|
-
* 色值是用户逐档指定的(给的是 hex),不是自己调的:
|
|
77
|
-
* max #ACB8CD · xhigh #95A5C3 · high #768298 · medium #697387
|
|
78
|
-
* low #5C6577 · minimal #505867
|
|
79
|
-
* 色相是用户挑的灰蓝(不是绿),换色只许按用户给的值改,别拿主题色或自己的审美去替换。
|
|
80
|
-
* off / 未知 → 空串,表示「不上色,沿用默认色」—— 用户 2026-11-06 定过:推理关着就保持原样。
|
|
75
|
+
* 推理强度 → 主题色 token。空串表示「不上色,沿用默认色」。
|
|
81
76
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
77
|
+
* 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
|
|
78
|
+
* 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
|
|
79
|
+
* (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
|
|
80
|
+
* 「off 时保持默认色」。
|
|
86
81
|
*
|
|
87
|
-
* ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`
|
|
82
|
+
* ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortColorToken`)。不抽公共模块是故意的:
|
|
88
83
|
* 安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,抽成共享文件会让
|
|
89
84
|
* 「删掉输入框样式目录」连带把 todo 面板搞崩。
|
|
90
85
|
*/
|
|
91
86
|
export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
|
|
92
|
-
max: { rgb: '
|
|
93
|
-
xhigh: { rgb: '
|
|
94
|
-
high: { rgb: '
|
|
95
|
-
medium: { rgb: '
|
|
96
|
-
low: { rgb: '
|
|
97
|
-
minimal: { rgb: '
|
|
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 },
|
|
98
93
|
}
|
|
99
94
|
|
|
100
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
|
-
//
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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,28 +971,25 @@ export function todoAnimFrame(phase: number): string {
|
|
|
974
971
|
return TODO_ANIM_FRAMES[i]
|
|
975
972
|
}
|
|
976
973
|
|
|
977
|
-
/**
|
|
978
|
-
*
|
|
979
|
-
* max #ACB8CD · xhigh #95A5C3 · high #768298 · medium #697387
|
|
980
|
-
* low #5C6577 · minimal #505867
|
|
981
|
-
* 色相是用户挑的灰蓝(不是绿),换色只许按用户给的值改,别拿主题色或自己的审美去替换。
|
|
982
|
-
* off / 未知 → 空串,表示「不上色」:调用方用灰阶(用户 2026-11-06 定过,推理关着就别发光)。
|
|
974
|
+
/**
|
|
975
|
+
* 推理强度 → 主题色 token。空串表示「不上推理色,用调用方给的兼底色」。
|
|
983
976
|
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
977
|
+
* 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
|
|
978
|
+
* 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
|
|
979
|
+
* (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
|
|
980
|
+
* 「off 时保持默认色」。
|
|
987
981
|
*
|
|
988
|
-
* ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`
|
|
982
|
+
* ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`thinkingColorToken`)。
|
|
989
983
|
* 不抽公共模块是故意的:安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,
|
|
990
984
|
* 抽成共享文件会让「删掉输入框样式目录」连带把 todo 面板搞崩。
|
|
991
985
|
*/
|
|
992
986
|
export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
|
|
993
|
-
max: { rgb: '
|
|
994
|
-
xhigh: { rgb: '
|
|
995
|
-
high: { rgb: '
|
|
996
|
-
medium: { rgb: '
|
|
997
|
-
low: { rgb: '
|
|
998
|
-
minimal: { rgb: '
|
|
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 },
|
|
999
993
|
}
|
|
1000
994
|
|
|
1001
995
|
export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
|
|
@@ -1017,23 +1011,31 @@ export function renderPanelLines(
|
|
|
1017
1011
|
animPhase = -1,
|
|
1018
1012
|
nickname = '',
|
|
1019
1013
|
effort = '',
|
|
1020
|
-
colorMode = 'truecolor',
|
|
1021
1014
|
): string[] {
|
|
1022
1015
|
// ★ 空看板不再直接返回空数组(2026-10-06 用户要求:「即便 todo 没有列表,也要在 pi 上显示」)。
|
|
1023
1016
|
// 理由与看板段一致:面板是「机制在不在」的常驻指示,进去就看不见 = 不知道机制活着。
|
|
1024
1017
|
// 下面仍要画框和标题,所以这里只是不再提前 return。
|
|
1025
1018
|
//
|
|
1026
1019
|
// ★ 主色跟推理强度走(2026-11-06 用户需求):标题、[状态]、树线 ├─└─、复选框 [ ]、
|
|
1027
|
-
// 进行中动画同用一色,与 pi
|
|
1028
|
-
// ★
|
|
1029
|
-
// ★ 2026-11-07 用户再改:面板里**所有文字都用这一色**(标题、状态、id、描述、子项、+N more),
|
|
1030
|
-
// 不再给描述/子项降成灰 —— 拿“颜色深浅”区分主次的写法作废了,层次只靠符号与缩进。
|
|
1031
|
-
// 别把描述改回 muted:那是用户主动去掉的(原话「所有文字都一致吧」)。
|
|
1032
|
-
// ★ off 时整块面板用灰阶(2026-11-06 用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
|
|
1020
|
+
// 进行中动画同用一色,与 pi 输入框描边/文字同色系。
|
|
1021
|
+
// ★ off 时整块面板用灰阶(同日用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
|
|
1033
1022
|
// 高亮色只在真的开着推理时给,否则「推理关了面板还在发光」让人误以为还在深度思考。
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
const
|
|
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)
|
|
1037
1039
|
const lines: string[] = []
|
|
1038
1040
|
// 上描边行已删除(2026-11-04 用户要求「去掉上边框」)。
|
|
1039
1041
|
// 历史:那行曾用 `thinkingOff` 对齐 pi 输入框描边色(排查三轮才定色)。
|
|
@@ -1045,7 +1047,7 @@ export function renderPanelLines(
|
|
|
1045
1047
|
const title = whoPart ? `Todo (${data.total}) — ${whoPart}` : `Todo (${data.total})`
|
|
1046
1048
|
// 标题也顶最左(2026-11-04 用户)。旧代码曾在空看板时多缩 1 格,2026-11-06 空看板
|
|
1047
1049
|
// 已整块不显示(见 refreshTodoPanel),那个分支没意义了。
|
|
1048
|
-
lines.push(clipToWidth(
|
|
1050
|
+
lines.push(clipToWidth(paint(C.title, title), width))
|
|
1049
1051
|
|
|
1050
1052
|
const lastIdx = data.rows.length - 1
|
|
1051
1053
|
data.rows.forEach((t, i) => {
|
|
@@ -1057,33 +1059,36 @@ export function renderPanelLines(
|
|
|
1057
1059
|
// 进行中的行带一个小动画(其余行不加,避免满屏都在跳)
|
|
1058
1060
|
// 单字符 spinner 紧跟 [进行中]:靠前放才能一眼看到(放行尾会被长描述挤到前面去看不见)。
|
|
1059
1061
|
// 2026-11-06 用户要求:「放在 [进行中] 后面」。
|
|
1060
|
-
const
|
|
1062
|
+
const hasState = ['进行中', '讨论中', '搁置', '滞留中'].includes(t.state)
|
|
1063
|
+
const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + paint(C.doing, todoAnimFrame(animPhase)) : ''
|
|
1061
1064
|
// 复选框 `[ ]`(2026-10-16 用户要求):面板是 todo.md 的只读视图,
|
|
1062
1065
|
// 就该长成 todo 原貌 —— 摆成与文件里 `- [ ]` 同形,读到就认得出对应哪一行。
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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}`) : ''
|
|
1068
1073
|
lines.push(clipToWidth(head + desc, width))
|
|
1069
1074
|
if (t.kids && t.kids.length) {
|
|
1070
1075
|
// 子项:标准 md 嵌套列表 → 画成树
|
|
1071
1076
|
// ├─ [ ] 子项一
|
|
1072
1077
|
// └─ [x] 子项二
|
|
1073
1078
|
// 树线前缀 = 父任务的 │(非末条)或空格(末条),与父任务分支对齐。
|
|
1074
|
-
//
|
|
1075
|
-
const stem = isLast ? ' ' :
|
|
1079
|
+
// 勾过的用 dim 压暗(一眼看出哪些已完成),未勾用正常色。
|
|
1080
|
+
const stem = isLast ? ' ' : paint(C.tree, '│ ')
|
|
1076
1081
|
const n = t.kids.length
|
|
1077
1082
|
t.kids.forEach((k, ki) => {
|
|
1078
1083
|
const kb = ki === n - 1 ? '└─' : '├─'
|
|
1079
|
-
const kbox =
|
|
1080
|
-
const ktext =
|
|
1081
|
-
lines.push(clipToWidth(`${stem} ${
|
|
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))
|
|
1082
1087
|
})
|
|
1083
1088
|
}
|
|
1084
1089
|
})
|
|
1085
1090
|
|
|
1086
|
-
if (data.hidden > 0) lines.push(clipToWidth(
|
|
1091
|
+
if (data.hidden > 0) lines.push(clipToWidth(paint(C.title, ` +${data.hidden} more`), width))
|
|
1087
1092
|
return lines
|
|
1088
1093
|
}
|
|
1089
1094
|
/** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
|
|
@@ -1122,11 +1127,6 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
|
|
|
1122
1127
|
(tui: any, theme: any) => {
|
|
1123
1128
|
tuiRef = tui
|
|
1124
1129
|
lastTui = tui // 供 hidePanel 在"无动画"时也能请求重绘
|
|
1125
|
-
// 主色要按终端的色深选编码(24 位色 / 256 色)—— getColorMode() 是 pi 主题自己
|
|
1126
|
-
// 算好的那一个;取不到就当支持 24 位色(真拿不准时宁可亮一点也别一格都不出)。
|
|
1127
|
-
const colorMode = (() => {
|
|
1128
|
-
try { return String(theme?.getColorMode?.() || 'truecolor') } catch { return 'truecolor' }
|
|
1129
|
-
})()
|
|
1130
1130
|
return {
|
|
1131
1131
|
render: (width: number) =>
|
|
1132
1132
|
renderPanelLines(
|
|
@@ -1137,7 +1137,6 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
|
|
|
1137
1137
|
animPhase,
|
|
1138
1138
|
sessionNickname,
|
|
1139
1139
|
effortLevel,
|
|
1140
|
-
colorMode,
|
|
1141
1140
|
),
|
|
1142
1141
|
invalidate: () => {},
|
|
1143
1142
|
}
|
package/package.json
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abs-agent-brain-sync
|
|
3
|
-
description: "USE FOR abs / agent-brain-sync
|
|
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 —
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
后者只给页名+摘要更省 token。MCP 侧分别对应 `abs_search` / `abs_query`。
|
|
34
|
+
## Find knowledge: three tiers
|
|
35
35
|
|
|
36
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
57
|
+
## Tasks: the board is alive
|
|
53
58
|
|
|
54
59
|
```bash
|
|
55
|
-
abs todo
|
|
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
|
-
-
|
|
63
|
-
-
|
|
64
|
-
- `搁置` =
|
|
65
|
-
-
|
|
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
|
-
|
|
72
|
+
## Triggers are intent, not keywords
|
|
70
73
|
|
|
71
|
-
|
|
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
|
-
|
|
77
|
+
Exception: the user types `abs` alone or `abs` + command → execute; no subcommand = startup.
|
|
75
78
|
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
| `.brain/`
|
|
102
|
-
|
|
|
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
|
-
|
|
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 {
|
|
22
|
-
|
|
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
|
-
|
|
100
|
-
// 坑: 曾在 index 段内与页面清单平铺 —— AI 会当普通清单划过,而它每条都是付过代价的。
|
|
101
|
-
...(rulesSection(index) ? [rulesSection(index), ''] : []),
|
|
102
|
-
collapseIndex(index) || '(index.md 为空)',
|
|
115
|
+
`📖 Rules ${ruleItems.length} 条(违反会丢数据/静默失效;全文: abs rule)`,
|
|
103
116
|
'',
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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, 最新
|
|
110
|
-
recentLogLines(log,
|
|
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
|
-
|
|
127
|
-
//
|
|
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
|
-
|
|
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 并清引用',
|
package/src/brain/maintain.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
62
|
+
.map((l) => clip(l, clipLen))
|
|
63
63
|
.join('\n');
|
|
64
64
|
}
|
package/src/todo/archive.js
CHANGED
|
@@ -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 补:摘要正文本身必须是**纯文本一句话**。
|
package/src/todo/task-cmd.js
CHANGED
|
@@ -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';
|