@fanchao8609/agent_brain_sync 0.1.1 → 0.2.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/package.json +1 -1
- package/skill/SKILL.md +5 -0
- package/src/install.js +84 -3
- package/src/store.js +13 -0
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -128,6 +128,11 @@ monorepo 若多个子包各自独立交付,可各建一份 `.brain/`;横向
|
|
|
128
128
|
> (经 `abs wrapup`,机械、幂等去重,不替你做判断)。**下会话 `abs load` 会自动把滞留顶到顶部**
|
|
129
129
|
> (`⏳ 上会话滞留`),所以收尾不是靠自觉记日志,而是开场被强制接上。要不要把某个任务标 done,
|
|
130
130
|
> 仍由你判断(快照只记录「哪些还开着」,不猜完成)。
|
|
131
|
+
>
|
|
132
|
+
> **主动注入(pi 已实现,别等它、也别嫌它吵)**:pi 扩展在 `agent_end` 检测「本会话真改过文件
|
|
133
|
+
> (write/edit/非只读 bash)」且「`.brain/log.md` 今日无记录」时,会注入一条 `[abs 收尾提醒]` 消息
|
|
134
|
+
> 逼你走本循环(每会话最多一次,已收尾/无图谱则不打扰)。**收到就照做,别复述提醒、别解释为什么在收尾**;
|
|
135
|
+
> 确无可沉淀产出回一句「无可沉淀」即可。Claude/Codex 侧靠 `Stop` 事件(见 event.sh)达成同样效果。
|
|
131
136
|
|
|
132
137
|
收到 Stop / "结束/先这样/切别的事" / 长任务告一段落,立即执行(快、准、不啰嗦):
|
|
133
138
|
|
package/src/install.js
CHANGED
|
@@ -281,11 +281,13 @@ function piPluginSource() {
|
|
|
281
281
|
* 纪律: hook 事件只进技术日志, 不进图谱 log.md (log.md 只收工作成果沉淀, 与 event.sh 同纪律)。
|
|
282
282
|
* Pi 事件: session_start / session_shutdown (对应 host hook 的 SessionStart/SessionEnd)。
|
|
283
283
|
* session_shutdown 额外触发 abs wrapup: 把当前项目未完成任务快照到 wrapup.log (跨会话收尾保险)。
|
|
284
|
+
* agent_end 收尾注入: 本会话真改过文件 + .brain 今日无记录 → 注入一条收尾指令, 逼 agent 走收尾循环
|
|
285
|
+
* (被动记日志不够 —— 没人提醒就不会有人收尾)。每会话最多一次, 且已收尾后不再打扰。
|
|
284
286
|
*/
|
|
285
287
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
286
|
-
import { appendFile, mkdir } from "node:fs/promises"
|
|
288
|
+
import { appendFile, mkdir, readFile, stat } from "node:fs/promises"
|
|
287
289
|
import { homedir } from "node:os"
|
|
288
|
-
import { join } from "node:path"
|
|
290
|
+
import { join, dirname } from "node:path"
|
|
289
291
|
import { spawn } from "node:child_process"
|
|
290
292
|
|
|
291
293
|
const ABS_BIN = "${join(ABS_DIR, 'bin', 'abs.js')}"
|
|
@@ -311,13 +313,92 @@ function snapshotWrapup(cwd: string): void {
|
|
|
311
313
|
} catch {} // fire-and-forget
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
// ---- 收尾注入判定 ----
|
|
317
|
+
// 真改过文件的工具(read/grep/ls 不算, 那些不产生可沉淀的产出)
|
|
318
|
+
const WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "apply_patch", "bash"])
|
|
319
|
+
// bash 里只跑查询类命令不算改文件
|
|
320
|
+
const READONLY_CMD = /^\\s*(ls|cat|grep|rg|find|head|tail|wc|git\\s+(status|log|diff|show|branch)|pwd|which|echo|node\\s+-v|npm\\s+(ls|view)|curl)\\b/
|
|
321
|
+
|
|
322
|
+
function hasWriteWork(toolResults: any[]): boolean {
|
|
323
|
+
for (const r of toolResults || []) {
|
|
324
|
+
const name = String(r?.toolName || "")
|
|
325
|
+
if (!WRITE_TOOLS.has(name)) continue
|
|
326
|
+
if (r?.isError) continue
|
|
327
|
+
if (name === "bash") {
|
|
328
|
+
const cmd = String(r?.input?.command ?? r?.args?.command ?? "")
|
|
329
|
+
if (READONLY_CMD.test(cmd)) continue
|
|
330
|
+
}
|
|
331
|
+
return true
|
|
332
|
+
}
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** 从 cwd 向上找最近含 .brain/ 的祖先目录。 */
|
|
337
|
+
async function findBrain(cwd: string): Promise<string | null> {
|
|
338
|
+
let d = cwd || process.cwd()
|
|
339
|
+
for (;;) {
|
|
340
|
+
try {
|
|
341
|
+
const st = await stat(join(d, ".brain"))
|
|
342
|
+
if (st.isDirectory()) return join(d, ".brain")
|
|
343
|
+
} catch {}
|
|
344
|
+
const up = dirname(d)
|
|
345
|
+
if (up === d) return null
|
|
346
|
+
d = up
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** .brain/log.md 今天有记录吗? 有=已收尾, 不再打扰。 */
|
|
351
|
+
async function loggedToday(brain: string): Promise<boolean> {
|
|
352
|
+
try {
|
|
353
|
+
const txt = await readFile(join(brain, "log.md"), "utf8")
|
|
354
|
+
const d = new Date()
|
|
355
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
356
|
+
const today = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
|
|
357
|
+
return txt.includes(today)
|
|
358
|
+
} catch { return false }
|
|
359
|
+
}
|
|
360
|
+
|
|
314
361
|
export default function absPiHook(pi: ExtensionAPI): void {
|
|
315
362
|
pi.on("session_start", () => logHook("session_start").catch(() => {}))
|
|
316
|
-
|
|
363
|
+
|
|
364
|
+
// 收尾注入: 每个会话最多一次, 避免反复打扰。
|
|
365
|
+
let teardownNudged = false
|
|
366
|
+
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
367
|
+
if (teardownNudged) return
|
|
368
|
+
const cwd = (ctx && ctx.cwd) || process.cwd()
|
|
369
|
+
const brain = await findBrain(cwd)
|
|
370
|
+
if (!brain) return // 无图谱=不在这项目沉淀, 不打扰
|
|
371
|
+
if (!hasWriteWork(event?.messages ? collectToolResults(event.messages) : [])) return
|
|
372
|
+
if (await loggedToday(brain)) return // 今天已收尾过
|
|
373
|
+
teardownNudged = true
|
|
374
|
+
await logHook("agent_end:teardown-nudge").catch(() => {})
|
|
375
|
+
try {
|
|
376
|
+
pi.sendUserMessage(
|
|
377
|
+
"[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:\\n" +
|
|
378
|
+
"1) 跑 abs load 看 Today 还有哪些未完成;\\n" +
|
|
379
|
+
"2) 实际做完漏登记的 abs task done <id>,做到一半的 abs task note <id> --note \\"断点\\";\\n" +
|
|
380
|
+
"3) 值得留的经验 abs note \\"...\\"(宁少勿滥,能从代码 grep 到的不记);\\n" +
|
|
381
|
+
"4) abs log \\"完成 X:...\\" 记一行工作成果,新页同步进 index。\\n" +
|
|
382
|
+
"简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\\"无可沉淀\\"即可。",
|
|
383
|
+
{ deliverAs: "followUp" },
|
|
384
|
+
)
|
|
385
|
+
} catch {}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
pi.on("session_shutdown", (_e: any, ctx: any) => {
|
|
317
389
|
snapshotWrapup((ctx && ctx.cwd) || process.cwd())
|
|
318
390
|
logHook("session_shutdown").catch(() => {})
|
|
319
391
|
})
|
|
320
392
|
}
|
|
393
|
+
|
|
394
|
+
/** agent_end 的 event.messages 里翻出 toolResult 消息。 */
|
|
395
|
+
function collectToolResults(messages: any[]): any[] {
|
|
396
|
+
const out: any[] = []
|
|
397
|
+
for (const m of messages || []) {
|
|
398
|
+
if (m && m.role === "toolResult") out.push(m)
|
|
399
|
+
}
|
|
400
|
+
return out
|
|
401
|
+
}
|
|
321
402
|
${MARK}
|
|
322
403
|
`;
|
|
323
404
|
}
|
package/src/store.js
CHANGED
|
@@ -422,6 +422,12 @@ export async function cmdLint({ dir }) {
|
|
|
422
422
|
const pages = await listPages(vault);
|
|
423
423
|
const names = new Set(pages.map((p) => p.slug));
|
|
424
424
|
const linkedNames = new Set(pages.flatMap((p) => p.links));
|
|
425
|
+
// index.md 里列的 [[x]] —— 用于反向查死引用(列了但页不存在)
|
|
426
|
+
let indexLinks = [];
|
|
427
|
+
try {
|
|
428
|
+
const idx = await fs.readFile(join(vault, 'index.md'), 'utf8');
|
|
429
|
+
indexLinks = [...idx.matchAll(/\[\[([^\]|#]+)/g)].map((m) => m[1].trim());
|
|
430
|
+
} catch { /* 无 index 则不查 */ }
|
|
425
431
|
const issues = [];
|
|
426
432
|
|
|
427
433
|
for (const pg of pages) {
|
|
@@ -447,6 +453,13 @@ export async function cmdLint({ dir }) {
|
|
|
447
453
|
}
|
|
448
454
|
}
|
|
449
455
|
|
|
456
|
+
// index.md 反向检查: 列了 [[x]] 但 x 页不存在 —— 删页/归档 source 后忘了清 index 的残留。
|
|
457
|
+
// (page→index 的 INDEX-MISSING 已有, 这里补 index→page, 否则死引用静默留在入口文件里)
|
|
458
|
+
for (const ln of indexLinks) {
|
|
459
|
+
if (/slug|Name|name|Date|页面名$/.test(ln)) continue; // 模板占位行
|
|
460
|
+
if (!names.has(ln)) issues.push(`INDEX-DEAD-LINK: index.md -> [[${ln}]] (该页不存在, 删页后忘清 index?)`);
|
|
461
|
+
}
|
|
462
|
+
|
|
450
463
|
const nsrc = pages.filter((p) => p.dir === 'sources').length;
|
|
451
464
|
if (nsrc > 10) issues.push(`SOURCES-PILED-UP: sources/ has ${nsrc} files > 10; 提炼归档旧 source`);
|
|
452
465
|
|