@fanchao8609/agent_brain_sync 1.9.8 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/abs.pi.ts +533 -3
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +102 -330
- package/src/store.js +13 -3
- package/src/userconfig.js +40 -0
package/hooks/abs.pi.ts
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* 已删除(2026-09-18):收尾注入本身(agent_end + sendUserMessage deliverAs=followUp)。
|
|
11
11
|
* 实报「每次干活干一会出来, 任务就中断了」。两次同一个根因:
|
|
12
12
|
* **往对话里插一句话本身就是设计错误,不是频率问题** —— 改守卫(每轮→每会话)治不了它。
|
|
13
|
-
*
|
|
13
|
+
* 别再加回来(指 sendUserMessage 注入;静态 system prompt 内容不在此列,见下方 todo 指引)。
|
|
14
14
|
*/
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
16
|
-
import { appendFile, mkdir, stat } from "node:fs/promises"
|
|
16
|
+
import { appendFile, mkdir, readFile, stat } from "node:fs/promises"
|
|
17
17
|
import { homedir } from "node:os"
|
|
18
18
|
import { join } from "node:path"
|
|
19
19
|
import { spawn } from "node:child_process"
|
|
@@ -57,21 +57,550 @@ async function findBrain(cwd: string): Promise<string | null> {
|
|
|
57
57
|
*/
|
|
58
58
|
let agentEndSeen = false
|
|
59
59
|
|
|
60
|
+
/** “刚才调的是 todo 工具”标记 —— 由 tool_call 置位、tool_execution_end 消费。
|
|
61
|
+
* 同样在模块级,理由同上(闭包 flag 在重复注册时不共享)。 */
|
|
62
|
+
let pendingTodoTool = false
|
|
63
|
+
|
|
60
64
|
/** 会话边界重置埋点状态。
|
|
61
65
|
* 不重置 → 同进程第二个会话继承 true 永久不留痕(2026-09-13 实测过的坑)。 */
|
|
62
66
|
function resetThrottle(): void {
|
|
63
67
|
agentEndSeen = false
|
|
68
|
+
pendingTodoTool = false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// todo 及时性验证(2026-10-03):静态 system prompt 指引
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// 待验证假设:abs 的 todo「不及时」不是工具问题,是**触发机制**问题 ——
|
|
75
|
+
// `abs_task` 的指引只存在于工具 description 里,模型常忽略;而 pi 原生工具的
|
|
76
|
+
// `promptGuidelines` 会进 system prompt 的 Guidelines 段,每轮都在。
|
|
77
|
+
//
|
|
78
|
+
// 与历史两次失败做法的**分界线**(必须守住):
|
|
79
|
+
// 删掉的两次都是 `sendUserMessage(deliverAs:"followUp")` —— **往对话里插话**,
|
|
80
|
+
// 抢走一个 turn、打断用户。本次是**静态 prompt 内容**:不新增消息、不抢 turn、
|
|
81
|
+
// 不产生任何对话条目,只是连同其它 Guidelines 一起渲染进 system prompt。
|
|
82
|
+
// 若将来要扩展,也绝不能退化成插话。
|
|
83
|
+
//
|
|
84
|
+
// 关掉即设 ABS_TODO_GUIDE=0。
|
|
85
|
+
// 工具名写 `abs_task`(不带 mcp__abs__ 前缀)—— 2026-10-03 审查发现:MCP 默认
|
|
86
|
+
// exposure=codemode,模型侧看到的就叫 abs_task;写错名字等于让模型去找不存在的工具。
|
|
87
|
+
const TODO_GUIDELINES = [
|
|
88
|
+
'Use `abs_task` to track multi-step work **before** you start it, not after: on the first file edit of a task, call action "start" with a short id.',
|
|
89
|
+
'Mark a task "done" immediately when it finishes — never batch completions at the end of a session.',
|
|
90
|
+
'Before starting a task, record the checkpoint with action "note" (which file, which step) so a later session can resume.',
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
/** 当前项目是否有 .brain 图谱 —— 有才加指引(没图谱的项目里这指引是噪音)。
|
|
94
|
+
*
|
|
95
|
+
* 为何不用工具名判断(2026-10-03 两次实测都错):
|
|
96
|
+
* ① event.systemPromptOptions.selectedTools 在 handler 里是**基线值**,真实表要等
|
|
97
|
+
* handler 之后(agent-session.js:1573)才回填 → 实测 tools=31 无 abs。
|
|
98
|
+
* ② 改用 pi.getActiveTools() 仍为假 —— MCP 默认 exposure=codemode,工具**本就
|
|
99
|
+
* 不进 active 工具表**(且 pi 的 MCP 是懒连接,实测提示 "lazy: from cache, not
|
|
100
|
+
* connected yet")。
|
|
101
|
+
* 结论:判断「abs 能不能用」不该看工具注册表,直接看**项目有没有 .brain/**。 */
|
|
102
|
+
async function hasBrain(cwd: string): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
const st = await stat(join(cwd || process.cwd(), '.brain'))
|
|
105
|
+
return st.isDirectory()
|
|
106
|
+
} catch {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function injectTodoGuidelines(options: any): boolean {
|
|
112
|
+
if (String(process.env.ABS_TODO_GUIDE || '') === '0') return false
|
|
113
|
+
const list: string[] = options.promptGuidelines || (options.promptGuidelines = [])
|
|
114
|
+
for (const g of TODO_GUIDELINES) {
|
|
115
|
+
if (!list.includes(g)) list.push(g)
|
|
116
|
+
}
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// todo 面板(2026-10-03):把 .brain/todo.md 的未完成任务显示在编辑器上方
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// 纯展示层:只读 todo.md,不写任何东西。数据源就是 abs 自己的看板 —— 不做第二套状态。
|
|
124
|
+
// 与 rpiv-todo 的区别:那个把状态存会话 transcript(跨会话丢失),我们用磁盘文件(可续接)。
|
|
125
|
+
// 关掉即设 ABS_TODO_PANEL=0。
|
|
126
|
+
const PANEL_KEY = "abs-todo-panel"
|
|
127
|
+
const PANEL_MAX_ROWS = 10
|
|
128
|
+
|
|
129
|
+
// 标题后缀昵称池(2026-10-03 用户提供)—— 每次**启动会话**随机抽一条,会话内保持不变。
|
|
130
|
+
// 不能每帧随机(panel 每帧都重渲染,那样会疯狂闪烁)。
|
|
131
|
+
const USER_NICKNAMES = [
|
|
132
|
+
'🛌 昼伏夜出型',
|
|
133
|
+
'😴 沾枕头就醒',
|
|
134
|
+
'🌙 熬夜当修仙',
|
|
135
|
+
'⏰ 闹钟十连败',
|
|
136
|
+
'🧟 永远睡不醒',
|
|
137
|
+
'☕ 靠咖啡续命',
|
|
138
|
+
'🧋 靠奶茶续命',
|
|
139
|
+
'🍚 干饭第一名',
|
|
140
|
+
'🥘 吃辣只敢微辣',
|
|
141
|
+
'🍳 炸厨房常客',
|
|
142
|
+
'🥗 间歇性减肥',
|
|
143
|
+
'🍜 深夜爱放毒',
|
|
144
|
+
'🧊 冰镇式养生',
|
|
145
|
+
'⌨️ 键盘上摸鱼',
|
|
146
|
+
'🐟 划水大师',
|
|
147
|
+
'💻 复制粘贴大师',
|
|
148
|
+
'🚽 带薪上厕所',
|
|
149
|
+
'🕕 到点就跑',
|
|
150
|
+
'📅 明天再说吧',
|
|
151
|
+
'🐷 坚决不加班',
|
|
152
|
+
'😇 表面在微笑',
|
|
153
|
+
'🤯 内心已掀桌',
|
|
154
|
+
'💸 月月过路财神',
|
|
155
|
+
'🏦 隐形负翁',
|
|
156
|
+
'🤑 转发锦鲤求暴富',
|
|
157
|
+
'📉 钱包越来越瘦',
|
|
158
|
+
'🛒 购物车首富',
|
|
159
|
+
'📱 网上冲浪选手',
|
|
160
|
+
'🛸 意念回复专家',
|
|
161
|
+
'📞 电话一响就慌',
|
|
162
|
+
'🙈 已读绝不回',
|
|
163
|
+
'🤝 只管埋头夹菜',
|
|
164
|
+
'📝 收藏从不用',
|
|
165
|
+
'📚 学了就忘',
|
|
166
|
+
'🧠 记忆力七秒',
|
|
167
|
+
'🔋 1%才去充电',
|
|
168
|
+
'🛍️ 拆快递狂魔',
|
|
169
|
+
'🧳 云旅游专家',
|
|
170
|
+
'🏋️ 办卡只去洗澡',
|
|
171
|
+
'🚶 步数常年垫底',
|
|
172
|
+
'💇 秃飞猛进',
|
|
173
|
+
'🏴☠️ 飞翔荷兰人',
|
|
174
|
+
'🐌 蜗牛速度选手',
|
|
175
|
+
'🦥 躺平专业户',
|
|
176
|
+
'🎮 打完这把就睡',
|
|
177
|
+
'🍿 吃瓜第一线',
|
|
178
|
+
'🔍 搜索两小时',
|
|
179
|
+
'🧩 爱钻牛角尖',
|
|
180
|
+
'💤 梦里也在编程',
|
|
181
|
+
'🚀 明早一定做',
|
|
182
|
+
'🧘 边熬夜边养生',
|
|
183
|
+
'📺 刷剧不眨眼',
|
|
184
|
+
'🎧 单曲循环中',
|
|
185
|
+
// ── 职场抱怨(2026-10-03 第二批)──
|
|
186
|
+
'🫠 靠不住选手',
|
|
187
|
+
'🧯 专业背锅侠',
|
|
188
|
+
'🪑 会议室钉子户',
|
|
189
|
+
'📎 工具人本人',
|
|
190
|
+
'🫥 存在感为零',
|
|
191
|
+
'🥄 打杂一把好手',
|
|
192
|
+
'🧮 人形Excel',
|
|
193
|
+
'📋 需求搬运工',
|
|
194
|
+
'🔧 万能补丁匠',
|
|
195
|
+
'🗣️ 会议两小时',
|
|
196
|
+
'💬 一言不合拉会',
|
|
197
|
+
'📊 对齐一整天',
|
|
198
|
+
'🌀 讨论没结论',
|
|
199
|
+
'🎯 追需求成瘾',
|
|
200
|
+
'📝 纪要孤儿',
|
|
201
|
+
'🔥 排期永远紧',
|
|
202
|
+
'⏳ 催到怀疑人生',
|
|
203
|
+
'🌃 下班天已黑',
|
|
204
|
+
'📆 周末待命',
|
|
205
|
+
'🚨 临时插需求',
|
|
206
|
+
'🧨 上线前改需求',
|
|
207
|
+
'📈 汇报全靠编',
|
|
208
|
+
'🎤 PPT大师',
|
|
209
|
+
'🤡 背锅第一名',
|
|
210
|
+
'🫡 收到马上办',
|
|
211
|
+
'🙃 领导说得对',
|
|
212
|
+
'👏 掌声最热烈',
|
|
213
|
+
'🏓 甩锅乒乓球',
|
|
214
|
+
'🙋 不背锅侠',
|
|
215
|
+
'📮 抄送战斗机',
|
|
216
|
+
'🧊 已读不回群',
|
|
217
|
+
'🔕 消息免打扰',
|
|
218
|
+
'🪫 电量剩5%',
|
|
219
|
+
'🧓 入行即养老',
|
|
220
|
+
' 心力耗尽',
|
|
221
|
+
'😮💨 叹气专业户',
|
|
222
|
+
'🪦 激情已入土',
|
|
223
|
+
'📤 简历常年挂着',
|
|
224
|
+
'🧳 随时准备跑路',
|
|
225
|
+
'💼 骑驴找马中',
|
|
226
|
+
'🪙 谈薪谈不动',
|
|
227
|
+
'🥲 涨薪等明年',
|
|
228
|
+
// ── 自嘲(2026-10-03 第三批)──
|
|
229
|
+
'🎲 编程全靠蒙',
|
|
230
|
+
'🙏 AI救我狗命',
|
|
231
|
+
'🐛 Bug制造机',
|
|
232
|
+
'🔮 玄学调参',
|
|
233
|
+
'📿 面向祈祷编程',
|
|
234
|
+
'🩹 补丁摞补丁',
|
|
235
|
+
'🤞 能跑就行',
|
|
236
|
+
'🗿 代码能跑别动',
|
|
237
|
+
'🎰 随机数人生',
|
|
238
|
+
'🧙 咒语背诵者',
|
|
239
|
+
'📖 文档从不看',
|
|
240
|
+
'⌨️ 只会复制粘贴',
|
|
241
|
+
'🫠 菜得安详',
|
|
242
|
+
'🥹 菜狗本狗',
|
|
243
|
+
'🐣 刚会写Hello',
|
|
244
|
+
'🧸 删库跑路预备',
|
|
245
|
+
'🪫 脑子已关机',
|
|
246
|
+
'🫥 假装很忙',
|
|
247
|
+
'🎭 专业演技派',
|
|
248
|
+
'🃏 气氛组组长',
|
|
249
|
+
'🧊 情绪稳定到麻木',
|
|
250
|
+
'🐟 摸鱼终身成就',
|
|
251
|
+
'🛋️ 沙发项目经理',
|
|
252
|
+
'📺 带薪看视频',
|
|
253
|
+
'🍵 带薪养生',
|
|
254
|
+
'💤 工位睡神',
|
|
255
|
+
'⏳ 明日复明日',
|
|
256
|
+
'🗓️ 周报最后写',
|
|
257
|
+
'📉 进度条倒退',
|
|
258
|
+
'🕳️ 坑是自己挖的',
|
|
259
|
+
'🧨 技术债主',
|
|
260
|
+
'💀 穷得响叮当',
|
|
261
|
+
'📵 社交电池耗尽',
|
|
262
|
+
'🍼 成年巨婴',
|
|
263
|
+
'🪞 镜子前叹气',
|
|
264
|
+
'🧦 袜子不成对',
|
|
265
|
+
'🥲 笑着活下去',
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
/** 本会话的昵称 —— 模块级(扩展重复注册时闭包变量不共享,同 agentEndSeen 的坑)。
|
|
269
|
+
* 空串 = 未抽(或 ABS_TODO_NICK=0 关掉)。 */
|
|
270
|
+
let sessionNickname = ''
|
|
271
|
+
|
|
272
|
+
/** 抽一条昵称。传 rnd 便于测试注入;默认 Math.random。关掉:ABS_TODO_NICK=0。 */
|
|
273
|
+
export function pickNickname(rnd: () => number = Math.random): string {
|
|
274
|
+
if (String(process.env.ABS_TODO_NICK || '') === '0') return ''
|
|
275
|
+
const i = Math.min(USER_NICKNAMES.length - 1, Math.floor(rnd() * USER_NICKNAMES.length))
|
|
276
|
+
return USER_NICKNAMES[Math.max(0, i)]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 解析 todo.md 的 ## Todo 区 —— 只收未完成行(- [ ]),Done 区不计。
|
|
280
|
+
*
|
|
281
|
+
* who 不为空时只收“我的任务”:行内有 [[who]] 的,或**完全没标作者**的
|
|
282
|
+
* (老任务/手写的没标,漏掉比多显示更糟)。标了别人的则不收。
|
|
283
|
+
*
|
|
284
|
+
* 断点行(缩进的 ↳)归属到它上一条任务,不单独成条。 */
|
|
285
|
+
export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
286
|
+
total: number
|
|
287
|
+
rows: { state: string; id: string; desc: string; note: string }[]
|
|
288
|
+
hidden: number
|
|
289
|
+
} {
|
|
290
|
+
// 两阶段:先无过滤地按序解析(断点归属需要“上一条”是原文里真正的前一条),
|
|
291
|
+
// 再按作者过滤。若边解析边过滤,被滤掉的条目下方的断点会挂到它上面那一条
|
|
292
|
+
// —— 实测:别人任务的断点会显示在我的任务下面(2026-10-03 审查发现)。
|
|
293
|
+
type Row = { state: string; id: string; desc: string; note: string; authors: string[] }
|
|
294
|
+
const parsed: Row[] = []
|
|
295
|
+
let inTodo = false
|
|
296
|
+
for (const raw of String(md || '').split('\n')) {
|
|
297
|
+
const line = raw.trimEnd()
|
|
298
|
+
if (/^##\s+Todo\s*$/.test(line)) { inTodo = true; continue }
|
|
299
|
+
if (/^##\s+/.test(line)) { inTodo = false; continue }
|
|
300
|
+
if (!inTodo) continue
|
|
301
|
+
// 断点行:` ↳ 断点: …`(缩进)→ 挂到上一条任务
|
|
302
|
+
const note = line.match(/^\s+↳\s*断点:\s*(.*)$/)
|
|
303
|
+
if (note) {
|
|
304
|
+
const last = parsed[parsed.length - 1]
|
|
305
|
+
if (last && !last.note) last.note = note[1].trim()
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
// `- [ ] [状态] <id> …`;状态标记可能缺省
|
|
309
|
+
const m = line.match(/^-\s+\[\s\]\s+(?:\[([^\]]+)\]\s+)?(\S+)\s*(.*)$/)
|
|
310
|
+
if (!m) continue
|
|
311
|
+
const state = m[1] || '进行中'
|
|
312
|
+
const id = m[2]
|
|
313
|
+
const body = m[3] || ''
|
|
314
|
+
const authors = [...body.matchAll(/\[\[([^\]]+)\]\]/g)].map((x) => x[1])
|
|
315
|
+
// 剥掉作者标记(面板上碍眼且无信息量),再取 `—` 后正文
|
|
316
|
+
const desc = body
|
|
317
|
+
.replace(/\[\[[^\]]+\]\]/g, '')
|
|
318
|
+
.replace(/^\s*—\s*/, '')
|
|
319
|
+
.replace(/\s*\(认领\s*\d{4}-\d{2}-\d{2}\)\s*$/, '')
|
|
320
|
+
.trim()
|
|
321
|
+
parsed.push({ state, id, desc, note: '', authors })
|
|
322
|
+
}
|
|
323
|
+
const mine = who
|
|
324
|
+
? parsed.filter((r) => r.authors.length === 0 || r.authors.includes(who))
|
|
325
|
+
: parsed
|
|
326
|
+
const rows = mine.map(({ state, id, desc, note }) => ({ state, id, desc, note }))
|
|
327
|
+
return { total: rows.length, rows: rows.slice(0, max), hidden: Math.max(0, rows.length - max) }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** 当前使用者名(读 ~/.abs/config.json 的 user,ABS_USER 环境变量优先)。
|
|
331
|
+
* 读不到返回空串 —— 那时不做作者过滤(宁可全显示,也别因配置缺失而面板空白)。 */
|
|
332
|
+
async function currentUser(): Promise<string> {
|
|
333
|
+
const env = String(process.env.ABS_USER || '').trim()
|
|
334
|
+
if (env) return env
|
|
335
|
+
try {
|
|
336
|
+
const dir = process.env.ABS_CONFIG_DIR || join(homedir(), '.abs')
|
|
337
|
+
const raw = await readFile(join(dir, 'config.json'), 'utf8')
|
|
338
|
+
const u = JSON.parse(raw).user
|
|
339
|
+
return typeof u === 'string' ? u.trim() : ''
|
|
340
|
+
} catch {
|
|
341
|
+
return ''
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 按显示宽度截断(自己实现,不 import @earendil-works/pi-tui)。
|
|
346
|
+
*
|
|
347
|
+
* 为何不直接用 pi-tui 的 truncateToWidth(2026-10-03 实测):那是 pi 内部 alias,
|
|
348
|
+
* 裸包名只在 pi 的 jiti loader 下能解析 —— 测试环境用原生 node 加载扩展会
|
|
349
|
+
* ERR_MODULE_NOT_FOUND(实测挂了 6 个测试)。依赖宿主内部 alias 太脆。
|
|
350
|
+
*
|
|
351
|
+
* 宽度规则:CJK/全角记 2 列,其余记 1 列。足够让面板不溢出(不能处理 emoji ZWJ 序列,
|
|
352
|
+
* 但面板里没有—— todo 内容是任务名与说明)。 */
|
|
353
|
+
function dispWidth(s: string): number {
|
|
354
|
+
let w = 0
|
|
355
|
+
let i = 0
|
|
356
|
+
while (i < s.length) {
|
|
357
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i))
|
|
358
|
+
if (m) { i += m[0].length; continue }
|
|
359
|
+
const cp = s.codePointAt(i) || 0
|
|
360
|
+
const ch = String.fromCodePoint(cp)
|
|
361
|
+
i += ch.length
|
|
362
|
+
// 常见全角/CJK 区间(足以覆盖中文任务名)
|
|
363
|
+
const wide =
|
|
364
|
+
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
365
|
+
(cp >= 0x2e80 && cp <= 0xa4cf) ||
|
|
366
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
367
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
368
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
369
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
370
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
371
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
372
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff)
|
|
373
|
+
w += wide ? 2 : 1
|
|
374
|
+
}
|
|
375
|
+
return w
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 去掉 ANSI 转义序列(\x1b[...m 等),否则宽度计算会把颜色码也算进去。 */
|
|
379
|
+
function stripAnsi(s: string): string {
|
|
380
|
+
// eslint-disable-next-line no-control-regex
|
|
381
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '')
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 控宽截断:超宽则尾部换省略号(省略号算 1 列)。
|
|
385
|
+
*
|
|
386
|
+
* 注意:入参可能含 ANSI 颜色码(我们的 head 就是 fg() 拼的)。颜色码占 0 列,
|
|
387
|
+
* 必须先剥掉再量宽 —— 否则每行都会被误判超宽而截断。
|
|
388
|
+
* 截断后不再恢复颜色(该行尾会失去颜色,但省略号前的内容色仍在)—— 可接受,
|
|
389
|
+
* 因为只有超长行才会走到这里。 */
|
|
390
|
+
function clipToWidth(s: string, width: number): string {
|
|
391
|
+
if (width <= 0) return ''
|
|
392
|
+
if (dispWidth(stripAnsi(s)) <= width) return s
|
|
393
|
+
const keep = width - 1
|
|
394
|
+
let out = ''
|
|
395
|
+
let w = 0
|
|
396
|
+
let i = 0
|
|
397
|
+
// 逐字符扫描,遇到 ANSI 序列原样跳过(不计宽)
|
|
398
|
+
while (i < s.length) {
|
|
399
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i))
|
|
400
|
+
if (m) { out += m[0]; i += m[0].length; continue }
|
|
401
|
+
const cp = s.codePointAt(i) || 0
|
|
402
|
+
const ch = String.fromCodePoint(cp)
|
|
403
|
+
i += ch.length
|
|
404
|
+
const cw = dispWidth(ch)
|
|
405
|
+
if (w + cw > keep) break
|
|
406
|
+
out += ch
|
|
407
|
+
w += cw
|
|
408
|
+
}
|
|
409
|
+
return out + '…'
|
|
410
|
+
}
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// “进行中”行的迷你动画(2026-10-03 用户需求):三个小符号依次由空心变实心。
|
|
413
|
+
// 帧宽固定 3 列 —— 宽度不变才不会让面板每帧抖动(这也是不用 ⠋⠙⠹ 那种 spinner 的原因:
|
|
414
|
+
// 那个只闪一个符,用户明确要的是“三个点依次点亮”)。
|
|
415
|
+
// 平铺 6 帧:亮→全亮→退回(呼吸感),而不是全亮后硬跳回全空。
|
|
416
|
+
const TODO_ANIM_FRAMES = ['○○○', '●○○', '●●○', '●●●', '●●○', '●○○']
|
|
417
|
+
const TODO_ANIM_MS = 250
|
|
418
|
+
|
|
419
|
+
/** 动画相位(每 tick +1)。模块级 —— 同 agentEndSeen,闭包 flag 在重复注册时不共享。 */
|
|
420
|
+
let animPhase = 0
|
|
421
|
+
let animTimer: ReturnType<typeof setInterval> | undefined
|
|
422
|
+
|
|
423
|
+
/** 启动/保持动画定时器。重复调用不会叠加(已存在则不动)。
|
|
424
|
+
* getTui 是为了每 tick 拿**当前**的 tui 引用 —— widget 可能因刷新被重建,
|
|
425
|
+
* 持旧的引用会调到一个已被丢弃的 renderer。 */
|
|
426
|
+
function startAnimTimer(getTui: () => any): void {
|
|
427
|
+
if (animTimer) return
|
|
428
|
+
animTimer = setInterval(() => {
|
|
429
|
+
animPhase++
|
|
430
|
+
try { getTui()?.requestRender?.() } catch {}
|
|
431
|
+
}, TODO_ANIM_MS)
|
|
432
|
+
// 别阻止进程退出(扩展是宿主进程的一分子,定时器不该吊住事件循环)
|
|
433
|
+
;(animTimer as any)?.unref?.()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function stopAnimTimer(): void {
|
|
437
|
+
if (animTimer) { clearInterval(animTimer); animTimer = undefined }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** 取当前动画帧(phase 每 tick +1,由定时器驱动)。非进行中的行不传 phase(不显示)。 */
|
|
441
|
+
export function todoAnimFrame(phase: number): string {
|
|
442
|
+
const n = TODO_ANIM_FRAMES.length
|
|
443
|
+
const i = ((Math.floor(phase) % n) + n) % n
|
|
444
|
+
return TODO_ANIM_FRAMES[i]
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function renderPanelLines(
|
|
448
|
+
data: { total: number; rows: { state: string; id: string; desc: string; note: string }[]; hidden: number },
|
|
449
|
+
who: string,
|
|
450
|
+
width: number,
|
|
451
|
+
fg: (color: string, s: string) => string,
|
|
452
|
+
animPhase = -1,
|
|
453
|
+
nickname = '',
|
|
454
|
+
): string[] {
|
|
455
|
+
if (data.total === 0) return []
|
|
456
|
+
const colorOf = (state: string): string => (state === '滞留中' ? 'muted' : state === '讨论中' ? 'dim' : 'accent')
|
|
457
|
+
const lines: string[] = []
|
|
458
|
+
// 上描边:一条深灰横线,把面板与上方内容分开。
|
|
459
|
+
// 留 1 列余量 —— 终端对“恰好占满宽度”的行有时会折行(各终端行为不一致)。
|
|
460
|
+
lines.push(fg('dim', '─'.repeat(Math.max(0, width - 1))))
|
|
461
|
+
// 昵称附在作者名后(会话内固定,启动时随机抽)。
|
|
462
|
+
const whoPart = who ? who + (nickname ? ` · ${nickname}` : '') : nickname
|
|
463
|
+
const title = whoPart ? `📋 todo (${data.total}) — ${whoPart}` : `📋 todo (${data.total})`
|
|
464
|
+
lines.push(clipToWidth(fg('accent', title), width))
|
|
465
|
+
|
|
466
|
+
const lastIdx = data.rows.length - 1
|
|
467
|
+
data.rows.forEach((t, i) => {
|
|
468
|
+
// `└─` 只给最后一条 —— 不管它有没有断点。
|
|
469
|
+
// (2026-10-03 审查修正:原条件 `i === lastIdx && !t.note` 会让“末条带断点”时
|
|
470
|
+
// 全篇没有 └─ 收尾,断点行的 │ 又延伸到视觉底部,看起来像列表被截断。)
|
|
471
|
+
const isLast = i === lastIdx
|
|
472
|
+
const branch = isLast ? '└─' : '├─'
|
|
473
|
+
// 进行中的行带一个小动画(其余行不加,避免满屏都在跳)
|
|
474
|
+
const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + fg('accent', todoAnimFrame(animPhase)) : ''
|
|
475
|
+
const head = `${fg('dim', branch)} ${fg(colorOf(t.state), `[${t.state}]`)} ${fg('text', t.id)}${anim}`
|
|
476
|
+
const desc = t.desc ? ` ${fg('dim', '—')} ${fg('muted', t.desc)}` : ''
|
|
477
|
+
lines.push(clipToWidth(head + desc, width))
|
|
478
|
+
if (t.note) {
|
|
479
|
+
// 断点行:挂在父任务下。末条时用空格对齐(└─ 下方无续);否则用 │ 延伸。
|
|
480
|
+
const cont = isLast ? ' ' : fg('dim', '│ ')
|
|
481
|
+
lines.push(clipToWidth(`${cont} ${fg('dim', '↳ ' + t.note)}`, width))
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
if (data.hidden > 0) lines.push(clipToWidth(fg('dim', ` +${data.hidden} more`), width))
|
|
486
|
+
return lines
|
|
487
|
+
}
|
|
488
|
+
async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
|
|
489
|
+
if (String(process.env.ABS_TODO_PANEL || '') === '0') return
|
|
490
|
+
if (!ui || typeof ui.setWidget !== 'function') return
|
|
491
|
+
try {
|
|
492
|
+
if (!(await hasBrain(cwd))) { ui.setWidget(PANEL_KEY, undefined); stopAnimTimer(); return }
|
|
493
|
+
const md = await readFile(join(cwd, '.brain', 'todo.md'), 'utf8')
|
|
494
|
+
const who = await currentUser()
|
|
495
|
+
const data = parseOpenTasks(md, PANEL_MAX_ROWS, who)
|
|
496
|
+
if (data.total === 0) { ui.setWidget(PANEL_KEY, undefined); stopAnimTimer(); return }
|
|
497
|
+
// factory 形式:render(width) 每帧拿真实宽度 → 按宽度截断,不再断字。
|
|
498
|
+
// 动画:phase 存在模块级,render 时读当前值;定时器只在本帧重绘,不重建 widget。
|
|
499
|
+
let tuiRef: any = null
|
|
500
|
+
ui.setWidget(
|
|
501
|
+
PANEL_KEY,
|
|
502
|
+
(tui: any, theme: any) => {
|
|
503
|
+
tuiRef = tui
|
|
504
|
+
return {
|
|
505
|
+
render: (width: number) =>
|
|
506
|
+
renderPanelLines(
|
|
507
|
+
data,
|
|
508
|
+
who,
|
|
509
|
+
width,
|
|
510
|
+
(c: string, s: string) => theme.fg(c, s),
|
|
511
|
+
animPhase,
|
|
512
|
+
sessionNickname,
|
|
513
|
+
),
|
|
514
|
+
invalidate: () => {},
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
{ placement: 'aboveEditor' },
|
|
518
|
+
)
|
|
519
|
+
// 只在真有“进行中”任务时跑定时器 —— 否则白刷 CPU(无动画可播)。
|
|
520
|
+
const needAnim = data.rows.some((t) => t.state === '进行中')
|
|
521
|
+
if (needAnim) startAnimTimer(() => tuiRef)
|
|
522
|
+
else stopAnimTimer()
|
|
523
|
+
} catch {
|
|
524
|
+
try { ui.setWidget(PANEL_KEY, undefined) } catch {}
|
|
525
|
+
stopAnimTimer()
|
|
526
|
+
}
|
|
64
527
|
}
|
|
65
528
|
|
|
66
529
|
export default function absPiHook(pi: ExtensionAPI): void {
|
|
67
530
|
// 埋点状态同上,故意声明在**模块级**(不在被反复调用的工厂函数里)。
|
|
68
531
|
resetThrottle()
|
|
69
532
|
|
|
70
|
-
pi.on("session_start", (event: any,
|
|
533
|
+
pi.on("session_start", (event: any, ctx: any) => {
|
|
71
534
|
resetThrottle()
|
|
535
|
+
// 本会话的随机昵称 —— 在这里抽一次(不是每帧抽,否则面板会疯狂闪)。
|
|
536
|
+
// 每次 session_start(startup/reload/new/resume/fork)重抽 → “每次打开 pi 都是随机的”。
|
|
537
|
+
sessionNickname = pickNickname()
|
|
538
|
+
logHook(`session_start nickname=${sessionNickname || '(off)'}`).catch(() => {})
|
|
539
|
+
// 启动/重载时就画出面板(reason=startup|reload|new|resume|fork)。
|
|
540
|
+
// 时序:pi 在 session_start 时已给 ctx.ui(无 UI 时是 noOp,调了不报错),
|
|
541
|
+
// 但设 setWidget 需真的 TUI —— 故用 hasUI 挡一下并留痕,便于判定“没显示”的原因。
|
|
542
|
+
const cwd = (ctx && ctx.cwd) || process.cwd()
|
|
543
|
+
if (ctx && ctx.hasUI) {
|
|
544
|
+
refreshTodoPanel(ctx.ui, cwd)
|
|
545
|
+
.then(() => logHook(`session_start panel drawn reason=${event?.reason}`).catch(() => {}))
|
|
546
|
+
.catch(() => logHook(`session_start panel error reason=${event?.reason}`).catch(() => {}))
|
|
547
|
+
} else {
|
|
548
|
+
logHook(`session_start panel skipped no_ui reason=${event?.reason}`).catch(() => {})
|
|
549
|
+
}
|
|
72
550
|
return logHook("session_start").catch(() => {})
|
|
73
551
|
})
|
|
74
552
|
|
|
553
|
+
// 面板实时刷新(两事件配合)—— 为何不能只用其中一个:
|
|
554
|
+
// 实测(2026-10-03)MCP 工具在 tool_execution_end 里的 toolName 是代理入口名
|
|
555
|
+
// `mcp__abs`,**不是子工具名**,且该事件不带 args → 无法从它分辨是不是 abs_task。
|
|
556
|
+
// 而 tool_call 带 input,能从里面认出目标工具,但它在**执行前**触发(此刻 todo.md 还没变)。
|
|
557
|
+
// 故:tool_call 认出“刚才调的是 todo 工具”→ 置标记;tool_execution_end 看到标记就刷 → 清标记。
|
|
558
|
+
// 始终保留 turn_end 兜底(实测每条 assistant 消息都触发,约 4 秒一次)。
|
|
559
|
+
// 标记在模块级(重复注册时闭包 flag 不共享 —— 见 agentEndSeen 的注解)。
|
|
560
|
+
pi.on("tool_call", (event: any) => {
|
|
561
|
+
const name = String(event?.toolName || '')
|
|
562
|
+
const input = event?.input || {}
|
|
563
|
+
// 两种形态:① MCP 代理入口 mcp__abs + input.tool=abs_task;② 直接工具名 abs_task
|
|
564
|
+
const target = String(input?.tool ?? input?.name ?? '')
|
|
565
|
+
if (/abs_(task|board)/.test(name) || /abs_(task|board)/.test(target)) {
|
|
566
|
+
pendingTodoTool = true
|
|
567
|
+
logHook(`tool_call todo_tool name=${name} target=${target || '-'}`).catch(() => {})
|
|
568
|
+
}
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
pi.on("tool_execution_end", (_event: any, ctx: any) => {
|
|
572
|
+
if (!pendingTodoTool) return
|
|
573
|
+
pendingTodoTool = false
|
|
574
|
+
logHook(`tool_execution_end panel refresh after todo tool`).catch(() => {})
|
|
575
|
+
refreshTodoPanel(ctx?.ui, (ctx && ctx.cwd) || process.cwd()).catch(() => {})
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
pi.on("turn_end", (_event: any, ctx: any) => {
|
|
579
|
+
const cwd = (ctx && ctx.cwd) || process.cwd()
|
|
580
|
+
logHook(`turn_end panel refresh cwd=${cwd}`).catch(() => {})
|
|
581
|
+
refreshTodoPanel(ctx?.ui, cwd).catch(() => {})
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
// todo 及时性验证(2026-10-03):往 system prompt 的 Guidelines 段追加静态条目。
|
|
585
|
+
// 不新增消息、不抢 turn、不产生对话条目 —— 与删掉的两次注入做法本质不同(见文件头)。
|
|
586
|
+
// 留一行日志:否则「有没有生效」只能凭感觉,无法验证。
|
|
587
|
+
pi.on("before_agent_start", (event: any, ctx: any) => {
|
|
588
|
+
const cwd = (ctx && ctx.cwd) || process.cwd()
|
|
589
|
+
// 异步 handler:pi 会 await(emitBeforeAgentStart 是 await 的),故可以查盘。
|
|
590
|
+
return (async () => {
|
|
591
|
+
try {
|
|
592
|
+
if (!(await hasBrain(cwd))) {
|
|
593
|
+
logHook(`before_agent_start todo_guide=off reason=no_brain`).catch(() => {})
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
const ok = injectTodoGuidelines(event?.systemPromptOptions)
|
|
597
|
+
logHook(`before_agent_start todo_guide=${ok ? 'on' : 'off'} cwd=${cwd}`).catch(() => {})
|
|
598
|
+
} catch (e: any) {
|
|
599
|
+
logHook(`before_agent_start error=${e?.message || 'unknown'}`).catch(() => {})
|
|
600
|
+
}
|
|
601
|
+
})()
|
|
602
|
+
})
|
|
603
|
+
|
|
75
604
|
// 收尾注入已停用(2026-09-18):sendUserMessage(deliverAs:"followUp") 会在每轮
|
|
76
605
|
// agent_end 抢一个 follow-up turn,用户实报「每次干活干一会出来, 任务就中断了」。
|
|
77
606
|
//
|
|
@@ -88,6 +617,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
88
617
|
})
|
|
89
618
|
|
|
90
619
|
pi.on("session_shutdown", (_e: any, ctx: any) => {
|
|
620
|
+
stopAnimTimer() // 面板没了,动画定时器必须一起停(否则残留定时器白刷已丢弃的 tui)
|
|
91
621
|
snapshotWrapup((ctx && ctx.cwd) || process.cwd())
|
|
92
622
|
logHook("session_shutdown").catch(() => {})
|
|
93
623
|
})
|
package/package.json
CHANGED
|
@@ -1,413 +1,185 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abs-agent-brain-sync
|
|
3
|
-
description: abs (agent-brain-sync)
|
|
3
|
+
description: abs (agent-brain-sync) 跨会话记忆与任务续接。适用于「开工续接状态」「沉淀会话收获」「任务登记不丢」「经验落成知识页」「图谱体检」等任务。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# abs — 跨会话记忆 (agent-brain-sync)
|
|
7
7
|
|
|
8
8
|
## 最高优先:触发总则(凌驾本文所有流程)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**关键词不是触发器,意图才是。**句子里出现 abs 词(收尾/todo/沉淀/load/log/note)不等于要执行 abs 动作:谈论、提问、吐槽、定规则("太啰嗦""这个设计怎样")**只回应,不执行动作**;明确让我做事才执行。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
- 拿不准时**先问**。规则变更("以后简短点")写入本文,不执行动作。
|
|
13
|
+
- 输出简短:收尾/提示/todo 汇报一两句。本总则适用于所有工具。
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|---|---|
|
|
16
|
-
| 谈论、提问、吐槽、定规则("太啰嗦""要简短""这个设计怎样") | **只回应,不执行动作** |
|
|
17
|
-
| 明确让我做事("收尾""记一下""去做") | 执行 |
|
|
18
|
-
|
|
19
|
-
**判断依据是整段话的意图,不是里面出现过哪个词。**
|
|
20
|
-
|
|
21
|
-
反例(真实发生):用户说"尽量简短的汇报" —— 那是在**定规则**,不是在**下命令**;
|
|
22
|
-
被误当成指令跑了一次收尾。
|
|
15
|
+
## 命令速查
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- 规则变更("以后简短点")写入本文,**不执行动作**。
|
|
27
|
-
- 输出简短:收尾/提示/todo 汇报尽量一两句,不写小作文。
|
|
28
|
-
- 本总则适用于所有工具,不限 abs。
|
|
29
|
-
|
|
30
|
-
把 AI 编码经验从会话沙盒里救出来。每个会话都是无状态的——Claude、OpenCode、Cursor
|
|
31
|
-
各开一堆会话,经验/进度/踩坑全碎片化,重开像失忆。本技能用一个放**项目根目录**、
|
|
32
|
-
Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
33
|
-
**骨架/任务/暂存/检索/体检走 abs 工具(不手工建骨架、不手工登记任务);深提炼(把暂存经验
|
|
34
|
-
写成 concept/entity 页)必须手工——那是判断力,abs 不替你判断什么值得沉淀。**
|
|
35
|
-
|
|
36
|
-
## CLI 命令速查(`abs`,完整帮助:`abs help`)
|
|
17
|
+
完整帮助:`abs help`。agent 读写优先走 MCP(`abs_load`/`abs_task`/`abs_note`/`abs_query`/`abs_lint`/`abs_rule`)比 bash 跑 CLI 快;`abs wrapup` / `abs teardown-check` 是 hook 内部命令。
|
|
18
|
+
分工:hook 自动记技术日志到 `~/.abs/log/`(不用管);CLI/MCP 实时落盘;**skill 负责深提炼 + 收尾 + 修 index**。
|
|
37
19
|
|
|
38
20
|
```bash
|
|
39
|
-
#
|
|
40
|
-
abs
|
|
41
|
-
abs
|
|
42
|
-
abs
|
|
43
|
-
abs status # 当前项目 + 图谱概要
|
|
44
|
-
abs query <词1> [词2 …] # 检索 .brain/ 知识页(多词 OR)
|
|
45
|
-
abs resolve <页名或id> # 反查页面路径(改名后 id 不变)
|
|
46
|
-
abs lint # 图谱体检(死链/悬挂/超限/堆积/未提炼/Rules 超限)
|
|
47
|
-
|
|
48
|
-
# 写
|
|
49
|
-
abs todo add <id> --note "做什么" # 登记任务(start 同义)
|
|
50
|
-
abs todo note <id> --note "断点/进度" # 实时落 ↳ 断点 行
|
|
51
|
-
abs todo state <id> --note "进行中|讨论中|滞留中" # 改状态标记(原地)
|
|
52
|
-
abs todo done <id> [--as 落地|否决|仅方案] # 完成(默认 落地)
|
|
53
|
-
abs log "完成 X:…" # 记一行工作成果(无参=查看)
|
|
54
|
-
abs note "经验一句话" [--tags 坑,docker] # 经验实时暂存 → sources/
|
|
55
|
-
abs concept <slug> --title "标题" [--tags a,b] # 建概念页骨架(头/中/尾,只给结构不给内容)
|
|
56
|
-
abs rule [add "一句话"] # 读写 index.md 的 ## Rules 硬规则
|
|
57
|
-
|
|
58
|
-
# 维护
|
|
59
|
-
abs todo archive [--keep-days N] [--dry-run] # 归档 Done 旧日期组 → sessions/
|
|
60
|
-
abs init [--repair] # 建图谱;--repair 只补缺不覆盖
|
|
61
|
-
abs config [set user <名字>] # 使用者姓名(写操作需先设)
|
|
21
|
+
abs load # 开机读状态;abs query <词> 检索;abs resolve <页名> 反查路径
|
|
22
|
+
abs todo add <id> --note "做什么" # 登记(note 补断点 / state 改标记 / done 完成);abs log "完成 X" 记成果
|
|
23
|
+
abs note "经验" [--tags a,b] # 暂存经验;abs concept <slug> --title "…" 建概念页骨架
|
|
24
|
+
abs rule [add "一句话"] # 读写 ## Rules;abs lint 体检;abs todo archive 归档;abs init 建图谱
|
|
62
25
|
```
|
|
63
26
|
|
|
64
|
-
> **agent 读写优先走 MCP**(`abs_load`/`abs_task`/`abs_note`/`abs_query`/`abs_lint`/
|
|
65
|
-
> `abs_rule`)—— 常驻 ~2ms,比 bash 跑 CLI(每次起 node 进程 27ms)快一个量级。
|
|
66
|
-
> CLI 留给「人手动查看」。`abs wrapup` / `abs teardown-check` 是 hook 内部命令,不需手动调。
|
|
67
|
-
|
|
68
27
|
## 触发总入口(每次命中技能,第一步先走这里)
|
|
69
28
|
|
|
70
|
-
|
|
71
|
-
然后再看用户真正要什么:
|
|
29
|
+
技能被触发时第一步走这条链,再看用户要什么:
|
|
72
30
|
|
|
73
|
-
1. **找图谱**:只看**当前目录**是否含 `.brain
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
2. **没有?按意图决定建档与否**:
|
|
77
|
-
- 用户意图是**沉淀**(收尾/「把这次记下来」)或**即将开工的长期任务**(会跨会话,
|
|
78
|
-
用词如「帮我做 X / 开工 / 继续开发 / 修 X」)→ **先建档**:`abs init`。别在没图谱时就开写。
|
|
79
|
-
- 用户是**闲聊 / 一次性问句 / 明说不要建档 / 目录只读** → **不建档**,当普通会话处理,
|
|
80
|
-
避免在无关项目乱落文件。
|
|
81
|
-
3. **有/刚建好?分析用户意图**,分流:
|
|
31
|
+
1. **找图谱**:只看**当前目录**是否含 `.brain/`。不许向上搜索(会命中 `~/.brain` 的无关图谱,把项目静默挂错),必须 cd 到项目根。
|
|
32
|
+
2. **没有?按意图定建档**:意图是**沉淀**或**即将开工的长期任务**("帮我做 X / 开工 / 修 X")→ **先 `abs init`**,别在没图谱时就开写;**闲聊 / 一次性问句 / 明说不要建档 / 只读目录** → 不建档,当普通会话处理。
|
|
33
|
+
3. **有/刚建好?分流**:
|
|
82
34
|
|
|
83
35
|
| 用户意图 | 走哪 |
|
|
84
36
|
|---|---|
|
|
85
|
-
| 总结经验 / 结束了 / "把这次记下来" |
|
|
37
|
+
| 总结经验 / 结束了 / "把这次记下来" | 收尾循环(沉淀) |
|
|
86
38
|
| 查询坑 / "我上次怎么解决 X" | `abs query` 检索 |
|
|
87
|
-
| 体检图谱 / `abs lint` | `abs lint`
|
|
88
|
-
| 开新任务 / 继续开发 / "帮我做 X" | 开场 Init Sync
|
|
89
|
-
| 报 bug / 要求加功能 | **受理协议**:先方案 → 再登记 →
|
|
39
|
+
| 体检图谱 / `abs lint` | `abs lint` |
|
|
40
|
+
| 开新任务 / 继续开发 / "帮我做 X" | 开场 Init Sync(续接);新需求先走受理协议 |
|
|
41
|
+
| 报 bug / 要求加功能 | **受理协议**:先方案 → 再登记 → 问开工 |
|
|
90
42
|
| 意图不明 / 默认 | **续 todo**:读未完成项开工续做 |
|
|
91
43
|
|
|
92
|
-
**主心骨**:意图不明且项目有 `.brain/` 时,默认**续 todo
|
|
93
|
-
不是停在闲聊。
|
|
44
|
+
**主心骨**:意图不明且项目有 `.brain/` 时,默认**续 todo**。
|
|
94
45
|
|
|
95
46
|
## 新需求受理协议(bug / 新功能:先方案 → 再登记 → 问开工)
|
|
96
47
|
|
|
97
|
-
用户报 bug
|
|
48
|
+
用户报 bug 或要求加功能时先别动代码,三步:
|
|
98
49
|
|
|
99
|
-
1.
|
|
100
|
-
|
|
101
|
-
- bug → 根因;功能 → 做法。**有证据给证据,没查到就直说"未定位"**
|
|
102
|
-
- 要改哪些文件、怎么验证(跑什么、看什么)
|
|
103
|
-
2. **登记 todo**:`abs todo add <id> --note "<一句话需求+关键约束>"`
|
|
104
|
-
方案里的关键结论(根因/取舍)再 `abs todo note <id> --note ...` 落到断点。
|
|
50
|
+
1. **总结方案**(一屏内,给用户过目):准确复述需求(不确定就写明假设);bug 给根因、功能给做法;要改哪些文件、怎么验证。
|
|
51
|
+
2. **登记 todo**:`abs todo add <id> --note "<需求+关键约束>"`;根因/取舍用 `abs todo note <id>` 落到断点。
|
|
105
52
|
3. **问是否开工**:明确问一句,**等确认再改代码**。
|
|
106
53
|
|
|
107
|
-
|
|
108
|
-
- 用户已说"开工 / 直接做 / 修它" —— 那就是授权
|
|
109
|
-
- 一行级修字、纯查询、纯收尾沉淀 —— 无方案可言
|
|
110
|
-
- 同一需求已登记且用户确认过 —— 接着做即可
|
|
111
|
-
|
|
112
|
-
> 为什么:方案先过目能省掉整轮返工;登记让跨会话可续;"问开工"把决定权留在用户手里。
|
|
113
|
-
> **这不是拖延** —— 总结方案本身就是工作,做完再问。
|
|
114
|
-
## 图谱定位
|
|
115
|
-
|
|
116
|
-
`.brain/` 放**项目根**,一个项目一份。所有命令只认**当前目录**的 `.brain/`(在项目根运行,不传路径)。
|
|
117
|
-
**不向子目录归属,也不向上搜索**(上爬会命中 `~/.brain`,把无关项目静默挂错)。宁可报错也不猜。
|
|
118
|
-
`abs status` 显示当前定位。
|
|
119
|
-
|
|
120
|
-
## `.brain/` 怎么组织(每个文件/分区做什么、怎么用)
|
|
121
|
-
|
|
122
|
-
骨架由 `abs init` 生成(`--repair` 只补缺不覆盖)。
|
|
123
|
-
|
|
124
|
-
### `index.md` —— 图谱入口
|
|
125
|
-
|
|
126
|
-
| 分区 | 放什么 | 怎么用 |
|
|
127
|
-
|---|---|---|
|
|
128
|
-
| `## Rules` | 本项目铁律 | 见下(load 里**原样全量**输出,不折) |
|
|
129
|
-
| `## Concepts` `## Entities` `## Sources` `## Syntheses` `## Sessions` | 各类页的清单 | 每页一行 `- [[slug]] — 一句话`(`abs note`/建归档页会自动登记),load 里折成计数 |
|
|
130
|
-
|
|
131
|
-
**`## Rules` 区**:铁律清单,`abs load` 每次都全量读(代码里明确不折它)。
|
|
132
|
-
- 一句一条,**不带链接**(链接去概念页自己的「## 关联连接」挂),不展开。
|
|
133
|
-
- 只有「违反会丢数据 / 静默失效 / 白干活」级才进 —— 普通经验进 `concepts/`。
|
|
134
|
-
- 读写:`abs rule` / `abs rule add "一句话"`(>42 字符被拒;也不接受 `[[链接]]`/URL);`abs lint` 超 30 条会报。
|
|
135
|
-
|
|
136
|
-
### `log.md` —— 工作成果流水
|
|
137
|
-
|
|
138
|
-
倒序一行摘要(`abs log "完成 X:…"`)。**只记成果,不收工具动作流水**(那在 `~/.abs/log/`)。
|
|
139
|
-
load 只展示最新 5 条、每条按语义边界收口。
|
|
140
|
-
|
|
141
|
-
### `todo.md` —— 活看板(进度唯一真源)
|
|
142
|
-
|
|
143
|
-
**只有两区**(2026-09-13 精简):未完成的一切都进 `## Todo`,状态用**行首标记**表达。
|
|
144
|
-
|
|
145
|
-
| 分区 | 放什么 |
|
|
146
|
-
|---|---|
|
|
147
|
-
| `## Todo` | 未完成的一切。行首 `[进行中]` / `[讨论中]` / `[滞留中]`(`abs todo state`) |
|
|
148
|
-
| `## Done` | 已完成,**必须带结语 `【落地/否决/仅方案】` + `(完成 YYYY-MM-DD)`** |
|
|
149
|
-
|
|
150
|
-
行形态:`- [ ] [进行中] <id> [[name]] — 说明 (认领 YYYY-MM-DD)`
|
|
54
|
+
**例外(可直接开工,回复里须说明援引哪一条)**:用户已说"开工/直接做/修它";一行级修字、纯查询、纯收尾沉淀;同一需求已登记且用户确认过。
|
|
151
55
|
|
|
152
|
-
|
|
153
|
-
Done 区由 `abs wrapup` 在会话结束时自动把「超 3 天且整天都已完成」的组迁进
|
|
154
|
-
**当天会话快照的 `## 📦 任务归档` 段**(`sessions/log-<日期>.md`)—— 归档不另建文件
|
|
155
|
-
(任一天有未完成则整天不迁)。**什么时候动它见「进行中」一节。**
|
|
56
|
+
`.brain/` 放项目根,一个项目一份;所有命令只认当前目录的 `.brain/`,不向上搜索。`abs status` 显示当前定位。
|
|
156
57
|
|
|
157
|
-
|
|
58
|
+
## `.brain/` 怎么组织
|
|
158
59
|
|
|
159
|
-
|
|
160
|
-
|---|---|---|
|
|
161
|
-
| `entities/` | 具名的**事物**(能说"它是什么") | `Docker.md` |
|
|
162
|
-
| `concepts/` | 可复用的**规律/坑**(能说"这么做就避坑") | `docker-prisma-429.md` |
|
|
163
|
-
| `sources/` | 实时经验**暂存**(`abs note` 自动落) | `YYYY-MM-DD-slug.md` |
|
|
164
|
-
| `syntheses/` | **横向**选型/架构取舍(跨多个 entity/concept 的判断) | `synthesis-slug.md` |
|
|
165
|
-
| `sessions/` | 会话快照(含 `## 🪝 Next Session Hook`)+ 当日任务归档段 | **`log-YYYY-MM-DD.md`** |
|
|
166
|
-
|
|
167
|
-
归类拿不准时**默认 `concepts/`**。
|
|
168
|
-
|
|
169
|
-
### `sessions/` 命名契约:一天一个文件(硬规则)
|
|
60
|
+
`.brain/` 文件结构(`abs init` 生成,`--repair` 只补缺不覆盖):
|
|
170
61
|
|
|
171
|
-
|
|
62
|
+
文件结构(全部在 `.brain/` 下):
|
|
172
63
|
|
|
173
64
|
```
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
├─ ## 🪝 Next Session Hook(强制)
|
|
178
|
-
└─ ## 📦 任务归档(`abs todo archive` 自动写,机器生成勿手改)
|
|
65
|
+
/ .brain/
|
|
66
|
+
index.md log.md todo.md
|
|
67
|
+
entities/ concepts/ sources/ syntheses/ sessions/
|
|
179
68
|
```
|
|
180
69
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
abs
|
|
190
|
-
|
|
70
|
+
| 位置 | 放什么 | 硬约束 |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `index.md` `## Rules` | 本项目铁律 | 一句一条,不带链接;只有「违反会丢数据/静默失效/白干活」级才进(普通经验进 `concepts/`);>42 字符或含链接/URL 被拒 |
|
|
73
|
+
| `index.md` 清单区 | 各类页清单 | 每页一行 `- [[slug]] — 一句话` |
|
|
74
|
+
| `log.md` | 成果流水,倒序一行 | 只记成果,不收工具动作流水(那在 `~/.abs/log/`) |
|
|
75
|
+
| `todo.md` | 活看板,只有 `## Todo` + `## Done` | 未完成用行首标记 `[进行中]`/`[讨论中]`/`[滞留中]`;Done 必带 `【落地/否决/仅方案】` + `(完成 YYYY-MM-DD)` |
|
|
76
|
+
| `entities/` | 具名事物("它是什么") | `Docker.md` |
|
|
77
|
+
| `concepts/` | 可复用规律/坑("这么做就避坑") | `docker-prisma-429.md`;归类拿不准时默认这里 |
|
|
78
|
+
| `sources/` | 经验暂存(`abs note` 自动落) | `YYYY-MM-DD-slug.md`;是暂存,不是归档 |
|
|
79
|
+
| `syntheses/` | 横向选型/架构取舍 | `synthesis-slug.md` |
|
|
80
|
+
| `sessions/` | 会话快照 + 当日归档段 | **一天只允许一个文件,只能叫 `log-<日期>.md`** |
|
|
191
81
|
|
|
192
|
-
|
|
193
|
-
- 该日**已有快照** → 只替换归档段,**段外的手写内容逐字保留**(不覆盖人工内容)
|
|
194
|
-
- 该日**没有快照** → 建一个只有归档段的 `log-<日期>.md`(那天本来就只有任务记录)
|
|
195
|
-
- 同日二次归档 → 段内追加,不重复建段
|
|
82
|
+
`todo.md` 行形态:`- [ ] [进行中] <id> [[认领人]] — 说明 (认领 YYYY-MM-DD)`(`[[认领人]]` = 使用者名,非任务名)。`abs todo done <id>` 勾选并归位到 Done 日期组顶部,断点(`↳` 行)随迁;超 3 天且整天完成的组由 `abs wrapup` 迁进当天快照的归档段。
|
|
196
83
|
|
|
197
|
-
|
|
198
|
-
(手写时才可能犯错)。
|
|
84
|
+
**`sessions/` 一天一个文件**(多主题多写几个 `##` 段,不许拆文件),含快照正文(AI 手写)+ `## 关联连接` + `## 🪝 Next Session Hook`(强制)+ `## 📦 任务归档`(机器写,勿手改)。归档唯一动作 `abs todo archive`:已有快照只替换归档段(段外内容逐字保留),没快照则建一个只有归档段的文件,同日二次归档段内追加。
|
|
199
85
|
|
|
200
|
-
|
|
201
|
-
1. **不要**再把归档单独建文件(`<日期>-todo归档.md`)——归档已有专门的坑位段,`abs todo archive` 会写进去。
|
|
202
|
-
存量旧页不必手改(lint 不报),但新归档不再产生它。
|
|
203
|
-
2. **不要**在 `sessions/` 放 `tags: [source]` 的页——暂存页属 `sources/`。
|
|
204
|
-
当天做的一组工作不是「暂存线索」,而是**当天的快照正文**:直接写进 `log-<日期>.md`。
|
|
205
|
-
3. **不要**一天拆多个快照(`log-<日期>-<主题>.md`)——多主题就多写几个 `##` 段。
|
|
206
|
-
4. **不要**手工搬大文件进来当归档(如把仓库根 `todo.md` 全文倒进 `sessions/`)——
|
|
207
|
-
那要么进 `sources/`,要么摘出规律进 `concepts/`;全文属外部产物,用指针就行。
|
|
86
|
+
`abs lint` 兜底:死链 / 孤岛 / 悬挂页 / 缺 frontmatter / 超限 / sources 堆积 / 超龄未提炼 / index 漏列 / Rules 超限 / 缺尾。
|
|
208
87
|
|
|
209
88
|
### 容量纪律(写任何页之前过四关)
|
|
210
89
|
|
|
211
|
-
|
|
212
|
-
1. **再命中**:下会话不知道这条会踩同坑/重做同决定?会→存,不会→不存。代码能 grep 到的一律不记。
|
|
213
|
-
2. **单页上限**:`entities/concepts/syntheses` 单页 <150 行且 <8KB,超了拆或外链。
|
|
214
|
-
3. **sources 是暂存**:提炼成规律后删/归档,并清掉指向它的引用(防死链)。
|
|
215
|
-
4. **能不能用一行链接代替新增整页**?
|
|
90
|
+
不过关就不写或压缩:① **再命中** —— 下会话不知道这条会踩同坑/重做同决定?不会就不存,代码能 grep 到的一律不记。② **单页上限** —— `entities/concepts/syntheses` 单页 <150 行且 <8KB,超了拆或外链。③ **sources 是暂存** —— 提炼成规律后删/归档,并清引用(防死链)。④ 能不能用一行链接代替新增整页?
|
|
216
91
|
|
|
217
|
-
`abs lint` 兜底:死链/孤岛/**悬挂页(NO-INBOUND)**/缺 frontmatter/超限/sources 堆积/**超龄未提炼(SOURCE-UNDISTILLED)**/index 漏列/Rules 超限/**缺尾(NO-TAIL,concept 页没有「做完怎么确认」)**。
|
|
218
92
|
## 开场:Init Sync(开工 / 默认续 todo)
|
|
219
93
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
1. **读状态**:`abs load`(或 MCP `abs_load`)读 index 的 Rules + 图谱计数 + todo 看板 + 最近 log。
|
|
223
|
-
> **开工前先看 `## Rules`** —— 那是本项目踩过坑后定下的硬规则,每条都是曾经付过代价的。
|
|
224
|
-
> 违反的代价一般是丢数据/静默失效/白干活,而它就在 load 输出里,没有理由不看。
|
|
225
|
-
>
|
|
226
|
-
> **load 输出是折叠过的,不是全量。** 两个无上限增长的区块在读取侧收口:
|
|
227
|
-
> - **Done 区** → 按日期计数(曾占 load 输出 68.8%,长历史项目上单次 load 吃掉 40% 上下文)
|
|
228
|
-
> - **index 页面清单** → 各分区只给页数(concept 清单占 load 输出 64%,隨图谱线性增长)
|
|
229
|
-
> - 「最近动作」每条按语义边界收口到 220 字符
|
|
230
|
-
>
|
|
231
|
-
> **`## Rules` 区原样保留** —— 那是 load 要传达的状态本身(代码里明确不折)。
|
|
232
|
-
> 要全量明细:`abs todo --full` / `abs index`,或直接读 `.brain/` 文件、
|
|
233
|
-
> `.brain/sessions/log-<日期>.md`(含 `## 📦 任务归档` 段)。
|
|
234
|
-
2. **对账滞留(强制,别跳过)**:若 `abs load` 顶部出现 `⏳ 上会话滞留`,说明上会话有任务做完/做到一半就断了。**先收尾再开工**:
|
|
235
|
-
- 快照里的任务现在真做完了 → `abs todo done <id>`(done 后下次 load 滞留自动消失);
|
|
236
|
-
- 还没做完 → `abs todo note <id> --note "接到哪/改到哪个文件"` 补断点(别空手续接)。
|
|
237
|
-
滞留没清完就不算接上了状态——这是「任务做完没进 Done」的根治动作。
|
|
238
|
-
3. **读命中页**:`abs query <词>` 检索(多词 OR)→ 拿到页名后读那几个文件。
|
|
239
|
-
```bash
|
|
240
|
-
abs query <词> # 全文检索,只回命中几页
|
|
241
|
-
abs resolve <页名或id> # 反查文件路径(页改过名时用 id 能找回)
|
|
242
|
-
```
|
|
243
|
-
> ⚠️ **绝不通读 `.brain/`**(29 页就约 11 万 token,全读塞满窗口)。
|
|
244
|
-
> 要状态→`abs load`;要主题→`abs query <词>`;要完整页清单→`abs index`(或直接读 `.brain/index.md`);要某页→`abs resolve` 拿到路径后**只读那一页**。
|
|
245
|
-
> 汇总多文件时用 `ctx_execute` 类工具在沙箱里处理,**只打印结论**。
|
|
246
|
-
4. **续 todo**:默认续 todo 分支 → 把顶部未完成项当当前任务开做。
|
|
247
|
-
5. **登记新任务**:有明确新任务而 todo 没有 → `abs todo add <id> --note 做什么` 再动工。
|
|
248
|
-
|
|
249
|
-
## 进行中:什么时候动它(最重要的节)
|
|
250
|
-
|
|
251
|
-
**todo 不是收尾仪式,是随改随写的活看板。** 每个任务边界立即更新,与 git commit 同反射。
|
|
252
|
-
|
|
253
|
-
**看板只有两区**:`## Todo`(未完成的一切)+ `## Done`(已完成)。
|
|
254
|
-
未完成的状态用**行首标记**表达:`[进行中]` `[讨论中]` `[滞留中]`。
|
|
255
|
-
|
|
256
|
-
| 时机 | 动作 | 落到哪 |
|
|
257
|
-
|---|---|---|
|
|
258
|
-
| 认领新任务 / 聊出一个话题 | `abs_task {action:start, id:"T-1", note:"做什么"}` | Todo `[进行中]` |
|
|
259
|
-
| 只在讨论、还没动手 | `abs_task {action:state, id:"T-1", note:"讨论中"}` | 原地改标记 |
|
|
260
|
-
| 卡住了/等人等数据 | `abs_task {action:state, id:"T-1", note:"滞留中"}` | 原地改标记 |
|
|
261
|
-
| 被打断/干到一半 | `abs_task {action:note, id:"T-1", note:"改到哪个文件/到哪步"}` | 原地 ↳断点 |
|
|
262
|
-
| 子任务做完 | `abs_task {action:done, id:"T-1", note:"结语文字", as:"落地|否决|仅方案"}` | Done(as 默认 落地;做了又撤用否决,只设计过用仅方案,别让假【落地】污染看板) |
|
|
263
|
-
| 总结出经验/坑/规律 | `abs_note {text:"一句话", tags:"坑,docker"}` | sources/ |
|
|
264
|
-
|
|
265
|
-
> **为什么只有两区**(2026-09-13 实测):跨 4 个项目,Backlog/Today 常年 **0 条**,而 log.md
|
|
266
|
-
> 有 120 条。根因是 AI 的工作方式「一口气做完」——任务从开始到完成都在同一会话内走完,
|
|
267
|
-
> 中间那个「挂到进行时分区」的动作既来不及也不需要。**进行时分区是符合直觉但不符合实际
|
|
268
|
-
> 工作流的抽象**,故删掉,状态改用行首标记。
|
|
269
|
-
|
|
270
|
-
**核心:事件发生的那一刻就落,别攒到收尾。** 动作一变,扫一眼属于哪行,调对应工具。
|
|
94
|
+
收到第一个核心开发指令**之前**走这条链载入上下文:
|
|
271
95
|
|
|
272
|
-
|
|
96
|
+
1. **读状态**:`abs load`(MCP `abs_load`)读 Rules + 图谱计数 + todo + 最近 log。**开工前先看 `## Rules`**。load 输出是折叠的(Done 按日期计数、清单只给页数);全量明细用 `abs todo --full` / `abs index`。
|
|
97
|
+
2. **对账滞留(强制,别跳过)**:若顶部出现 `⏳ 上会话滞留`,说明上会话任务断了,先收尾再开工 —— 真做完的 `abs todo done <id>`;没做完的 `abs todo note <id> --note "接到哪/改到哪个文件"` 补断点。
|
|
98
|
+
3. **读命中页**:`abs query <词>` → `abs resolve <页名或id>` 拿路径后**只读命中那几页**。**绝不通读 `.brain/`**;汇总多文件用 `ctx_execute` 类工具在沙箱里处理,只打印结论。
|
|
99
|
+
4. **续 todo**:把顶部未完成项当当前任务开做;有明确新任务而 todo 没有 → `abs todo add <id> --note 做什么` 再动工。
|
|
273
100
|
|
|
274
|
-
|
|
275
|
-
→ 立刻去读代码/改文件(因为觉得「还没开始做,没什么可登的」)→ 一口气做完 → 只在收尾
|
|
276
|
-
补一条 done。结果:**看板在被看的时候(会话下半场、别的会话接手)永远是空的**,
|
|
277
|
-
跨会话续接丢锚点。用户原话:「todo 登记不及时,总是空的」。
|
|
101
|
+
## 进行中:什么时候动它
|
|
278
102
|
|
|
279
|
-
|
|
103
|
+
**todo 是随改随写的活看板,同 git commit 同反射。**
|
|
280
104
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
105
|
+
| 时机 | `abs_task` action | 落到哪 |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| 认领新任务 / 聊出一个话题 | `start` + note 做什么 | Todo `[进行中]` |
|
|
108
|
+
| 只在讨论、还没动手 | `state` + note 讨论中 | 原地改标记 |
|
|
109
|
+
| 卡住了/等人等数据 | `state` + note 滞留中 | 原地改标记 |
|
|
110
|
+
| 被打断/干到一半 | `note` + 改到哪个文件/到哪步 | 原地 ↳断点 |
|
|
111
|
+
| 子任务做完 | `done` + note 结语 + as 落地\|否决\|仅方案 | Done(别让假【落地】污染看板) |
|
|
112
|
+
| 总结出经验/坑/规律 | 改用 `abs_note` text + tags | sources/ |
|
|
287
113
|
|
|
288
|
-
|
|
289
|
-
> `.brain/` 今日**零 todo 记录**,直到收尾注入才补。用户看板全程是空的。
|
|
290
|
-
> 代价不是「记录少了」,而是**用户中途想看进度时看不到** —— 活看板的价值就在中途。
|
|
114
|
+
**硬规则(开工前触发器,别被状态表漏掉):**
|
|
291
115
|
|
|
292
|
-
|
|
116
|
+
1. **开工前先登记** —— 认领任何要动文件/跑命令的任务,**第一步是 `abs_task` action=start**,然后才读第一个文件。名字想不到就先起粗糙的(`fix-hook-nudge`)—— 名字可事后改,**未登记的开工补不回来**。
|
|
117
|
+
2. **一段活儿干完立刻 done** —— 不是等整个需求收尾。宁可拆成 5 条小的,别攒成 1 条大的。
|
|
118
|
+
3. **动手超过两三轮还没登记 = 已在失控路上** —— 立刻补 `start`。
|
|
293
119
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
>
|
|
297
|
-
> **跨会话任务只用 abs todo,别用宿主原生 todo**(Claude TodoWrite / codex todo-list /
|
|
298
|
-
> OpenCode todowrite / pi `/list`)—— 那些多是会话内临时,不写 `.brain/todo.md`,
|
|
299
|
-
> 下会话接不上、收尾没影。原生 todo 顶多记“本会话不跨断点的临时拆解”。
|
|
120
|
+
**经验刚冒出来就落**:`abs_note` 暂存 `sources/`(幂等去重),宁少勿滥。
|
|
121
|
+
**跨会话任务只用 abs todo,别用宿主原生 todo**(TodoWrite / todo-list / `/list`)—— 那些是会话内临时,不写 `.brain/todo.md`,下会话接不上。
|
|
300
122
|
|
|
301
123
|
## 收尾循环(用户明确要求收尾时才走)
|
|
302
124
|
|
|
303
|
-
|
|
125
|
+
用户**明确说要收尾/结束/切别的事**时按下面走(不是每个词都触发,见开头总则):
|
|
304
126
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
> 挂钩只在 `~/.abs/log/hooks.log` 留痕,不再往对话里说话。
|
|
127
|
+
1. **读 todo** → `abs load`,看 Todo 还有哪些没完成。
|
|
128
|
+
2. **对账** → 漏登记的 `abs todo done <id>`;做一半补 `abs todo note <id> --note 断点`;卡住的 `abs todo state <id> --note 滞留中`。别让干完的事还留在 Todo。
|
|
129
|
+
3. **沉淀经验(该沉淀才沉淀)** → 值得记的坑/可复用技巧 → `abs note` 暂存;值得深提炼的按 Teardown 走。
|
|
130
|
+
4. **判教训够不够格进 Rules(别跳过)** → 是否「违反会丢数据/静默失效/白干活」级?
|
|
131
|
+
- **够格 → 提议,不直写**:输出一行 `[Rules 提议] <一句话>` 问用户要不要加,确认后才 `abs rule add "<一句话>"`(短句,不带链接/解释)。
|
|
132
|
+
- 不够格 → 不提,普通经验留在概念页。
|
|
133
|
+
**为什么单列**:Rules 是唯一每次 load 全量送达的通道,概念页只在关键词命中时出现;够格不加 = 下次不送达。
|
|
134
|
+
5. **更新 index/log/todo** → 新页同步进 index;`abs log "完成 X:..."` 记一行成果;跑 `abs lint` 确认自洽。
|
|
314
135
|
|
|
315
|
-
|
|
136
|
+
**完成标准**:看板反映真实状态(Done 无滞留半成品)、该沉淀已落、够格的教训已提议进 Rules、index/log/todo 与事实一致。
|
|
316
137
|
|
|
317
|
-
|
|
318
|
-
2. **判有没有做完没登记** → 实际完成了漏登记的 `abs todo done <id>`;做到一半补
|
|
319
|
-
`abs todo note <id> --note 断点`;卡住的 `abs todo state <id> --note 滞留中`。别让干完的事还留在 Todo。
|
|
320
|
-
3. **沉淀经验(该沉淀才沉淀)** → 踩了值得记的坑/有可复用技巧/跨会话判断 → `abs note`
|
|
321
|
-
暂存;值得深提炼的(规律/坑/决策)按 Teardown 走完整流程。
|
|
322
|
-
4. **判本次教训够不够格进 Rules(别跳过)** → 过一遍:这条是否**「违反会丢数据/静默失效/白干活」**级?
|
|
323
|
-
- **够格 → 提议,不直写**:输出一行 `[Rules 提议] <一句话>` 并问用户要不要加,得到确认才
|
|
324
|
-
`abs rule add "<一句话>"`(短句,不带链接/解释;链接去概念页挂)。
|
|
325
|
-
- 不够格 → 不提。普通经验留在概念页,别进 Rules(否则长成第二份概念库)。
|
|
326
|
-
> **为什么单列一步**:Rules 是全系统**唯一每次 load 全量送达**的通道(代码里明确不折),
|
|
327
|
-
> 而概念页只在关键词命中时出现。**够格却不加 = 这条教训下次不会送达。**
|
|
328
|
-
> 实测坑(2026-09-15):原先把这动作塞在第 4 步里当附属从句,结果 14 条 Rule 中 13 条
|
|
329
|
-
> 来自两次人工注入,机制本身长期 0 新增 —— 同类坑反复发作(静默失效 5 次)。
|
|
330
|
-
> 详见 concepts/learning-loop-collect-distill-deliver.md。
|
|
331
|
-
> **别改成 lint 报警**:那是「靠提醒才能工作的功能」,已被本仓硬规则否定。
|
|
332
|
-
5. **更新 index/log/todo** → 新页同步进 index;`log.md` 倒序记一行**工作成果**摘要
|
|
333
|
-
(`abs log "完成 X:..."`,不是工具动作);todo 对账。
|
|
334
|
-
跑 `abs lint` 确认自洽。
|
|
335
|
-
|
|
336
|
-
**完成标准**:看板反映真实状态(Done 无滞留半成品)、该沉淀已落、**够格的教训已提议进 Rules**、index/log/todo 与事实一致。
|
|
138
|
+
Stop 时 hook 把「未完成任务 + 断点」快照进 `~/.abs/log/wrapup.log`,下会话 `abs load` 自动把滞留顶到顶部。**不主动往对话里插收尾提醒** —— 插一个 turn 就是打断(挂钩只在 `~/.abs/log/hooks.log` 留痕)。
|
|
337
139
|
|
|
338
140
|
## 收尾:Teardown Sync(深提炼,工具不替判断)
|
|
339
141
|
|
|
340
142
|
任务告一段落/结束前,把**真实发生**写回图谱。只写做过/跑过/测过的事实,禁止脑补。按序:
|
|
341
143
|
|
|
342
|
-
1. **暂存线索**:`abs note
|
|
343
|
-
2. **抽规律**:值得留的 →
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
(`abs note` 落的页默认是 `draft`)。
|
|
350
|
-
3. **沉淀实体**:碰了重要未记录的事物 → `entities/<TitleCase>.md`。
|
|
351
|
-
4. **对账 todo**:做完的归位 Done(标结语+日期),做一半的补断点,卡住的改 `[滞留中]`。
|
|
352
|
-
5. **综合(可选)**:推进了选型/取舍 → `syntheses/`。
|
|
353
|
-
6. **收拢 sources**:提炼成规律的删 source,**同步清指向它的引用**(防死链)。
|
|
354
|
-
7. **修 index + 记 log**:新页同步 index;**过 Rules 门槛的规律加一行到 `## Rules`**;
|
|
355
|
-
`log.md` 倒序记一行摘要。
|
|
356
|
-
8. **留接力棒**:`sessions/log-YYYY-MM-DD.md`,强制写 `## 🪝 Next Session Hook`。
|
|
357
|
-
|
|
358
|
-
**完成标准**:每条过了容量纪律的知识一处落点;index 与事实一致;sessions 有带 Hook 快照。
|
|
144
|
+
1. **暂存线索**:`abs note` 记做了什么、改哪些文件、验证命令。
|
|
145
|
+
2. **抽规律**:值得留的 → `abs concept <slug> --title "…"` 建页(自动带四段骨架)再填内容。四段别缺,尤其末尾「验证」段(`abs lint` 报 NO-TAIL)。**判断仍归你**:值不值得留、新建还是并入已有页;核实过的把 `status` 改 `active`。
|
|
146
|
+
3. **查推翻**:旧页有被本次推翻的说法 → `abs supersede <旧页> --by <新页>`(别删页)。
|
|
147
|
+
4. **沉淀实体 / 综合**:重要未记录的事物 → `entities/<TitleCase>.md`;选型/取舍 → `syntheses/`。
|
|
148
|
+
5. **收拢 sources**:提炼成规律的删 source,**同步清指向它的引用**(防死链)。
|
|
149
|
+
6. **修 index + 记 log**:新页同步 index;过 Rules 门槛的规律加一行到 `## Rules`。
|
|
150
|
+
7. **留接力棒**:`sessions/log-YYYY-MM-DD.md` 强制写 `## 🪝 Next Session Hook`。
|
|
359
151
|
|
|
360
152
|
## 知识页格式
|
|
361
153
|
|
|
362
|
-
统一 frontmatter:`tags / author / updated / status
|
|
363
|
-
`tags` 首标签 ∈ `entity|concept|source|synthesis|session-log`。`author` 与 `entities/<name>.md` 同名。
|
|
154
|
+
统一 frontmatter:`tags / author / updated / status`。`tags` 首标签 ∈ `entity|concept|source|synthesis|session-log`;`author` 与 `entities/<name>.md` 同名。
|
|
364
155
|
|
|
365
|
-
**`status`
|
|
366
|
-
|
|
367
|
-
| 值 | 含义 | 读取侧行为 |
|
|
368
|
-
|---|---|---|
|
|
369
|
-
| `active` | 当前有效(**缺字段默认就是它**,存量页无需改) | 正常展示 |
|
|
370
|
-
| `draft` | 待核实(`abs note` 新落的经验默认这个) | 展示但标注 `[draft 未核实]` |
|
|
371
|
-
| `superseded` | **已被推翻,别再依据它** | `query` 默认隐藏(计数据告知) |
|
|
156
|
+
**`status` 三个值(别写别的)**:`active` 当前有效(缺字段默认就是它,正常展示);`draft` 待核实(`abs note` 默认落这个,展示但标 `[draft 未核实]`);`superseded` 已被推翻,别再依据(`query` 默认隐藏)。
|
|
372
157
|
|
|
373
|
-
|
|
374
|
-
```bash
|
|
375
|
-
abs supersede <页名> --by <取代它的新页> # 不写 --by 也行 = 单纯弃用,无替代
|
|
376
|
-
```
|
|
377
|
-
→ 把 `status` 改成 `superseded` 并写 `superseded-by`。**历史必须留**:删了会让下个会话重踩同一个坑、重新记一遍。
|
|
378
|
-
→ 核实后发现仍有效:把 `status` 改回 `active`(一行,可反悔)。
|
|
379
|
-
→ 拒写悬空引用:`--by` 指向不存在的页会直接拒绝。
|
|
158
|
+
推翻一条经验用 `abs supersede <页名> --by <新页>`(改 `status` 为 `superseded` 并写 `superseded-by`;不写 `--by` = 单纯弃用)。历史必须留;核实后仍有效就把 `status` 改回 `active`。`--by` 指向不存在的页会被拒。
|
|
380
159
|
|
|
381
|
-
- **每页必须有 `## 关联连接`**,用 `[[页面名]]`
|
|
382
|
-
**链路解释写在 `—` 后面**(如 `[[hook-throttle-alignment]] — 节流判据要对齐「真收尾」`),
|
|
383
|
-
这样 AI 不点开就知道该不该跟进;只写链点不写解释,等于没链。
|
|
160
|
+
- **每页必须有 `## 关联连接`**,用 `[[页面名]]` 链相关页,严禁孤岛页。链路解释写在 `—` 后面(`[[hook-throttle-alignment]] — 节流判据要对齐「真收尾」`),只写链点等于没链。
|
|
384
161
|
- **命名即链接**:`[[Docker]]` → `entities/Docker.md`;`[[docker-prisma-429]]` → `concepts/`。不建别名层。
|
|
385
162
|
- **知识冲突**:不静默覆盖,加 `## 知识冲突` 两版都留、标来源时间,交人工裁决。
|
|
386
163
|
- 概念页骨架:`触发场景 / ❌表现(贴报错) / 🛠解法(根因+修复+验证命令) / 关联连接`。
|
|
387
164
|
|
|
388
165
|
## 维护:query / lint
|
|
389
166
|
|
|
390
|
-
- `abs query <词>`
|
|
391
|
-
|
|
392
|
-
已被推翻的经验默认不返回(`--all` 可看)—— 但若你确实需要拿旧结论对照,记得它已被推翻。
|
|
393
|
-
- `abs lint` 体检:死链/孤岛/**悬挂页**/缺 frontmatter/模板残留/超尺寸/sources 堆积/**超龄未提炼**/index 漏列/Rules 超限/**取代者悬空**/**draft 超龄**。
|
|
394
|
-
- `NO-INBOUND`:有出边但无人 `[[链接]]` 到你 = 挂在图上没人接(孤岛检查只抓"零出零入")。
|
|
395
|
-
- `SOURCE-UNDISTILLED`:source 超 7 天仍未链到任何 concept = 暂存了没归位。
|
|
396
|
-
- `SUPERSEDED-DANGLING`:`superseded-by` 指向的页不存在(删页后忘同步)。
|
|
397
|
-
- `DRAFT-STALE`:concepts 等长期停在 `draft` = 既没核实也没推翻,核实/推翻后各改一行。
|
|
167
|
+
- `abs query <词>` → 读命中页 → 答用 `[[页面名]]` 标来源。**代码问题(符号在哪/谁调用)不在 .brain,直接读源码**;已推翻经验默认不返回(`--all` 可看)。
|
|
168
|
+
- `abs lint` 报错码:`NO-INBOUND` 有出边无人链接 / `SOURCE-UNDISTILLED` source 超 7 天未链 concept / `SUPERSEDED-DANGLING` superseded-by 页不存在 / `DRAFT-STALE` 停在 draft / `NO-TAIL` 缺「做完怎么确认」段。
|
|
398
169
|
|
|
399
|
-
##
|
|
170
|
+
## ⛔ 禁止做
|
|
400
171
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
172
|
+
1. **禁止手工建骨架 / 手工登记任务** —— 用 `abs init` / `abs todo add`。
|
|
173
|
+
2. **禁止由关键词直接触发动作**("简短""收尾"等词出现在定规则的句子里时只回应,不执行)—— 拿不准先问。
|
|
174
|
+
3. **禁止通读 `.brain/`** —— 要状态用 `abs load`,要主题用 `abs query`,要单页用 `abs resolve` 后只读那页。
|
|
175
|
+
4. **禁止删知识页来表达"已失效"** —— 用 `abs supersede <旧页> --by <新页>`。
|
|
176
|
+
5. **禁止归档单独建文件 / 一天拆多个快照 / 手工搬大文件** —— 用 `abs todo archive`;一天只有一个 `log-<日期>.md`;大文件进 `sources/` 或摘出规律进 `concepts/`。
|
|
177
|
+
6. **禁止编造没跑过的验证结论** —— 只写做过/跑过/测过的事实;不清楚就写"未定位"。
|
|
178
|
+
7. **禁止直写 Rules** —— 够格的先输出 `[Rules 提议]` 问用户,确认后才 `abs rule add`。
|
|
179
|
+
8. **禁止用宿主原生 todo 承载跨会话任务** —— 用 `abs_task`,原生 todo 只记本会话临时拆解。
|
|
408
180
|
|
|
409
181
|
## 自我约束
|
|
410
182
|
|
|
411
|
-
- 只读写 `.brain/`
|
|
412
|
-
- 只写真实发生的事实;遵守容量纪律,宁缺毋滥。
|
|
183
|
+
- 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外)。只写真实发生的事实,遵守容量纪律。
|
|
413
184
|
- 双链/frontmatter/index 必须自洽 —— 坏链 = 掰断接力棒。
|
|
185
|
+
|
package/src/store.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import { join, resolve, dirname } from 'node:path';
|
|
5
5
|
import { requireBrain, brainPath, absLogDir, BRAIN_DIR } from './index.js';
|
|
6
|
-
import { requireUser, atTag, getUser } from './userconfig.js';
|
|
6
|
+
import { requireUser, atTag, getUser, placeholderWarn } from './userconfig.js';
|
|
7
7
|
import { stripStateMark, ensureStateMark, normalizeTodo, addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText, upsertArchiveSection, DONE_KINDS, withDoneKind, doneKindOf, doneDateOf, collapseDone, SEC, rebuildStructure } from './todo.js';
|
|
8
8
|
import { editFile, SKIP } from './lock.js';
|
|
9
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
@@ -300,14 +300,24 @@ export async function cmdLoad({ dir }) {
|
|
|
300
300
|
if (shape.warn.length) rows.push(' (无头文件不自动改:结构可能整体脱轨,请手工对齐 .brain/ 模板)');
|
|
301
301
|
sections.push('--- 文件形状核对 ---', ...rows, '');
|
|
302
302
|
}
|
|
303
|
-
//
|
|
303
|
+
// 未设姓名/姓名是占位名时开场就提醒 —— load 是开机第一屏,不在这里提,
|
|
304
304
|
// 用户要撞到第一次写操作才知道(init/load 一路沉默)。
|
|
305
|
-
|
|
305
|
+
// 占位名(历史遗留 tester/foo)必须提:配置里有值 ≠ 名字是对的。
|
|
306
|
+
// 一次调用拿两者(placeholderWarn 内部也要 getUser)—— 原先分两次读盘(2026-10-03 审查)。
|
|
307
|
+
const who = await getUser();
|
|
308
|
+
const ph = who ? await placeholderWarn() : null;
|
|
309
|
+
if (!who) {
|
|
306
310
|
sections.push(
|
|
307
311
|
'⚠ 尚未设置使用者姓名(写操作会先报错)',
|
|
308
312
|
'→ abs config set user <你的名字> (或临时: ABS_USER=<名字> abs ...)',
|
|
309
313
|
''
|
|
310
314
|
);
|
|
315
|
+
} else if (ph) {
|
|
316
|
+
sections.push(
|
|
317
|
+
`⚠ 当前作者名是占位名 "${ph}" —— 之后所有项目的 todo/log 都会标它。`,
|
|
318
|
+
'→ abs config set user <你的真名> (或临时: ABS_USER=<名字> abs ...)',
|
|
319
|
+
''
|
|
320
|
+
);
|
|
311
321
|
}
|
|
312
322
|
sections.push(
|
|
313
323
|
// Rules 单独成段且放在最前(仅次于项目行/滞留):它是硬规则,不是普通清单。
|
package/src/userconfig.js
CHANGED
|
@@ -30,6 +30,34 @@ export async function getUser() {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** 占位名黑名单:这些名字没有任何正当理由当作者名,写进全局配置会污染之后所有项目。
|
|
34
|
+
* 教训(2026-09-18):一次手动 `abs config set user tester` 让新项目每条 todo 都标 [[tester]]。
|
|
35
|
+
* 注意只拦 setUser(持久配置),**不拦 ABS_USER 环境变量** —— env 是一次性显式覆盖,
|
|
36
|
+
* 且测试套件全程用 ABS_USER=tester(7 个测试文件),拦它会全线爆掉。 */
|
|
37
|
+
const PLACEHOLDER_NAMES = new Set([
|
|
38
|
+
'tester', 'test', 'testing', 'foo', 'bar', 'baz', 'foobar',
|
|
39
|
+
'admin', 'user', 'username', 'me', 'you', 'someone', 'nobody',
|
|
40
|
+
'example', 'demo', 'sample', 'tmp', 'temp', 'default', 'null', 'none',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/** 校验是否是像样的人名;不合格抛带指引的错误。setUser 专用(ABS_USER 不过此关)。 */
|
|
44
|
+
function assertRealName(name) {
|
|
45
|
+
const lower = name.toLowerCase();
|
|
46
|
+
if (PLACEHOLDER_NAMES.has(lower)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`✗ "${name}" 是占位名,不是真人姓名 —— 它会成为所有项目的作者标记。\n` +
|
|
49
|
+
' 请填你本人的名字(如 abs config set user 张三 / alice)'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
// 无意义重复串:aaa/xxx/111 之类。
|
|
53
|
+
// 长度门槛 ≥3 —— `oo`/`ee`/`ww` 这种两字母是合法的姓名缩写(2026-10-03 审查发现
|
|
54
|
+
// 原规则会误伤它们),也避免中文叠字小名(如 `中中`)被无故拒绝。
|
|
55
|
+
if (lower.length >= 3 && /^(.)\1+$/.test(lower)) {
|
|
56
|
+
throw new Error(`✗ "${name}" 看起来不是名字(重复字符)—— 请填你本人的名字`);
|
|
57
|
+
}
|
|
58
|
+
return name;
|
|
59
|
+
}
|
|
60
|
+
|
|
33
61
|
/** 写配置的 user 字段(保留其它键)。 */
|
|
34
62
|
export async function setUser(name) {
|
|
35
63
|
const clean = String(name || '').trim();
|
|
@@ -38,6 +66,7 @@ export async function setUser(name) {
|
|
|
38
66
|
if (!/^[\w\u4e00-\u9fff.-]+$/.test(clean)) {
|
|
39
67
|
throw new Error(`✗ 姓名 "${clean}" 含不支持的字符(只允许字母/数字/中文/._-,且不含空格)`);
|
|
40
68
|
}
|
|
69
|
+
assertRealName(clean);
|
|
41
70
|
const p = userConfigPath();
|
|
42
71
|
await fs.mkdir(join(p, '..'), { recursive: true });
|
|
43
72
|
let cfg = {};
|
|
@@ -66,6 +95,17 @@ export async function requireUser() {
|
|
|
66
95
|
throw e;
|
|
67
96
|
}
|
|
68
97
|
|
|
98
|
+
/** 只读体检:当前生效的作者名是否是占位名(脏配置检测)。
|
|
99
|
+
* 给 load 用 —— 不抛错,返回占位名或 null。历史遗留的 tester 配置靠这条被看见。 */
|
|
100
|
+
export async function placeholderWarn() {
|
|
101
|
+
const u = await getUser();
|
|
102
|
+
if (!u) return null;
|
|
103
|
+
const lower = u.toLowerCase();
|
|
104
|
+
// 与 assertRealName 同一规则(含长度门槛 ≥3),两处不能写成不同判据
|
|
105
|
+
if (PLACEHOLDER_NAMES.has(lower) || (lower.length >= 3 && /^(.)\1+$/.test(lower))) return u;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
69
109
|
/** 标记串:`[[name]]`(wikilink 到人页 entities/<name>.md)。
|
|
70
110
|
* 用 wiki 链接而非裸 `@name`:人是图谱实体,点得进去看技术栈/特点。
|
|
71
111
|
* 旧数据里的裸 `@name` 仍可解析(见 todo.js extractAuthor),但不回填。 */
|