@fanchao8609/agent_brain_sync 1.25.0 → 1.26.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/README.md +6 -4
- package/bin/abs.js +22 -2
- package/bin/mcp.js +41 -25
- package/hooks/{pi-rounded-format.ts → abs-rounded-editor/format.ts} +1 -1
- package/hooks/{pi-rounded-editor.ts → abs-rounded-editor/index.ts} +1 -1
- package/hooks/abs.omp.ts +26 -4
- package/hooks/abs.pi.ts +36 -4
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +53 -9
- package/src/brain/capture.js +35 -5
- package/src/brain/db-cmd.js +12 -4
- package/src/brain/db.js +40 -7
- package/src/brain/ingest.js +377 -0
- package/src/brain/layers.js +40 -0
- package/src/brain/load.js +70 -0
- package/src/brain/summon-cmd.js +4 -3
- package/src/brain/summon.js +64 -7
- package/src/install.js +45 -10
- package/src/lint.js +54 -3
- package/src/todo/structure.js +18 -1
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ pi 与 omp 宿主额外多一层 UI(其它宿主没有):**编辑器上方
|
|
|
75
75
|
- **子项(缩进的 `- [ ]`)挂在父任务下**;`├─` / `└─` 表结构
|
|
76
76
|
- **子项可各自勾选**:` - [ ] 一句话` → ` - [x] 一句话`;全部勾上才能 `done` 父任务
|
|
77
77
|
- **「进行中」带三点动画**`○○○ → ●●○ → ●●●`(250ms/帧)—— 一眼看出哪条在跑
|
|
78
|
-
- **实时刷新**:我调 `abs_task`
|
|
78
|
+
- **实时刷新**:我调 `abs_task` 后立即重画,不用等我讲完话
|
|
79
79
|
- **上描边与输入框同色满宽**(主题色 `thinkingOff`,跟 pi 输入框的边框一致)
|
|
80
80
|
- 窄终端按显示宽度截断(CJK 计 2 列),不溢出
|
|
81
81
|
|
|
@@ -120,13 +120,15 @@ abs todo done TASK-1
|
|
|
120
120
|
abs note "一句话经验" --tags 坑,docker # 经验暂存 → sources/
|
|
121
121
|
abs log "完成 X" # 记一行流水;abs log 无参 = 查看
|
|
122
122
|
abs query <词> # 检索图谱(多词 OR);已被推翻的经验默认隐藏(--all 可看)
|
|
123
|
-
abs capture [--layer 1-
|
|
123
|
+
abs capture [--layer 1-6] [--kind …] "一句话" # 按七层判层后落到位(L7 用 abs note;--kind: gotcha|decision|tradeoff|howto)
|
|
124
124
|
abs db rebuild # 扫所有登记项目 → 建图数据库(~/.abs/graph.db)
|
|
125
|
-
abs db search <词> [--full]
|
|
125
|
+
abs db search <词> [--full] [--no-rules] # 走库检索:亮度排序+跨项目;--full 附正文,--no-rules 省铁律
|
|
126
126
|
abs db status # 库体检:七层分布 / 悬挂链接 / 已入库项目
|
|
127
127
|
abs db add # 把当前项目登记进 ~/.abs/projects.json
|
|
128
128
|
abs db evolve # 知识进化体检:被推翻/断链/主题堆积/僵化页
|
|
129
|
-
abs summon [--
|
|
129
|
+
abs summon [--status] # 列出还没落页的会话(只读,不写盘)
|
|
130
|
+
abs ingest <会话文件或 id> # 把一场 pi 会话落成会话页(session-id 双向回溯)
|
|
131
|
+
# 已填 → 亮度×0.5;未填 → 整页不进检索
|
|
130
132
|
abs resolve <页名或id> # 反查页面路径(页改名后 id 不变)
|
|
131
133
|
abs supersede <页名> [--by <新页>] # 标记经验已失效(不删文件,保留历史;query 默认不再返回)
|
|
132
134
|
abs status # 当前项目 + 图谱概要
|
package/bin/abs.js
CHANGED
|
@@ -6,6 +6,7 @@ import { cmdInit, cmdStatus, cmdLoad, cmdRecall, cmdTask, cmdLog, cmdQuery, cmdL
|
|
|
6
6
|
import { cmdDbRebuild, cmdDbSearch, cmdDbStatus, cmdDbEvolve, addProject } from '../src/brain/db-cmd.js';
|
|
7
7
|
import { cmdCapture } from '../src/brain/capture.js';
|
|
8
8
|
import { cmdSummon } from '../src/brain/summon-cmd.js';
|
|
9
|
+
import { cmdIngest } from '../src/brain/ingest.js';
|
|
9
10
|
import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
|
|
10
11
|
import { runInstall, runUninstall } from '../src/install.js';
|
|
11
12
|
import { readFileSync } from 'node:fs';
|
|
@@ -144,6 +145,17 @@ const FLAG_SPEC = {
|
|
|
144
145
|
'repair': { type: 'boolean' },
|
|
145
146
|
'no-mcp': { type: 'boolean' },
|
|
146
147
|
'no-skill': { type: 'boolean' },
|
|
148
|
+
// ingest 用:强制重建已有会话页。
|
|
149
|
+
// ★ 不声明就会被 parseArgs 当布尔 true(见上面 kind 的注释)。
|
|
150
|
+
// (--mark 已在 summon 那里声明过,两家共用,不能重复写 —— no-dupe-keys 会报)
|
|
151
|
+
'force': { type: 'boolean' },
|
|
152
|
+
// ingest 用:--last 取最近一场会话(hook 在 session_shutdown 调,不用给 id);
|
|
153
|
+
// --auto 只落真改过文件的会话(纯聊天不落页)。
|
|
154
|
+
'last': { type: 'boolean' },
|
|
155
|
+
'auto': { type: 'boolean' },
|
|
156
|
+
// db search 用:不输出 L1 铁律(查主题时它们是噪音;开工看铁律是 load 的职责)。
|
|
157
|
+
// ★ 不声明会被 parseArgs 当布尔 true —— 见上面 kind 的注释。
|
|
158
|
+
'no-rules': { type: 'boolean' },
|
|
147
159
|
};
|
|
148
160
|
|
|
149
161
|
/** 已被"规范键"接管的 raw flag:不再原样漏出(见 parseArgv 返回处的白名单注释)。 */
|
|
@@ -250,8 +262,9 @@ const usage = `abs — 跨会话记忆工具(.brain/ 图谱)
|
|
|
250
262
|
todo 任务看板 (add/note/state/done/archive)
|
|
251
263
|
index | log | status 索引 / 流水 / 概要
|
|
252
264
|
note <经验> [--tags a,b] 经验暂存 → sources/
|
|
253
|
-
capture [--layer 1-
|
|
265
|
+
capture [--layer 1-6] "…" 按层判定落点后直接写入 (不传=L4; L7 用 abs note)
|
|
254
266
|
summon [--mark|--status] 会话总结托底: 列出未总结/有更新的会话 (不写盘)
|
|
267
|
+
ingest <会话文件> 把一场 pi 会话落成会话页 (带 session id 双向回溯)
|
|
255
268
|
concept <slug> --title … 建概念页骨架
|
|
256
269
|
query <词…> [--all] 检索知识页 (多词 OR)
|
|
257
270
|
db rebuild|search|status|evolve 图数据库: 建库 / 亮度搜索 / 体检 / 进化体检
|
|
@@ -555,6 +568,13 @@ async function main() {
|
|
|
555
568
|
console.log(await cmdSummon({ dir: opts.dir, mark: !!opts.mark, full: !!opts.full, status: !!opts.status, limit: Number(opts.limit) || 15 }));
|
|
556
569
|
break;
|
|
557
570
|
}
|
|
571
|
+
case 'ingest': {
|
|
572
|
+
console.log(await cmdIngest({
|
|
573
|
+
dir: opts.dir, file: opts._.join(' '), force: !!opts.force, mark: !!opts.mark,
|
|
574
|
+
last: !!opts.last, auto: !!opts.auto, session: opts.session,
|
|
575
|
+
}));
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
558
578
|
case 'capture': {
|
|
559
579
|
console.log(await cmdCapture({ dir: opts.dir, text: opts._.join(' '), layer: opts.layer, tags: opts.tags, title: opts.title, kind: opts.kind }));
|
|
560
580
|
break;
|
|
@@ -601,7 +621,7 @@ async function main() {
|
|
|
601
621
|
const sub = opts._[0];
|
|
602
622
|
const rest = opts._.slice(1);
|
|
603
623
|
if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
|
|
604
|
-
if (sub === 'search') { console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8, full: !!opts.full })); break; }
|
|
624
|
+
if (sub === 'search') { console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8, full: !!opts.full, noRules: !!opts['no-rules'] })); break; }
|
|
605
625
|
if (sub === 'status') { console.log(await cmdDbStatus()); break; }
|
|
606
626
|
if (sub === 'evolve') { console.log(await cmdDbEvolve({ staleDays: Number(opts.staleDays) || 90 })); break; }
|
|
607
627
|
if (sub === 'add') {
|
package/bin/mcp.js
CHANGED
|
@@ -12,7 +12,9 @@ import { readFileSync } from 'node:fs';
|
|
|
12
12
|
import { join, dirname } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { findBrainRoot, absLogDir, AbsError } from '../src/index.js';
|
|
15
|
-
import {
|
|
15
|
+
import { cmdLoad, cmdStatus, cmdTask, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdRule, cmdResolve, cmdSupersede } from '../src/store.js';
|
|
16
|
+
// abs_search 用:db search 的实现(它比 cmdQuery 强 —— 带亮度排序、能扩散、能跨项目)
|
|
17
|
+
import { cmdDbSearch } from '../src/brain/db-cmd.js';
|
|
16
18
|
|
|
17
19
|
// ---------- 技术日志: MCP 请求跟踪(调试用, 与图谱 log.md 完全分开) ----------
|
|
18
20
|
// 落 ~/.abs/log/mcp.log: 每次工具调用一行 [时间] tool cwd 参数摘要 → 耗时/结果摘要。
|
|
@@ -76,8 +78,14 @@ function tool(name, description, schema, handler) {
|
|
|
76
78
|
// 分级标注(借 OpenContext 的 P0/P1/P2 惯例):P0=每次开机必调;P1=干活中按需;P2=诊断/收尾。
|
|
77
79
|
// 后来人(和 AI)照这个顺序调,别一上来就 lint。
|
|
78
80
|
// P0 abs_resolve_project / abs_load
|
|
79
|
-
// P1
|
|
80
|
-
// P2 abs_status / abs_rule / abs_lint
|
|
81
|
+
// P1 abs_task / abs_query / abs_note / abs_resolve
|
|
82
|
+
// P2 abs_status / abs_rule / abs_lint
|
|
83
|
+
//
|
|
84
|
+
// 2026-11-05 删两个(用户定,判据见 concept feature-delete-not-patch):
|
|
85
|
+
// abs_board ── 它吐的内容已被 abs_load 完全覆盖(load 里就有 Todo Board 段),
|
|
86
|
+
// 是「与已有功能重叠 → 删」的又一例。
|
|
87
|
+
// abs_wrapup ── 它是 hook 的内部命令(session_shutdown 自己会跑),
|
|
88
|
+
// 给人调没有意义;暴露成工具只会诱使 agent 手动跑一遍。
|
|
81
89
|
tool(
|
|
82
90
|
'abs_resolve_project',
|
|
83
91
|
'定位当前项目的 .brain 图谱根(只认给定目录本身的 .brain/,不向上搜索)。多项目隔离的唯一入口。',
|
|
@@ -94,17 +102,6 @@ tool(
|
|
|
94
102
|
}
|
|
95
103
|
);
|
|
96
104
|
|
|
97
|
-
tool(
|
|
98
|
-
'abs_board',
|
|
99
|
-
'读取当前项目 todo 看板(含任务与子项)。先 resolve 得到项目,再传其根目录。',
|
|
100
|
-
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
101
|
-
async ({ cwd }) => {
|
|
102
|
-
const root = await findBrainRoot(cwd || process.cwd());
|
|
103
|
-
if (!root) return errNoBrain(cwd || process.cwd());
|
|
104
|
-
return { content: [{ type: 'text', text: await cmdBoard({ dir: root }) }] };
|
|
105
|
-
}
|
|
106
|
-
);
|
|
107
|
-
|
|
108
105
|
tool(
|
|
109
106
|
'abs_load',
|
|
110
107
|
'开机读状态:index 的 Rules + 图谱计数 + todo 看板 + 最近 log。跨会话续接的入口。',
|
|
@@ -212,6 +209,36 @@ tool(
|
|
|
212
209
|
}
|
|
213
210
|
);
|
|
214
211
|
|
|
212
|
+
tool(
|
|
213
|
+
'abs_search',
|
|
214
|
+
'按亮度检索知识图谱(跨项目):比 abs_query 更强 —— 带亮度排序、能从邻居页扩散、能查到别的项目的经验(标 [跨项目])。'
|
|
215
|
+
+ '适合「我上次怎么解决 X」这类回忆。铁律默认省略(开工时 abs_load 会给),需要时传 include_rules=true。',
|
|
216
|
+
{
|
|
217
|
+
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
218
|
+
query: z.string().describe('检索词(一句话或几个词都行,中文两字以上即可)'),
|
|
219
|
+
limit: z.number().int().min(1).max(20).optional().describe('最多几页,默认 8'),
|
|
220
|
+
include_rules: z.boolean().optional().describe('true=连 L1 铁律一起返回(默认省略,省 token)'),
|
|
221
|
+
},
|
|
222
|
+
async ({ cwd, query, limit, include_rules }) => {
|
|
223
|
+
// 库是全局的(~/.abs/graph.db),不需要 findBrainRoot —— 但先确认项目本身有图谱,
|
|
224
|
+
// 否则用户在一个陌生目录调用会得到「别的项目的结果」而不自知。
|
|
225
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
226
|
+
if (!root) return errNoBrain(cwd || process.cwd());
|
|
227
|
+
const text = await cmdDbSearch({
|
|
228
|
+
query: String(query || ''),
|
|
229
|
+
limit: Number(limit) || 8,
|
|
230
|
+
noRules: !include_rules,
|
|
231
|
+
});
|
|
232
|
+
if (/库还没建|无命中/.test(text)) {
|
|
233
|
+
return err(text, {
|
|
234
|
+
code: 'NO_MATCH',
|
|
235
|
+
fallback: '库可能是旧的:跑 abs db rebuild 重扫;或换更具体的词',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return { content: [{ type: 'text', text }] };
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
215
242
|
tool(
|
|
216
243
|
'abs_rule',
|
|
217
244
|
'读/写 index.md 的 ## Rules 硬规则区。action=list 列出;action=add 追加一条(只放一句话,≤42 字符、不带链接;展开写概念页)。',
|
|
@@ -276,17 +303,6 @@ tool(
|
|
|
276
303
|
}
|
|
277
304
|
);
|
|
278
305
|
|
|
279
|
-
tool(
|
|
280
|
-
'abs_wrapup',
|
|
281
|
-
'收尾保险:把当前 todo 未完成任务快照到 ~/.abs/log/wrapup.log(下会话 load 时展示滞留对账)。',
|
|
282
|
-
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
283
|
-
async ({ cwd }) => {
|
|
284
|
-
const root = await findBrainRoot(cwd || process.cwd());
|
|
285
|
-
if (!root) return errNoBrain(cwd || process.cwd());
|
|
286
|
-
return { content: [{ type: 'text', text: await cmdWrapup({ dir: root }) }] };
|
|
287
|
-
}
|
|
288
|
-
);
|
|
289
|
-
|
|
290
306
|
// 错误带逃生口:Agent 收到错要能自救,而不是回来问人。
|
|
291
307
|
// 借 OpenContext 的做法——错误里直接给「退一步怎么做」,且带结构化 code 供程序判分支。
|
|
292
308
|
// 判据:凡 isError 的返回,调用方读完应知道下一步跑什么命令。
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* 注意:自动补全菜单行排在底边之下、且自带内容宽度,不套竖线(否则菜单被框住会错位)。
|
|
19
19
|
*/
|
|
20
20
|
import { CustomEditor } from '@earendil-works/pi-coding-agent'
|
|
21
|
-
import { buildStatus, roundBorder, shortenModel } from './
|
|
21
|
+
import { buildStatus, roundBorder, shortenModel } from './format.ts'
|
|
22
22
|
|
|
23
23
|
const SIDE = '│'
|
|
24
24
|
|
package/hooks/abs.omp.ts
CHANGED
|
@@ -130,6 +130,27 @@ function snapshotWrapup(cwd: string): void {
|
|
|
130
130
|
} catch {} // fire-and-forget
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** 会话关闭时把这场会话落成会话页(2026-11-05,与 pi 侧同一机制)。
|
|
134
|
+
*
|
|
135
|
+
* 为何挂 session_shutdown 而不靠 agent 主动调:「靠提醒让 LLM 养成习惯」本仓已
|
|
136
|
+
* 两次实测否证(concept remind-vs-gate);收尾必须由机械事件触发。
|
|
137
|
+
*
|
|
138
|
+
* 为何传 --session 而不是 --last:`--last` 按文件 mtime 选,同时开两个会话时会选错
|
|
139
|
+
* —— pi 侧实测「关会话 A 却落了 B 的页」。这里能拿到 sid 就传,拿不到才退回 --last。
|
|
140
|
+
*
|
|
141
|
+
* 为何不往对话里插话:走到 shutdown 已经没有对话了,只落盘,输出丢弃。
|
|
142
|
+
* 下次开工 abs load 会把未填的会话页摆出来 —— 摆事实,判断仍归 agent。 */
|
|
143
|
+
function snapshotSessionPage(cwd: string, sid = ""): void {
|
|
144
|
+
const args = ["ingest", "--auto"]
|
|
145
|
+
if (sid) args.push("--session", sid); else args.push("--last")
|
|
146
|
+
try {
|
|
147
|
+
const child = spawn(process.execPath, [ABS_BIN, ...args], {
|
|
148
|
+
cwd: cwd || process.cwd(), stdio: "ignore", detached: true,
|
|
149
|
+
})
|
|
150
|
+
child.unref()
|
|
151
|
+
} catch {} // fire-and-forget,失败不影响关会话
|
|
152
|
+
}
|
|
153
|
+
|
|
133
154
|
/** 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)。 */
|
|
134
155
|
async function findBrain(cwd: string): Promise<string | null> {
|
|
135
156
|
const d = cwd || process.cwd()
|
|
@@ -525,8 +546,8 @@ export function renderPanelLines(
|
|
|
525
546
|
// 上描边行已删除(2026-11-04 与 pi 版同步,用户要求「去掉上边框」)。首行现在是标题行。
|
|
526
547
|
const whoPart = who ? who + (nickname ? ` · ${nickname}` : '') : nickname
|
|
527
548
|
const title = whoPart ? `Todo (${data.total}) — ${whoPart}` : `Todo (${data.total})`
|
|
528
|
-
//
|
|
529
|
-
lines.push(clipToWidth(' ' + fg('accent', title), width))
|
|
549
|
+
// 空看板标题前加 1 格(与输入框文字对齐);有任务时不加,任务行顶到最左(2026-11-04 用户)。
|
|
550
|
+
lines.push(clipToWidth((data.total === 0 ? ' ' : '') + fg('accent', title), width))
|
|
530
551
|
|
|
531
552
|
const lastIdx = data.rows.length - 1
|
|
532
553
|
// 空看板只显示标题行(2026-11-04 用户要求去掉「(无未完成任务)」那行)。
|
|
@@ -540,7 +561,7 @@ export function renderPanelLines(
|
|
|
540
561
|
const box = fg('dim', '[ ]')
|
|
541
562
|
const head = `${fg('dim', branch)} ${box} ${fg(colorOf(t.state), `[${t.state}]`)} ${fg('text', t.id)}${anim}`
|
|
542
563
|
const desc = t.desc ? ` ${fg('dim', '—')} ${fg('muted', t.desc)}` : ''
|
|
543
|
-
lines.push(clipToWidth(
|
|
564
|
+
lines.push(clipToWidth(head + desc, width))
|
|
544
565
|
if (t.kids && t.kids.length) {
|
|
545
566
|
// 子项:标准 md 嵌套列表 → 画成树
|
|
546
567
|
const stem = isLast ? ' ' : fg('dim', '│ ')
|
|
@@ -554,7 +575,7 @@ export function renderPanelLines(
|
|
|
554
575
|
}
|
|
555
576
|
})
|
|
556
577
|
|
|
557
|
-
if (data.hidden > 0) lines.push(clipToWidth(fg('dim', `
|
|
578
|
+
if (data.hidden > 0) lines.push(clipToWidth(fg('dim', ` +${data.hidden} more`), width))
|
|
558
579
|
return lines
|
|
559
580
|
}
|
|
560
581
|
|
|
@@ -845,6 +866,7 @@ export default function absOmpExtension(pi: ExtensionAPI): void {
|
|
|
845
866
|
pi.on("session_shutdown", (_e: SessionShutdownEvent, ctx: ExtensionContext) => {
|
|
846
867
|
stopAnimTimer() // 面板没了,动画定时器必须一起停(否则残留定时器白刷已丢弃的 tui)
|
|
847
868
|
snapshotWrapup(ctx.cwd)
|
|
869
|
+
snapshotSessionPage(ctx.cwd, sessionIdFor(ctx))
|
|
848
870
|
logHook("session_shutdown").catch(() => {})
|
|
849
871
|
})
|
|
850
872
|
}
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -125,6 +125,37 @@ function snapshotWrapup(cwd: string): void {
|
|
|
125
125
|
} catch {} // fire-and-forget
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** 会话关闭时把这场会话落成会话页(2026-11-05)。
|
|
129
|
+
*
|
|
130
|
+
* 为何挂这里而不是靠 agent 主动调:本仓已两次实测「靠提醒让 LLM 养成习惯」必失败
|
|
131
|
+
* (concept remind-vs-gate)——「有疑惑就查」这种规矩写进 skill 开场后也仍然漏
|
|
132
|
+
* (concept 检索有效不等于记忆被用)。所以收尾不能靠它想起来,得靠机械事件。
|
|
133
|
+
*
|
|
134
|
+
* 为何用 session_shutdown 而不是 agent_end:agent_end 是**每一轮回复**结束都触发,
|
|
135
|
+
* 2026-09-21/22 两次实测被用户当场指出「聊一句话就触发」;而且往对话里插消息
|
|
136
|
+
* 这条路已被 2026-09-18 / 2026-10-05 两次用户实报(任务中断 / 卡顿)否决。
|
|
137
|
+
* session_shutdown 是真正关会话,此刻 jsonl 已写完(实测 347 行 0 坏行),读得到全文。
|
|
138
|
+
*
|
|
139
|
+
* 为何不往对话里回话:这里没有对话了。它只落盘,输出丢弃。
|
|
140
|
+
* 下次开工 abs load 会把未填的会话页摆出来 —— 摆事实,判断仍归 agent。
|
|
141
|
+
*
|
|
142
|
+
* 判据在 ingest 的 --auto 里(只收真改过文件的会话);这里只负责把它叫起来。 */
|
|
143
|
+
function snapshotSessionPage(cwd: string, sid = ""): void {
|
|
144
|
+
// ★ 传本会话 id,不用 --last 猜(2026-11-05 实测修正):
|
|
145
|
+
// `--last` 按文件 mtime 选,同时开两个会话时会选错 ——
|
|
146
|
+
// 实测关会话 A 却落了 B 的页,因为 B 的 jsonl 更新。
|
|
147
|
+
// hook 这侧本来就能拿到会话 id(sessionIdFor),传下去即可精确定位。
|
|
148
|
+
// 取不到 id 时才退回 --last(老版本语境 / 宿主不提供),至少还能落一场。
|
|
149
|
+
const args = ["ingest", "--auto"];
|
|
150
|
+
if (sid) args.push("--session", sid); else args.push("--last");
|
|
151
|
+
try {
|
|
152
|
+
const child = spawn(process.execPath, [ABS_BIN, ...args], {
|
|
153
|
+
cwd: cwd || process.cwd(), stdio: "ignore", detached: true,
|
|
154
|
+
})
|
|
155
|
+
child.unref()
|
|
156
|
+
} catch {} // fire-and-forget,失败不影响关会话
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
/** 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)。 */
|
|
129
160
|
async function findBrain(cwd: string): Promise<string | null> {
|
|
130
161
|
const d = cwd || process.cwd()
|
|
@@ -936,8 +967,8 @@ export function renderPanelLines(
|
|
|
936
967
|
// 昵称附在作者名后(会话内固定,启动时随机抽)。
|
|
937
968
|
const whoPart = who ? who + (nickname ? ` · ${nickname}` : '') : nickname
|
|
938
969
|
const title = whoPart ? `Todo (${data.total}) — ${whoPart}` : `Todo (${data.total})`
|
|
939
|
-
//
|
|
940
|
-
lines.push(clipToWidth(' ' + fg('accent', title), width))
|
|
970
|
+
// 空看板标题前加 1 格(与输入框文字对齐);有任务时不加,任务行顶到最左(2026-11-04 用户)。
|
|
971
|
+
lines.push(clipToWidth((data.total === 0 ? ' ' : '') + fg('accent', title), width))
|
|
941
972
|
|
|
942
973
|
const lastIdx = data.rows.length - 1
|
|
943
974
|
// 空看板只显示标题行(2026-11-04 用户要求去掉「(无未完成任务)」那行)。
|
|
@@ -955,7 +986,7 @@ export function renderPanelLines(
|
|
|
955
986
|
const box = fg('dim', '[ ]')
|
|
956
987
|
const head = `${fg('dim', branch)} ${box} ${fg(colorOf(t.state), `[${t.state}]`)} ${fg('text', t.id)}${anim}`
|
|
957
988
|
const desc = t.desc ? ` ${fg('dim', '—')} ${fg('muted', t.desc)}` : ''
|
|
958
|
-
lines.push(clipToWidth(
|
|
989
|
+
lines.push(clipToWidth(head + desc, width))
|
|
959
990
|
if (t.kids && t.kids.length) {
|
|
960
991
|
// 子项:标准 md 嵌套列表 → 画成树
|
|
961
992
|
// ├─ [ ] 子项一
|
|
@@ -973,7 +1004,7 @@ export function renderPanelLines(
|
|
|
973
1004
|
}
|
|
974
1005
|
})
|
|
975
1006
|
|
|
976
|
-
if (data.hidden > 0) lines.push(clipToWidth(fg('dim', `
|
|
1007
|
+
if (data.hidden > 0) lines.push(clipToWidth(fg('dim', ` +${data.hidden} more`), width))
|
|
977
1008
|
return lines
|
|
978
1009
|
}
|
|
979
1010
|
/** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
|
|
@@ -1205,6 +1236,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
1205
1236
|
pi.on("session_shutdown", (_e: any, ctx: any) => {
|
|
1206
1237
|
stopAnimTimer() // 面板没了,动画定时器必须一起停(否则残留定时器白刷已丢弃的 tui)
|
|
1207
1238
|
snapshotWrapup((ctx && ctx.cwd) || process.cwd())
|
|
1239
|
+
snapshotSessionPage((ctx && ctx.cwd) || process.cwd(), sessionIdFor(ctx))
|
|
1208
1240
|
logHook("session_shutdown").catch(() => {})
|
|
1209
1241
|
})
|
|
1210
1242
|
}
|
package/package.json
CHANGED
|
@@ -14,12 +14,17 @@ description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。
|
|
|
14
14
|
怎么查:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
abs recall <关键词>
|
|
18
|
-
|
|
19
|
-
db search <词>
|
|
20
|
-
abs resolve <页名>
|
|
17
|
+
abs recall <关键词> # 有疑问时用:按亮度出最相关的几页(不给词就从当前处境推)
|
|
18
|
+
abs query <词> # 多词 OR,枚举全图字面命中;输出短(页名+摘要),省 token
|
|
19
|
+
abs db search <词> # CLI:亮度排序 + 邻居扩散 + 跨项目;加 --no-rules 省掉 18 条铁律
|
|
20
|
+
abs resolve <页名> # 拿到页名后反查路径,只读那一页
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
MCP 侧走 `abs_search`(等价于 `abs db search --no-rules`)—— 比 `abs_query` 强:
|
|
24
|
+
带亮度排序、能从邻居页扩散、能查到别的项目(标 `[跨项目]`)。
|
|
25
|
+
两者是**详略两档**,不是重复:`abs_query` 给字面命中的页名+摘要,最省;
|
|
26
|
+
`abs_search` 给亮度与扩散,找「我上次怎么解决 X」时更准。
|
|
27
|
+
|
|
23
28
|
查完只读命中那几页,**绝不通读 `.brain/`**。答用 `[[页面名]]` 标来源。
|
|
24
29
|
|
|
25
30
|
## 触发器
|
|
@@ -30,13 +35,15 @@ abs resolve <页名> # 拿到页名后反查路径,只读那一页
|
|
|
30
35
|
|
|
31
36
|
## 命令速查
|
|
32
37
|
|
|
33
|
-
完整帮助 `abs help`。读写优先走 MCP(`abs_load`/`abs_task`/`abs_note`/`abs_query`/`abs_lint`/`abs_rule`),比 CLI 快;`abs wrapup`/`abs teardown-check` 是 hook 内部命令。分工:hook 记技术日志;CLI/MCP 实时落盘;skill 负责深提炼 + 收尾 + 修 index。
|
|
38
|
+
完整帮助 `abs help`。读写优先走 MCP(`abs_load`/`abs_task`/`abs_note`/`abs_search`/`abs_query`/`abs_lint`/`abs_rule`),比 CLI 快;`abs wrapup`/`abs teardown-check` 是 hook 内部命令。分工:hook 记技术日志;CLI/MCP 实时落盘;skill 负责深提炼 + 收尾 + 修 index。
|
|
34
39
|
|
|
35
40
|
```bash
|
|
36
41
|
abs load # 开机读状态
|
|
37
|
-
abs query <词> #
|
|
42
|
+
abs query <词> # 字面 OR 命中,输出短;MCP 侧更推荐 abs_search(更准)
|
|
38
43
|
abs todo add <id> --note "做什么" # 登记;note/memo/state/done 见「进行中」
|
|
39
|
-
abs capture [--layer 1-
|
|
44
|
+
abs capture [--layer 1-6] [--kind …] "…" # 按七层判定落点后写入(L7 走 abs note)
|
|
45
|
+
abs summon # 列出没落页的会话(只读,不写盘)
|
|
46
|
+
abs ingest <会话文件或 id> # 把一场 pi 会话落成会话页(带 session id 双向回溯)
|
|
40
47
|
abs note "经验" [--tags a,b] # 暂存经验(一律 L7 混沌层)
|
|
41
48
|
abs concept <slug> --title "…" # 建概念页骨架
|
|
42
49
|
abs rule add "一句话" # 加 Rules;abs lint 体检;abs init 建图谱
|
|
@@ -121,9 +128,9 @@ abs rule add "一句话" # 加 Rules;abs lint 体检;abs init
|
|
|
121
128
|
| L2 规则 | 这个项目要遵守的做法 | `concepts/` + `layer: 2` | `abs capture --layer 2` |
|
|
122
129
|
| L3 工具 | 某工具怎么用、有什么坑 | `concepts/` + `layer: 3` | `abs capture --layer 3` |
|
|
123
130
|
| L4 认知 | 踩坑的根因与解法 | `concepts/` + `layer: 4` | `abs capture "…"`(默认) |
|
|
124
|
-
| L5 会话 | 这次做了什么 | `log.md` / `sessions/` | `abs log
|
|
131
|
+
| L5 会话 | 这次做了什么 | `log.md` / `sessions/` | `abs log`(当天工作)/ `abs ingest`(一场会话) |
|
|
125
132
|
| L6 断点 | 双链词 | 不立页,机器自动抽 | 不接手写 |
|
|
126
|
-
| L7 混沌 | 还没想清的碎片 | `sources/` | `abs capture
|
|
133
|
+
| L7 混沌 | 还没想清的碎片 | `sources/` | `abs note`(capture 的 L7 已拒,同一个落点) |
|
|
127
134
|
|
|
128
135
|
判据不明时用 L4 —— 但**知道是工具经验就写 L3**:实测算过,本仓 52 页 concepts 里 23 页其实是 L3。
|
|
129
136
|
|
|
@@ -139,6 +146,43 @@ abs rule add "一句话" # 加 Rules;abs lint 体检;abs init
|
|
|
139
146
|
不传就不写(旧页留空,不强制补)。实测只有 `gotcha` 能靠 tags/标题机械识别,
|
|
140
147
|
另三类判据在内容里 —— 别拿关键词瞎猜。
|
|
141
148
|
|
|
149
|
+
**尾巴按 kind 分档**(`abs lint` 的 NO-TAIL):`gotcha`/`howto` 必须有验证命令;
|
|
150
|
+
`decision`/`tradeoff` 写「当时依据什么选的」(取舍依据,不是可跑的命令);
|
|
151
|
+
无 `kind` 的旧页保持原要求。理由:决策类知识天生没有验证命令,硬要写就是编。
|
|
152
|
+
|
|
153
|
+
## 会话总结(把整场会话变成知识)
|
|
154
|
+
|
|
155
|
+
abs 不只记编程经验 —— **会话本身的结论也是知识**。两条路径,用途不同:
|
|
156
|
+
|
|
157
|
+
| 想要 | 命令 | 产出 |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| 一场会话干了什么/谈成了什么 | `abs ingest <会话文件或 id 前 8 位>` | `sessions/session-<日期>-s<id8>.md`,带三种骨架之一 |
|
|
160
|
+
| 还没落页的会话 | `abs summon` | 清单,不写盘 |
|
|
161
|
+
|
|
162
|
+
`abs ingest` 按「改过什么文件」机械判形状,不让模型猜:
|
|
163
|
+
|
|
164
|
+
- **技术类**(改了代码)→ 出了什么问题 / 怎么解决的 / 验证什么命令
|
|
165
|
+
- **业务操作类**(只改数据/配置/文档)→ 做了什么操作 / 结果怎样 / 下次注意事项
|
|
166
|
+
- **讨论类**(一个文件没改)→ 谈成了什么 / 没结论的地方
|
|
167
|
+
|
|
168
|
+
它只给骨架和素材(用户原话、改过的文件、工具调用次数),**不写结论** —— 内容要读会话后填。
|
|
169
|
+
|
|
170
|
+
**与会话的双向挂钩**(不要手改这三行):`session`(pi 会话 id)、`session-file`(jsonl 文件名)、
|
|
171
|
+
`project`(会话发生时的 cwd)。页文件名也带会话 id 前 8 位,拿会话文件能反查有没有总结过。
|
|
172
|
+
|
|
173
|
+
会话页在检索里的待遇按「填没填」分两档(判据 `digestFilled`,`layers.js`):
|
|
174
|
+
|
|
175
|
+
| 状态 | 检索 |
|
|
176
|
+
|---|---|
|
|
177
|
+
| **已填**(骨架段有真内容) | 亮度乘 0.5,能搜到也能扩散,但不抢知识页位置 |
|
|
178
|
+
| **未填**(骨架段只有占位注释) | **整页排除**,种子和扩散都进不来 |
|
|
179
|
+
|
|
180
|
+
未填整页排除的理由(实测):空骨架的「用户原话」段词频高、正文又短,bm25 天然占优。
|
|
181
|
+
造 20 张空骨架后查「登录超时」,前 12 名全是空页,真正讲过该问题的知识页被彻底挤出结果。
|
|
182
|
+
|
|
183
|
+
`abs load` 顶部会列出未填的张数(最多 5 个页名,按日期倒序)。**不必全部填** ——
|
|
184
|
+
挑最近、有结论的几场;其余留着当流水,不填也不会污染检索。
|
|
185
|
+
|
|
142
186
|
`abs capture` 之后跑 `abs db rebuild` 让图数据库收录,否则检索拿不到它。
|
|
143
187
|
|
|
144
188
|
## 进行中:什么时候动它
|
package/src/brain/capture.js
CHANGED
|
@@ -22,12 +22,13 @@ import { autoIndex } from './db-cmd.js';
|
|
|
22
22
|
import { summonTail } from './summon.js';
|
|
23
23
|
|
|
24
24
|
/** 层 → 落点目录。L1/L6 不立页,由下面的分支单独处理。 */
|
|
25
|
+
// 落点目录。L1/L6 不立页、L5/L7 不在 capture 职责内,都由下面的分支单独处理。
|
|
26
|
+
// (2026-11-05 审查删掉 `7: 'sources'`:L7 改成直接拒后它已是死代码,
|
|
27
|
+
// 留着会让下个读者以为走 capture 就能落混沌层。)
|
|
25
28
|
const LAYER_DIR = {
|
|
26
29
|
2: 'concepts',
|
|
27
30
|
3: 'concepts',
|
|
28
31
|
4: 'concepts',
|
|
29
|
-
5: 'sessions',
|
|
30
|
-
7: 'sources',
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
/** 判据表:输入时就看得到,不用记。 */
|
|
@@ -37,9 +38,9 @@ export function layerGuide() {
|
|
|
37
38
|
['2', '规则', '这个项目要遵守的做法', 'abs capture --layer 2 "…"'],
|
|
38
39
|
['3', '工具', '某工具怎么用、有什么坑', 'abs capture --layer 3 "…"'],
|
|
39
40
|
['4', '认知', '踩坑的根因与解法(默认)', 'abs capture "…"'],
|
|
40
|
-
['5', '会话', '这次会话做了什么', 'abs log / abs
|
|
41
|
+
['5', '会话', '这次会话做了什么', 'abs log / abs ingest'],
|
|
41
42
|
['6', '断点', '双链词,机器自动抽,不用手写', '(无)'],
|
|
42
|
-
['7', '混沌', '还没想清的碎片', 'abs
|
|
43
|
+
['7', '混沌', '还没想清的碎片', '用 abs note(同一个落点)'],
|
|
43
44
|
];
|
|
44
45
|
return [
|
|
45
46
|
'按七层判定这一句该落在哪:',
|
|
@@ -58,7 +59,8 @@ export async function cmdCapture({ dir, text, layer, tags, title, kind }) {
|
|
|
58
59
|
|
|
59
60
|
if (!clean) {
|
|
60
61
|
return [
|
|
61
|
-
'用法: abs capture [--layer 1-
|
|
62
|
+
'用法: abs capture [--layer 1-6] [--kind gotcha|decision|tradeoff|howto] "一句话" [--tags a,b] [--title "页标题"]',
|
|
63
|
+
' 注: L7(混沌/碎片)用 abs note,两者落点相同,不再留两个入口。',
|
|
62
64
|
'',
|
|
63
65
|
layerGuide(),
|
|
64
66
|
].join('\n');
|
|
@@ -79,6 +81,34 @@ export async function cmdCapture({ dir, text, layer, tags, title, kind }) {
|
|
|
79
81
|
return '✗ L6 断点层是机器从双链里自动抽的词,不接手写。\n 想加索引词:在页面正文写 [[词]],abs db rebuild 时会自动收录。';
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
// L7 混沌层:`abs note` 与 `abs capture --layer 7` 是**同一个落点**
|
|
85
|
+
// (都进 sources/,同样的文件名与正文结构)。实测 2026-11-05 确认。
|
|
86
|
+
// 两个入口做同一件事 = 每次都得先选一个,而选不选对没有任何后果 ——
|
|
87
|
+
// 纯认知负担。按本仓判据「与已有功能重叠 → 复用/删,别新增」
|
|
88
|
+
// (concept feature-delete-not-patch),保留被真实用过 228 次的 note,
|
|
89
|
+
// 这里直接拒,并给出等价命令 —— 不静默转发(静默转发会把两个入口都留着)。
|
|
90
|
+
if (L === 7) {
|
|
91
|
+
return [
|
|
92
|
+
'✗ L7 混沌层用 abs note,不用 capture。',
|
|
93
|
+
` abs note "${clip(clean, 40)}"`,
|
|
94
|
+
' 两者落点完全相同(都进 sources/);note 是这条主路径,别再二选一。',
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// L5 会话层同样不接 capture(2026-11-05 审查发现):它落 sessions/,
|
|
99
|
+
// 而 sessions/ 有两条硬规则(lint 的 SESSIONS-NAMING / SESSIONS-MISPLACED)——
|
|
100
|
+
// 当天只允许 `log-<日期>.md`、tags 必须是 session-log 类。capture 生成的
|
|
101
|
+
// `<日期>-<内容>.md` + `tags: [concept]` 必然被这两条同时报错。
|
|
102
|
+
// 实测:`abs capture --layer 5 "x"` → lint 报 2 个问题。命令自己产出坏数据。
|
|
103
|
+
// 真实用法各有正主:当天工作 → abs log;一场会话的结构化记录 → abs ingest。
|
|
104
|
+
if (L === 5) {
|
|
105
|
+
return [
|
|
106
|
+
'✗ L5 会话层不接 capture(它落 sessions/,会违反那里的命名与 tags 规则)。',
|
|
107
|
+
' 当天工作成果 → abs log "完成 X:…"',
|
|
108
|
+
' 一场会话的结构化记录 → abs ingest --last(或给会话 id)',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
// L1 铁律:复用 cmdRule 的门槛(≤42 字、禁链接),不另起一套
|
|
83
113
|
if (L === 1) {
|
|
84
114
|
const { cmdRule } = await import('./rules.js');
|
package/src/brain/db-cmd.js
CHANGED
|
@@ -190,7 +190,7 @@ export async function cmdDbRebuild({ dir } = {}) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/** `abs db search <词>` —— 走库搜索:FTS 种子 → 亮度扩散。 */
|
|
193
|
-
export async function cmdDbSearch({ query, limit = 8, full = false } = {}) {
|
|
193
|
+
export async function cmdDbSearch({ query, limit = 8, full = false, noRules = false } = {}) {
|
|
194
194
|
const q = String(query || '').trim();
|
|
195
195
|
if (!q) return '用法: abs db search <词>';
|
|
196
196
|
|
|
@@ -208,7 +208,11 @@ export async function cmdDbSearch({ query, limit = 8, full = false } = {}) {
|
|
|
208
208
|
const lit = illuminate(h, seeds, { limit });
|
|
209
209
|
|
|
210
210
|
const out = [`db search [${q}] → 种子 ${seeds.length} 条,亮起 ${lit.length} 页`];
|
|
211
|
-
|
|
211
|
+
// --no-rules:不输出 L1 铁律(2026-11-05)。
|
|
212
|
+
// 为何要它:铁律 18 条,占输出近一半。开工时(abs load)确实该先看,
|
|
213
|
+
// 但「查一个主题」时它们是噪音 —— 实测同一查询 43 行里有 4 行是铁律区,
|
|
214
|
+
// 而 MCP 侧每次调用都受这个成本影响。省略不等于不遵守:铁律由 load 负责送达。
|
|
215
|
+
if (rules.length && !noRules) {
|
|
212
216
|
out.push('', `--- L1 铁律 ${rules.length} 条(任何检索先出)---`);
|
|
213
217
|
for (const r of rules) out.push(`- ${r}`);
|
|
214
218
|
}
|
|
@@ -256,9 +260,13 @@ export async function cmdDbSearch({ query, limit = 8, full = false } = {}) {
|
|
|
256
260
|
if (body.split('\n').length > 60) out.push(' │ …(已截断,用 abs resolve 看全文)');
|
|
257
261
|
}
|
|
258
262
|
}
|
|
259
|
-
|
|
263
|
+
if (noRules) {
|
|
264
|
+
out.push('', '(↝ 表示由邻居扩散点亮,非直接命中;铁律已省略,开工时 abs load 会给)');
|
|
265
|
+
} else {
|
|
266
|
+
out.push('', '(L1 铁律恒在最前;↝ 表示由邻居扩散点亮,非直接命中)');
|
|
267
|
+
}
|
|
260
268
|
out.push('(只给摘要与成本;要看全文: abs db search <词> --full 或 abs resolve <页名>)');
|
|
261
|
-
if (project) {
|
|
269
|
+
if (project && !noRules) {
|
|
262
270
|
out.push(`(铁律只显示本项目 "${project}" 的 —— 别的项目的铁律不外借)`);
|
|
263
271
|
}
|
|
264
272
|
return out.join('\n');
|