@fanchao8609/agent_brain_sync 1.19.6 → 1.20.1
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 +8 -7
- package/bin/abs.js +7 -7
- package/bin/mcp.js +7 -7
- package/hooks/abs.omp.ts +15 -17
- package/hooks/abs.pi.ts +29 -34
- package/hooks/event.sh +1 -1
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +104 -99
- package/src/lint.js +22 -5
- package/src/store.js +20 -13
- package/src/todo/archive.js +5 -2
- package/src/todo/common.js +40 -31
- package/src/todo/spec.js +3 -5
- package/src/todo/structure.js +44 -16
- package/src/todo/tasks.js +8 -12
- package/src/todo/validate.js +7 -8
- package/src/todo.js +1 -0
- package/src/wrapup.js +17 -11
package/README.md
CHANGED
|
@@ -62,16 +62,15 @@ pi 与 omp 宿主额外多一层 UI(其它宿主没有):**编辑器上方
|
|
|
62
62
|
─────────────────────────────────────────────────
|
|
63
63
|
📋 todo (3) — fanchao · ☕ 靠咖啡续命
|
|
64
64
|
├─ [进行中] some-task ●●○ — 干活中
|
|
65
|
-
│
|
|
65
|
+
│ - [ ] hooks/abs.pi.ts:120
|
|
66
66
|
├─ [滞留中] waiting — 等外部输入
|
|
67
67
|
└─ [进行中] another ●●● — …
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
- **只显示未完成**,Done 归计数不占位(数量在标题里)
|
|
71
71
|
- **按当前使用者过滤**:显示 `[[我]]` 的 + 没标作者的(老任务/手写),别人的不显示
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
单独的 `↳ 断点: 一句话` 仍是纯附属行、不算任务
|
|
72
|
+
- **子项(缩进的 `- [ ]`)挂在父任务下**;`├─` / `└─` 表结构
|
|
73
|
+
- **子项可各自勾选**:` - [ ] 一句话` → ` - [x] 一句话`;全部勾上才能 `done` 父任务
|
|
75
74
|
- **「进行中」带三点动画**`○○○ → ●●○ → ●●●`(250ms/帧)—— 一眼看出哪条在跑
|
|
76
75
|
- **实时刷新**:我调 `abs_task` / `abs_board` 后立即重画,不用等我讲完话
|
|
77
76
|
- **上描边与输入框同色满宽**(主题色 `thinkingOff`,跟 pi 输入框的边框一致)
|
|
@@ -109,8 +108,8 @@ abs todo # 看板 Todo / Done(行首带状态标记)
|
|
|
109
108
|
|
|
110
109
|
abs todo add TASK-1 --note "要做什么"
|
|
111
110
|
abs todo note TASK-1 --note "改到 X 文件 L40" # 追加一条子项(标准 md 嵌套列表,可多条)
|
|
112
|
-
abs todo
|
|
113
|
-
abs todo
|
|
111
|
+
abs todo child-done TASK-1 [--n 2] # 勾子项(n=第几条,缺省最后一条)
|
|
112
|
+
abs todo child-undone TASK-1 [--n 2] # 取消勾(勾错了退回)
|
|
114
113
|
abs todo memo TASK-1 "随手记一段(可多句,不上看板)" # 任务笔记 → sessions/;无参=查看
|
|
115
114
|
abs todo state TASK-1 --note "进行中|讨论中|滞留中" # 改行首状态标记
|
|
116
115
|
abs todo done TASK-1
|
|
@@ -132,7 +131,9 @@ abs update # 升级到最新版并刷新四宿主
|
|
|
132
131
|
|
|
133
132
|
> `abs todo start` 与 `abs todo add` 等价(都登记任务)。
|
|
134
133
|
> **子项就是标准 Markdown 嵌套列表**:任务下缩进 2 空格的 `- [ ] …`,可多条、可各自勾选;
|
|
135
|
-
>
|
|
134
|
+
> 面板按缩进画树线,归档时原样保留缩进。
|
|
135
|
+
> 旧版的 `↳ 断点:` 写法已在 1.20.0 移除;存量会在 `abs load` 或任意写操作时
|
|
136
|
+
> 自动迁移成标准子项(内容一条不丢),不需要手工改。
|
|
136
137
|
> 任务 id 不能含空格(用连字符),含空格会被明确拒掉。
|
|
137
138
|
> 旧版 `abs task ...` / `abs board` 已废,打它们会报「未知命令」并退出码 1。
|
|
138
139
|
> `abs wrapup` / `abs teardown-check` 是 hook 内部命令,无需手动调用。
|
package/bin/abs.js
CHANGED
|
@@ -311,9 +311,9 @@ const subUsage = {
|
|
|
311
311
|
' abs todo 看板',
|
|
312
312
|
' abs todo add <id> [--note "做什么"] [--section 讨论中|滞留中|搁置]',
|
|
313
313
|
' abs todo rename <旧id> --note "<新id>" 改任务 id(起错名/不可读时)',
|
|
314
|
-
' abs todo note <id> --note "
|
|
315
|
-
' abs todo
|
|
316
|
-
' abs todo
|
|
314
|
+
' abs todo note <id> --note "进度/子项" 追加一条子项(标准 md 缩进列表)',
|
|
315
|
+
' abs todo child-done <id> [--n N] 勾子项(缺省最后一条)',
|
|
316
|
+
' abs todo child-undone <id> [--n N] 取消勾',
|
|
317
317
|
' abs todo memo <id> "随手记(可多句)" 任务笔记 → sessions/,不上看板;无文字=查看',
|
|
318
318
|
' abs todo state <id> --note 进行中|讨论中|滞留中|搁置',
|
|
319
319
|
' abs todo done <id> [--as 落地|否决|仅方案] [结语文字]',
|
|
@@ -378,8 +378,8 @@ const TODO_ACTIONS = {
|
|
|
378
378
|
add: 'start',
|
|
379
379
|
start: 'start', // add 的别名(老习惯保留)
|
|
380
380
|
note: 'note',
|
|
381
|
-
'
|
|
382
|
-
'
|
|
381
|
+
'child-done': 'child-done', // 勾子项(标准 md 缩进列表项)
|
|
382
|
+
'child-undone': 'child-undone', // 取消勾子项
|
|
383
383
|
memo: 'memo', // 任务随手记:多句/追加/不占看板(无文字时=查看)
|
|
384
384
|
state: 'state', // 改行首状态标记:进行中|讨论中|滞留中|搁置(原地,不搬区)
|
|
385
385
|
rename: 'rename', // 改任务 id:人工起错名、或旧 id 不可读时用
|
|
@@ -475,7 +475,7 @@ async function main() {
|
|
|
475
475
|
if (!action) {
|
|
476
476
|
throw new Error(
|
|
477
477
|
`✗ 未知子命令 "${sub}"\n` +
|
|
478
|
-
` 可用: add / start / note /
|
|
478
|
+
` 可用: add / start / note / child-done / child-undone / memo / state / done / archive\n` +
|
|
479
479
|
` 看板: abs todo(不带参数)\n` +
|
|
480
480
|
` 登记任务: abs todo add <id> --note "做什么"`,
|
|
481
481
|
);
|
|
@@ -499,7 +499,7 @@ async function main() {
|
|
|
499
499
|
break;
|
|
500
500
|
}
|
|
501
501
|
// 勾/取消勾子项:只收 <id>(子项位置 = 「某任务的第 N 条」,没有自己的 id)
|
|
502
|
-
if (action === '
|
|
502
|
+
if (action === 'child-done' || action === 'child-undone') {
|
|
503
503
|
rejectExtra(rest2, `abs todo ${sub} <id> [--n N]`);
|
|
504
504
|
if (!id) throw new Error(`✗ 缺 <id>\n 用法: abs todo ${sub} <id> [--n N]`);
|
|
505
505
|
const n = opts.n ? Number(opts.n) : undefined;
|
package/bin/mcp.js
CHANGED
|
@@ -90,7 +90,7 @@ tool(
|
|
|
90
90
|
|
|
91
91
|
tool(
|
|
92
92
|
'abs_board',
|
|
93
|
-
'读取当前项目 todo
|
|
93
|
+
'读取当前项目 todo 看板(含任务与子项)。先 resolve 得到项目,再传其根目录。',
|
|
94
94
|
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
95
95
|
async ({ cwd }) => {
|
|
96
96
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
@@ -112,12 +112,12 @@ tool(
|
|
|
112
112
|
|
|
113
113
|
tool(
|
|
114
114
|
'abs_task',
|
|
115
|
-
'任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补子项(**追加**,标准 md 嵌套列表,`abs todo note`)/
|
|
115
|
+
'任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补子项(**追加**,标准 md 嵌套列表,`abs todo note`)/ child_done、child_undone 勾/取消勾子项(n=第几条,缺省最后一条)/ memo 任务随手记(多句/追加/不上看板)/ state 改状态 / done 完成归位 Done(done 时 note=结语文字,as=结语类型:落地|否决|仅方案)。必填: cwd, action, id。多会话并行时,start/add 带上 session(值见 system prompt 的 [会话] 段)以标记「这条任务谁在做」。',
|
|
116
116
|
{
|
|
117
|
-
action: z.enum(['add', 'start', 'done', 'note', '
|
|
117
|
+
action: z.enum(['add', 'start', 'done', 'note', 'child_done', 'child_undone', 'memo', 'state']),
|
|
118
118
|
id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
|
|
119
|
-
n: z.number().int().positive().optional().describe('
|
|
120
|
-
cwd: z.string().describe('项目根目录(.brain/ 所在处)'), note: z.string().optional().describe('add/start=做什么; note
|
|
119
|
+
n: z.number().int().positive().optional().describe('child_done/child_undone 用:第几条子项(1-based,缺省=最后一条)'),
|
|
120
|
+
cwd: z.string().describe('项目根目录(.brain/ 所在处)'), note: z.string().optional().describe('add/start=做什么; note=追加一条子项(改到哪/下一步,一句话); memo=随手记(可多句、不定长,追加式;不传=查看已有笔记); state=进行中|讨论中|滞留中|搁置(搁置=用户改方向/不做了;滞留中=还要做只是卡住); done=结语文字'),
|
|
121
121
|
as: z.enum(['落地', '否决', '仅方案']).optional().describe('仅 done:结语类型。默认 落地。做了又撤/评估后不做用 否决,只设计过用 仅方案 —— 别让假【落地】污染看板'),
|
|
122
122
|
// 会话 id:多会话并行时标记「这条任务是谁在做」。
|
|
123
123
|
// 值取自 system prompt 里的 `[会话] 本会话 id = xxx`(hook 注入),由模型原样回传。
|
|
@@ -130,8 +130,8 @@ tool(
|
|
|
130
130
|
if (!root) return errNoBrain(cwd || process.cwd());
|
|
131
131
|
// add 是 start 的别名(与 CLI `abs todo add` 对齐)
|
|
132
132
|
const act = action === 'add' ? 'start' : action;
|
|
133
|
-
//
|
|
134
|
-
const act2 = act === '
|
|
133
|
+
// child_done → child-done(MCP 参数名不用连字符,内部 action 用)
|
|
134
|
+
const act2 = act === 'child_done' ? 'child-done' : act === 'child_undone' ? 'child-undone' : act;
|
|
135
135
|
return { content: [{ type: 'text', text: await cmdTask({ dir: root, action: act2, id, note, as, session, n }) }] };
|
|
136
136
|
}
|
|
137
137
|
);
|
package/hooks/abs.omp.ts
CHANGED
|
@@ -104,7 +104,7 @@ function inputField(input: unknown, key: string): string {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** 会改文件的工具名。用于判定「真的开始干活了」—— 排查类会话先跑一堆读命令,
|
|
107
|
-
* 若用「第一次 tool_call
|
|
107
|
+
* 若用「第一次 tool_call」当判据,子项会被写在与任务无关的文件上。
|
|
108
108
|
* 白名单必须按宿主实际用法穷举,漏一个就把功能静默关掉(含 bash:大量修改是经 bash 完成的)。 */
|
|
109
109
|
const WRITE_TOOL = /^(write|edit|apply_patch|patch|multiedit|notebookedit|bash|shell)$/i
|
|
110
110
|
|
|
@@ -193,6 +193,8 @@ const TODO_GUIDELINES = [
|
|
|
193
193
|
'Never mark a task "done" when tests are failing, the implementation is partial, or an error is unresolved — keep it in progress and add a task for the blocker instead.',
|
|
194
194
|
'Keep exactly one task in "进行中" at a time; when the user switches direction or drops a task, use action "state" with 搁置 (dropped/not doing it); 滞留中 for still-wanted-but-stuck; 讨论中 for still-open. If you finish one and another is ready, promote it explicitly.',
|
|
195
195
|
'When completing, pick the honest conclusion via the `as` field: 落地 (built and verified) / 否决 (decided against, or built then reverted) / 仅方案 (designed only). A wrong conclusion makes the next session treat "considered" as "completed".',
|
|
196
|
+
// 文档精简九条(用户 2026-10-22 定,原文见 skill 的「文档精简」一节):管每一行写进 .brain 的文字。
|
|
197
|
+
'Every line you write into .brain/ (todo rows, log rows, knowledge pages, session snapshots) must pass the nine-bullet doc-slimming rule in the abs skill: only what serves the topic (drop the rest), no unverified summaries, ordered and layered, plain declarative wording (no rhetorical questions, no guessing), no duplication (cite the other doc instead), terse prohibitions, nothing the user did not ask for, plain Chinese.',
|
|
196
198
|
]
|
|
197
199
|
|
|
198
200
|
/** 从 skill 里挑出的【硬规则】—— 只第一轮送, 与 load 同机会(同 pi 版方案 B)。 */
|
|
@@ -354,8 +356,8 @@ export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
|
354
356
|
rows: { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; sid: string }[]
|
|
355
357
|
hidden: number
|
|
356
358
|
} {
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
+
// 两阶段:先无过滤地按序解析(子项归属需要「上一条」是原文里真正的前一条),
|
|
360
|
+
// 再按作者过滤。若边解析边过滤,被滤掉的条目下方的子项会挂到它上面那一条。
|
|
359
361
|
type Row = { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; authors: string[]; sid: string }
|
|
360
362
|
const parsed: Row[] = []
|
|
361
363
|
let inTodo = false
|
|
@@ -364,7 +366,7 @@ export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
|
364
366
|
if (/^##\s+Todo\s*$/.test(line)) { inTodo = true; continue }
|
|
365
367
|
if (/^##\s+/.test(line)) { inTodo = false; continue }
|
|
366
368
|
if (!inTodo) continue
|
|
367
|
-
// 子项行(标准 md:缩进的 `- [ ]
|
|
369
|
+
// 子项行(标准 md:缩进的 `- [ ]`)→ 挂到上一条任务
|
|
368
370
|
const kid = parseChildLine(line)
|
|
369
371
|
if (kid && kid.indent > 0) {
|
|
370
372
|
const last = parsed[parsed.length - 1]
|
|
@@ -372,9 +374,9 @@ export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
|
372
374
|
continue
|
|
373
375
|
}
|
|
374
376
|
// `- [ ] [状态] <id> …`;状态标记可能缺省。
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
const m = line.match(/^\s
|
|
377
|
+
// 只认 `- [ ]`(2026-10-20 旧 `↳ [ ]` 写法已废弃)。
|
|
378
|
+
// 子项任务必须显示 —— 它就是一等待任务(写得进去、看得见、能 done)。
|
|
379
|
+
const m = line.match(/^\s*-\s*\[ \]\s*(?:\[([^\]]+)\]\s*)?(\S+)\s*(.*)$/)
|
|
378
380
|
if (!m) continue
|
|
379
381
|
const state = m[1] || '进行中'
|
|
380
382
|
const id = m[2]
|
|
@@ -417,13 +419,9 @@ async function currentUser(): Promise<string> {
|
|
|
417
419
|
* 宽度规则:CJK/全角记 2 列,其余记 1 列。 */
|
|
418
420
|
/** 解析子项行(与 src/todo/common.js 的 parseChild 同规则,双份实现 —— 见 pi 版说明)。 */
|
|
419
421
|
function parseChildLine(l: string): { done: boolean; text: string; indent: number } | null {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (!old[2].trim()) return null
|
|
424
|
-
return { done: /^\s*↳\s*\[x\]/.test(s), text: old[2].trim(), indent: old[1].length }
|
|
425
|
-
}
|
|
426
|
-
const m = s.match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/)
|
|
422
|
+
// 只认标准 md 缩进子项(2026-10-20 旧箭头写法已废弃)。
|
|
423
|
+
// 旧行由 CLI 侧 migrateLegacyChild 在读写时迁移掉,面板拿到的已是新格式。
|
|
424
|
+
const m = String(l || '').match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/)
|
|
427
425
|
if (!m) return null
|
|
428
426
|
return { done: m[2] === 'x', text: m[3].trim(), indent: m[1].length }
|
|
429
427
|
}
|
|
@@ -532,7 +530,7 @@ export function renderPanelLines(
|
|
|
532
530
|
return lines
|
|
533
531
|
}
|
|
534
532
|
data.rows.forEach((t, i) => {
|
|
535
|
-
// `└─` 只给最后一条 ——
|
|
533
|
+
// `└─` 只给最后一条 —— 不管它有没有子项(否则末条带子项时全篇没有收尾)。
|
|
536
534
|
const isLast = i === lastIdx
|
|
537
535
|
const branch = isLast ? '└─' : '├─'
|
|
538
536
|
const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + fg('accent', todoAnimFrame(animPhase)) : ''
|
|
@@ -707,7 +705,7 @@ export default function absOmpExtension(pi: ExtensionAPI): void {
|
|
|
707
705
|
resetThrottle()
|
|
708
706
|
|
|
709
707
|
pi.on("session_start", (_event: SessionStartEvent, ctx: ExtensionContext) => {
|
|
710
|
-
// 新会话 =
|
|
708
|
+
// 新会话 = 重新登记:不重置则同进程的第二个会话永远不再落子项
|
|
711
709
|
autoTaskSeen = false
|
|
712
710
|
filesSeen.clear() // 同理:新会话的文件痕迹不能带到下个会话
|
|
713
711
|
loadInjected.clear() // 同:新会话的每个目录都要重新摆一次开局状态
|
|
@@ -743,7 +741,7 @@ export default function absOmpExtension(pi: ExtensionAPI): void {
|
|
|
743
741
|
pendingTodoTool = true
|
|
744
742
|
logHook(`tool_call todo_tool name=${name} target=${target || '-'}`).catch(() => {})
|
|
745
743
|
}
|
|
746
|
-
//
|
|
744
|
+
// 自动登记子项:真动文件时把「改到哪个」记进本会话(可观测埋点,不写看板)。
|
|
747
745
|
if (!autoTaskSeen && isWriteCall(name, input)) {
|
|
748
746
|
autoTaskSeen = true
|
|
749
747
|
const sid = sessionIdFor(ctx)
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -95,15 +95,15 @@ function sessionIdFor(ctx: any): string {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** 会改文件的工具名。用于判定「真的开始干活了」—— 排查类会话先跑一堆读命令,
|
|
98
|
-
* 若用「第一次 tool_call
|
|
98
|
+
* 若用「第一次 tool_call」当判据,子项会被记在与任务无关的文件上。
|
|
99
99
|
*
|
|
100
100
|
* 为何含 bash:大量修改是经 bash 完成的(heredoc / sed -i / > 重定向),
|
|
101
|
-
* 漏掉它 = 纯 bash
|
|
101
|
+
* 漏掉它 = 纯 bash 会话永远不记子项(见 concept opencode-inject-channel-verdict
|
|
102
102
|
* 的同源教训:白名单必须按宿主实际用法穷举,漏一个就把功能静默关掉)。
|
|
103
103
|
*/
|
|
104
104
|
const WRITE_TOOL = /^(write|edit|apply_patch|patch|multiedit|notebookedit|bash|shell)$/i
|
|
105
105
|
|
|
106
|
-
/** bash 命令里没有写入迹象则不算「干活」—— 否则 `ls`/`git status`
|
|
106
|
+
/** bash 命令里没有写入迹象则不算「干活」—— 否则 `ls`/`git status` 也会记子项。 */
|
|
107
107
|
const BASH_WRITES = /(^|[^\w])(>|>>|sed\s+-i|tee\b|mkdir\b|rm\b|mv\b|cp\b|touch\b|cat\s*>)/
|
|
108
108
|
|
|
109
109
|
/** 这次 tool_call 是否算「真的动了文件」。 */
|
|
@@ -145,8 +145,8 @@ let agentEndSeen = false
|
|
|
145
145
|
* 同样在模块级,理由同上(闭包 flag 在重复注册时不共享)。 */
|
|
146
146
|
let pendingTodoTool = false
|
|
147
147
|
|
|
148
|
-
/**
|
|
149
|
-
*
|
|
148
|
+
/** 本会话是否已记过自动任务子项。模块级(重复注册时闭包 flag 不共享 —— 同 agentEndSeen 的坑)。
|
|
149
|
+
* 只记第一次写文件,不是每次都记:子项是「记到哪」不是流水账。 */
|
|
150
150
|
let autoTaskSeen = false
|
|
151
151
|
|
|
152
152
|
/** 本会话动过的文件(去重,最多记 FILES_SEEN_MAX 个)。
|
|
@@ -219,7 +219,7 @@ const TODO_GUIDELINES = [
|
|
|
219
219
|
'Use the abs task tool as soon as you receive instructions that involve changing this project — including investigation work where you read code or ask questions before any edit. Call it with action "start" and a short id before the work itself, not after. Skip it for pure questions about general knowledge, one-line answers, and conversation. (Tool name varies by host: `mcp__abs` with tool="abs_task", or `mcp__abs__abs_task`, or `abs_task` — use whichever form this session exposes.)',
|
|
220
220
|
// ② 完成即结:不许攒
|
|
221
221
|
'Mark a task "done" immediately when it finishes — never batch completions at the end of a session.',
|
|
222
|
-
// ③
|
|
222
|
+
// ③ 子项:跨会话接力靠它
|
|
223
223
|
'Before starting a task, record the checkpoint with action "note" (which file, which step) so a later session can resume.',
|
|
224
224
|
// ④ 反例(照 rpiv-todo):明确什么情况【不许】标完成 —— 只给正向要求时,
|
|
225
225
|
// 模型倾向于把"我以为做完了"当成完成,结语失真会污染下个会话的判断。
|
|
@@ -228,6 +228,9 @@ const TODO_GUIDELINES = [
|
|
|
228
228
|
'Keep exactly one task in "进行中" at a time; when the user switches direction or drops a task, use action "state" with 搁置 (dropped/not doing it); 滞留中 for still-wanted-but-stuck; 讨论中 for still-open. If you finish one and another is ready, promote it explicitly.',
|
|
229
229
|
// ⑥ 结语要真实:三种结语各有含义,别一律写落地。
|
|
230
230
|
'When completing, pick the honest conclusion via the `as` field: 落地 (built and verified) / 否决 (decided against, or built then reverted) / 仅方案 (designed only). A wrong conclusion makes the next session treat "considered" as "completed".',
|
|
231
|
+
// ⑦ 文档精简九条(用户 2026-10-22 定,原文见 skill 的「文档精简」一节):
|
|
232
|
+
// 管每一行写进 .brain 的文字;与上面六条行为规则不重叠,故不合并。
|
|
233
|
+
'Every line you write into .brain/ (todo rows, log rows, knowledge pages, session snapshots) must pass the nine-bullet doc-slimming rule in the abs skill: only what serves the topic (drop the rest), no unverified summaries, ordered and layered, plain declarative wording (no rhetorical questions, no guessing), no duplication (cite the other doc instead), terse prohibitions, nothing the user did not ask for, plain Chinese.',
|
|
231
234
|
]
|
|
232
235
|
|
|
233
236
|
/** 当前项目是否有 .brain 图谱 —— 有才加指引(没图谱的项目里这指引是噪音)。
|
|
@@ -717,15 +720,15 @@ export function pickNickname(rnd: () => number = Math.random): string {
|
|
|
717
720
|
* who 不为空时只收“我的任务”:行内有 [[who]] 的,或**完全没标作者**的
|
|
718
721
|
* (老任务/手写的没标,漏掉比多显示更糟)。标了别人的则不收。
|
|
719
722
|
*
|
|
720
|
-
*
|
|
723
|
+
* 子项行(缩进的 `- [ ]`)归属到它上一条任务,不单独成条。 */
|
|
721
724
|
export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
722
725
|
total: number
|
|
723
726
|
rows: { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; sid: string }[]
|
|
724
727
|
hidden: number
|
|
725
728
|
} {
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
// ——
|
|
729
|
+
// 两阶段:先无过滤地按序解析(子项归属需要“上一条”是原文里真正的前一条),
|
|
730
|
+
// 再按作者过滤。若边解析边过滤,被滤掉的条目下方的子项会挂到它上面那一条
|
|
731
|
+
// —— 实测:别人任务的子项会显示在我的任务下面(2026-10-03 审查发现)。
|
|
729
732
|
type Row = { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; authors: string[]; sid: string }
|
|
730
733
|
const parsed: Row[] = []
|
|
731
734
|
let inTodo = false
|
|
@@ -734,7 +737,7 @@ export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
|
734
737
|
if (/^##\s+Todo\s*$/.test(line)) { inTodo = true; continue }
|
|
735
738
|
if (/^##\s+/.test(line)) { inTodo = false; continue }
|
|
736
739
|
if (!inTodo) continue
|
|
737
|
-
// 子项行(标准 md:缩进的 `- [ ]
|
|
740
|
+
// 子项行(标准 md:缩进的 `- [ ]`)→ 挂到上一条任务。
|
|
738
741
|
// 子项可多条、可各自勾选(2026-10-16 用户定)。
|
|
739
742
|
const kid = parseChildLine(line)
|
|
740
743
|
if (kid && kid.indent > 0) {
|
|
@@ -743,10 +746,10 @@ export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
|
743
746
|
continue
|
|
744
747
|
}
|
|
745
748
|
// `- [ ] [状态] <id> …`;状态标记可能缺省。
|
|
746
|
-
//
|
|
747
|
-
//
|
|
749
|
+
// 只认 `- [ ]`(2026-10-20 旧 `↳ [ ]` 写法已废弃)。
|
|
750
|
+
// 子项任务必须显示 —— 它就是一等等任务(写得进去、看得见、能 done),
|
|
748
751
|
// 否则落回「静默死区」(用户在 .brain 里报的原症状)。
|
|
749
|
-
const m = line.match(/^\s
|
|
752
|
+
const m = line.match(/^\s*-\s*\[ \]\s*(?:\[([^\]]+)\]\s*)?(\S+)\s*(.*)$/)
|
|
750
753
|
if (!m) continue
|
|
751
754
|
const state = m[1] || '进行中'
|
|
752
755
|
const id = m[2]
|
|
@@ -800,15 +803,11 @@ async function currentUser(): Promise<string> {
|
|
|
800
803
|
*
|
|
801
804
|
* 为何不 import src:面板是宿主扩展,单独装到 ~/.pi/extensions/,与包内 src/ 不同目录
|
|
802
805
|
* (装完就断了)。故两端各一份 —— 规则简单(两步正则),且两边测试都盖。
|
|
803
|
-
* 标准 md:缩进的 `- [ ]
|
|
806
|
+
* 标准 md:缩进的 `- [ ] 内容`。 */
|
|
804
807
|
function parseChildLine(l: string): { done: boolean; text: string; indent: number } | null {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (!old[2].trim()) return null
|
|
809
|
-
return { done: /^\s*↳\s*\[x\]/.test(s), text: old[2].trim(), indent: old[1].length }
|
|
810
|
-
}
|
|
811
|
-
const m = s.match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/)
|
|
808
|
+
// 只认标准 md 缩进子项(2026-10-20 旧箭头写法已废弃)。
|
|
809
|
+
// 旧行由 CLI 侧 migrateLegacyChild 在读写时迁移掉,面板拿到的已是新格式。
|
|
810
|
+
const m = String(l || '').match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/)
|
|
812
811
|
if (!m) return null
|
|
813
812
|
return { done: m[2] === 'x', text: m[3].trim(), indent: m[1].length }
|
|
814
813
|
}
|
|
@@ -945,9 +944,9 @@ export function renderPanelLines(
|
|
|
945
944
|
return lines
|
|
946
945
|
}
|
|
947
946
|
data.rows.forEach((t, i) => {
|
|
948
|
-
// `└─` 只给最后一条 ——
|
|
949
|
-
// (2026-10-03 审查修正:原条件 `i === lastIdx && !t.note`
|
|
950
|
-
// 全篇没有 └─
|
|
947
|
+
// `└─` 只给最后一条 —— 不管它有没有子项。
|
|
948
|
+
// (2026-10-03 审查修正:原条件 `i === lastIdx && !t.note` 会让“末条带子项”时
|
|
949
|
+
// 全篇没有 └─ 收尾,子项行的 │ 又延伸到视觉底部,看起来像列表被截断。)
|
|
951
950
|
const isLast = i === lastIdx
|
|
952
951
|
const branch = isLast ? '└─' : '├─'
|
|
953
952
|
// 进行中的行带一个小动画(其余行不加,避免满屏都在跳)
|
|
@@ -1044,7 +1043,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
1044
1043
|
resetThrottle()
|
|
1045
1044
|
|
|
1046
1045
|
pi.on("session_start", (event: any, ctx: any) => {
|
|
1047
|
-
// 新会话 =
|
|
1046
|
+
// 新会话 = 重新登记:不重置则同进程的第二个会话永远不再落子项
|
|
1048
1047
|
autoTaskSeen = false
|
|
1049
1048
|
filesSeen.clear() // 同理:新会话的文件痕迹不能带到下个会话
|
|
1050
1049
|
loadInjected.clear() // 同:新会话的每个目录都要重新摆一次开局状态
|
|
@@ -1085,8 +1084,8 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
1085
1084
|
pendingTodoTool = true
|
|
1086
1085
|
logHook(`tool_call todo_tool name=${name} target=${target || '-'}`).catch(() => {})
|
|
1087
1086
|
}
|
|
1088
|
-
//
|
|
1089
|
-
// 为何不用「第一次 tool_call
|
|
1087
|
+
// 自动登记子项(2026-10-05):真的动文件时,把「改到哪个」记进本会话的自动任务。
|
|
1088
|
+
// 为何不用「第一次 tool_call」:排查类会话先跑一堆读命令,子项会落在无关文件上。
|
|
1090
1089
|
// 写类工具只留一行痕(可观测),不 spawn、不写看板 —— 见上面的删除说明。
|
|
1091
1090
|
if (!autoTaskSeen && isWriteCall(name, input)) {
|
|
1092
1091
|
autoTaskSeen = true
|
|
@@ -1162,11 +1161,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
1162
1161
|
}
|
|
1163
1162
|
if (rulesOn) openingRulesLogged = true
|
|
1164
1163
|
}
|
|
1165
|
-
|
|
1166
|
-
// 真实环境里 agent_brain_sync 从未 load_delivered 却 still skip → 与「按 cwd 分」矛盾。
|
|
1167
|
-
// 靠这行区分:Set 空(→ 判断写错)vs Set 里已有该 cwd(→ 有东西提前加进去了)。
|
|
1168
|
-
// 定位并修好后删掉这行。
|
|
1169
|
-
logHook(`before_agent_start todo_guide=${ok ? 'on' : 'off'} load_guide=${loadState} opening_rules=${openingRulesLogged ? 'on' : 'skip'} seen=[${[...loadInjected].join(',')}] pending=[${loadPending}] cwd=${cwd}`).catch(() => {})
|
|
1164
|
+
logHook(`before_agent_start todo_guide=${ok ? 'on' : 'off'} load_guide=${loadState} opening_rules=${openingRulesLogged ? 'on' : 'skip'} cwd=${cwd}`).catch(() => {})
|
|
1170
1165
|
|
|
1171
1166
|
// 自动登记已整体移除(2026-10-05 用户实报两点):
|
|
1172
1167
|
// ① 卡顿 —— agent_end 每轮 spawn 一个 node 进程
|
|
@@ -1205,7 +1200,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
1205
1200
|
}
|
|
1206
1201
|
// 每轮 spawn 已删除(2026-10-05 用户实报"卡顿"):agentEndSeen 一旦置位,
|
|
1207
1202
|
// 下面那个条件就恒真 → 每轮结束都起一个 node 进程,纯属白烧。
|
|
1208
|
-
//
|
|
1203
|
+
// 子项改由 agent 在用 abs_task 时自己写(那时它本来就在跑,不额外起进程)。
|
|
1209
1204
|
})
|
|
1210
1205
|
|
|
1211
1206
|
pi.on("session_shutdown", (_e: any, ctx: any) => {
|
package/hooks/event.sh
CHANGED
|
@@ -103,7 +103,7 @@ fi
|
|
|
103
103
|
printf '[%s] wrapup-remind 会话已结束(%s): 若 Today 有滞留任务或经验未沉淀, 下会话开头走收尾循环\n' \
|
|
104
104
|
"$(date '+%Y-%m-%d %H:%M:%S')" "$SESSION_ID"
|
|
105
105
|
} >> "$LOG_DIR/wrapup.log" 2>/dev/null || true
|
|
106
|
-
# 机械快照滞留清单: abs wrapup
|
|
106
|
+
# 机械快照滞留清单: abs wrapup 把当前项目未完成任务+子项追加到 wrapup.log (跨会话收尾保险)。
|
|
107
107
|
# abs wrapup 已自行写 wrapup.log (honor ABS_LOG_DIR); 这里 stdout 只进 hooks.log 作痕迹, 避免污染 wrapup.log。
|
|
108
108
|
"$NODE_BIN" "$ABS_BIN" wrapup >>"$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
109
109
|
fi
|