@fanchao8609/agent_brain_sync 1.1.0 → 1.2.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/bin/abs.js +7 -1
- package/hooks/event.sh +32 -1
- package/package.json +1 -1
- package/src/install.js +28 -6
- package/src/store.js +66 -1
package/bin/abs.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// bin/abs.js — abs CLI 入口。
|
|
3
3
|
// abs <cmd> [args]
|
|
4
4
|
// 命令: init / board / status / load / task / install / uninstall / help
|
|
5
|
-
import { cmdInit, cmdBoard, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdShow, cmdRepair, cmdWrapup } from '../src/store.js';
|
|
5
|
+
import { cmdInit, cmdBoard, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdShow, cmdRepair, cmdWrapup, cmdTeardownCheck } from '../src/store.js';
|
|
6
6
|
import { runInstall, runUninstall, installSummary } from '../src/install.js';
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
@@ -69,6 +69,7 @@ function parseArgv(args) {
|
|
|
69
69
|
else if (a === '--id') { o.id = args[++i]; }
|
|
70
70
|
else if (a === '--section') { o.section = args[++i]; }
|
|
71
71
|
else if (a === '--note') { o.note = args[++i]; }
|
|
72
|
+
else if (a === '--payload') { o.payload = args[++i]; }
|
|
72
73
|
else if (a === '--yes') { o.yes = true; }
|
|
73
74
|
else if (a === '--repair') { o.repair = true; }
|
|
74
75
|
else if (a === '--no-mcp') { o.mcp = false; }
|
|
@@ -155,6 +156,11 @@ async function main() {
|
|
|
155
156
|
console.log(await cmdWrapup({ dir: opts.dir }));
|
|
156
157
|
break;
|
|
157
158
|
}
|
|
159
|
+
// Stop hook 用: 判定是否注入收尾指令。stdout 给 shell (`push:<json>` / `{}`), 无额外输出。
|
|
160
|
+
case 'teardown-check': {
|
|
161
|
+
process.stdout.write((await cmdTeardownCheck({ dir: opts.dir, payload: opts.payload })) + '\n');
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
158
164
|
case 'update': await cmdUpdate({ yes: opts.yes }); break;
|
|
159
165
|
case '--version': case '-v': case 'version': console.log(pkgVersion()); break;
|
|
160
166
|
case 'help': case undefined: case '--help': console.log(usage); break;
|
package/hooks/event.sh
CHANGED
|
@@ -15,6 +15,18 @@ EVENT="__EVENT__"
|
|
|
15
15
|
|
|
16
16
|
PAYLOAD=$(cat 2>/dev/null | head -c 500)
|
|
17
17
|
|
|
18
|
+
# ── Stop 收尾注入 (CC/Codex 的"主动推") ─────────────────────────────────
|
|
19
|
+
# 守卫同 pi 插件四条件: ①本会话真改过文件 ②本项目有 .brain ③log.md 今日无条目 ④每会话一次。
|
|
20
|
+
# 判定在 CLI 里做(cmdTeardownCheck),这里只解析结果。
|
|
21
|
+
# 关键: 回宿主的 decision:block 必须走 stdout 同步输出,所以判定要在前台算完,不能放后台子 shell。
|
|
22
|
+
BAIL='{}'
|
|
23
|
+
if [ "$EVENT" = "Stop" ]; then
|
|
24
|
+
TEARDOWN=$("$NODE_BIN" "$ABS_BIN" teardown-check --payload "$PAYLOAD" 2>/dev/null)
|
|
25
|
+
case "$TEARDOWN" in
|
|
26
|
+
push:*) BAIL=$(printf '%s' "$TEARDOWN" | sed 's/^push://') ;;
|
|
27
|
+
esac
|
|
28
|
+
fi
|
|
29
|
+
|
|
18
30
|
# 后台执行,hook 立即返回——宿主热路径零阻塞。
|
|
19
31
|
(
|
|
20
32
|
# 幂等: 同一 payload 指纹在 60s 内只落一行 (防重复触发)。
|
|
@@ -36,10 +48,29 @@ PAYLOAD=$(cat 2>/dev/null | head -c 500)
|
|
|
36
48
|
SESSION_ID=$(printf '%s' "$PAYLOAD" | sed -n 's/.*"session_id":"\([^"]*\)".*/\1/p' | head -1)
|
|
37
49
|
LOG_DIR="${ABS_LOG_DIR:-$HOME/.abs/log}"
|
|
38
50
|
mkdir -p "$LOG_DIR" 2>/dev/null
|
|
51
|
+
|
|
52
|
+
# 日志轮转: hooks.log 是 append-only 热路径, 无上限会无限长。
|
|
53
|
+
# 用 wc -c 判大小(/bin/sh 无 GNU stat -c); 超限就轮转一份 .1 并截断。
|
|
54
|
+
# 阈值可经 ABS_LOG_MAX_BYTES 覆盖; 只要一份历史(.1), 不做多层——排查靠近期痕迹, 旧的不值当留。
|
|
55
|
+
ABS_LOG_MAX="${ABS_LOG_MAX_BYTES:-1048576}" # 1 MiB
|
|
56
|
+
LOGF="$LOG_DIR/hooks.log"
|
|
57
|
+
if [ -f "$LOGF" ]; then
|
|
58
|
+
SZ=$(wc -c < "$LOGF" 2>/dev/null | tr -d ' ')
|
|
59
|
+
case "$SZ" in ''|*[!0-9]*) SZ=0 ;; esac
|
|
60
|
+
if [ "$SZ" -gt "$ABS_LOG_MAX" ] 2>/dev/null; then
|
|
61
|
+
mv -f "$LOGF" "$LOGF.1" 2>/dev/null || : > "$LOGF"
|
|
62
|
+
printf '[%s] logrotate hooks.log (%s bytes -> .1)\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$SZ" >> "$LOGF" 2>/dev/null || true
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
39
65
|
{
|
|
40
66
|
printf '[%s] %s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$EVENT" "$SHORT"
|
|
41
67
|
} >> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
42
68
|
|
|
69
|
+
case "$BAIL" in
|
|
70
|
+
'{}') ;;
|
|
71
|
+
*) printf '[%s] %s teardown-nudge 注入收尾指令\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$EVENT" >> "$LOG_DIR/hooks.log" 2>/dev/null || true ;;
|
|
72
|
+
esac
|
|
73
|
+
|
|
43
74
|
# Stop 专用: 低噪声 wrap-up 提醒 (独立文件, 不进 per-event hooks.log, 不碰图谱 todo/log.md)。
|
|
44
75
|
# 纯机械信号——"一个会话结束了"; 是否真有滞留任务/经验由 agent 自觉判断, hook 不替做。
|
|
45
76
|
if [ "$EVENT" = "Stop" ]; then
|
|
@@ -54,5 +85,5 @@ PAYLOAD=$(cat 2>/dev/null | head -c 500)
|
|
|
54
85
|
) &
|
|
55
86
|
|
|
56
87
|
# Claude Code 等宿主要求 stdout 以 { 开头才不告警。
|
|
57
|
-
printf '
|
|
88
|
+
printf '%s\n' "$BAIL"
|
|
58
89
|
exit 0
|
package/package.json
CHANGED
package/src/install.js
CHANGED
|
@@ -286,12 +286,16 @@ const server = async ({ client, directory }) => {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// 判定"今天是否收尾过": 必须匹配 log.md 的条目头 '## [YYYY-MM-DD HH:MM]'。
|
|
290
|
+
// 坑: 曾用裸日期 substring(includes('2026-09-10')), 结果正文/任务行里任何一处
|
|
291
|
+
// 提到今天就能把 nudge 永久压掉(误判为已收尾), 守卫形同虚设。
|
|
289
292
|
async function loggedToday(brain) {
|
|
290
293
|
try {
|
|
291
294
|
const txt = await readFile(join(brain, "log.md"), "utf8")
|
|
292
295
|
const d = new Date()
|
|
293
296
|
const pad = (n) => String(n).padStart(2, "0")
|
|
294
|
-
|
|
297
|
+
const today = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
|
|
298
|
+
return new RegExp("^## \\\\[" + today + " \\\\d{2}:\\\\d{2}\\\\]", "m").test(txt)
|
|
295
299
|
} catch { return false }
|
|
296
300
|
}
|
|
297
301
|
|
|
@@ -303,11 +307,24 @@ const server = async ({ client, directory }) => {
|
|
|
303
307
|
"4) abs log \\"完成 X:...\\" 记一行工作成果,新页同步进 index。\\n" +
|
|
304
308
|
"简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\\"无可沉淀\\"即可。"
|
|
305
309
|
|
|
310
|
+
// bash 里只跑查询类命令不算改文件 (与 pi 侧 READONLY_CMD 同义, 但生成代码里要写进模板串)
|
|
311
|
+
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/
|
|
312
|
+
|
|
306
313
|
return {
|
|
307
|
-
// 观测真实写操作: write/edit/patch
|
|
308
|
-
|
|
314
|
+
// 观测真实写操作: write/edit/patch 类工具成功即标记。
|
|
315
|
+
// 坑: 曾漏掉 bash —— opencode 里很多修改是经 bash(heredoc/sed) 完成的, 纯 bash 会话
|
|
316
|
+
// 永远不置位 wroteFiles, 于是收尾提醒静默不发。与 pi 侧 WRITE_TOOLS 保持一致。
|
|
317
|
+
"tool.execute.after": async ({ tool, args }) => {
|
|
309
318
|
const t = String(tool || "").toLowerCase()
|
|
310
|
-
if (["write", "edit", "patch", "multiedit", "apply_patch"].includes(t))
|
|
319
|
+
if (["write", "edit", "patch", "multiedit", "apply_patch"].includes(t)) {
|
|
320
|
+
wroteFiles = true
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
// bash 里只有非只读命令算改文件(ls/cat/git status 之类不算)
|
|
324
|
+
if (t === "bash") {
|
|
325
|
+
const cmd = String((args && (args.command || args.cmd)) || "")
|
|
326
|
+
if (cmd && !READONLY_CMD.test(cmd)) wroteFiles = true
|
|
327
|
+
}
|
|
311
328
|
},
|
|
312
329
|
|
|
313
330
|
event: async ({ event }) => {
|
|
@@ -428,14 +445,19 @@ async function findBrain(cwd: string): Promise<string | null> {
|
|
|
428
445
|
}
|
|
429
446
|
}
|
|
430
447
|
|
|
431
|
-
/**
|
|
448
|
+
/**
|
|
449
|
+
* .brain/log.md 今天有记录吗? 有=已收尾, 不再打扰。
|
|
450
|
+
* 必须匹配条目头 '## [YYYY-MM-DD HH:MM]' —— 裸日期 substring 会被正文里任意一处
|
|
451
|
+
* 今天的日期误命中, 导致"已收尾"误判、nudge 永不触发。
|
|
452
|
+
*/
|
|
432
453
|
async function loggedToday(brain: string): Promise<boolean> {
|
|
433
454
|
try {
|
|
434
455
|
const txt = await readFile(join(brain, "log.md"), "utf8")
|
|
435
456
|
const d = new Date()
|
|
436
457
|
const pad = (n: number) => String(n).padStart(2, "0")
|
|
437
458
|
const today = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
|
|
438
|
-
|
|
459
|
+
// 注意: 这里是真实 TS, 不是生成代码的模板字符串。用拼接形式避免反斜杠逃逸歧义。
|
|
460
|
+
return new RegExp("^## \\\\[" + today + " \\\\d{2}:\\\\d{2}\\\\]", "m").test(txt)
|
|
439
461
|
} catch { return false }
|
|
440
462
|
}
|
|
441
463
|
|
package/src/store.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/store.js — CLI 命令实现:图谱读写层。
|
|
2
2
|
// 命令: init / board / status / load / task / query / lint
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
4
|
+
import { join, resolve, dirname } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { requireBrain, findBrainRoot, brainPath } from './index.js';
|
|
6
7
|
import { addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, moveBlocked, insertDoneGrouped } from './todo.js';
|
|
7
8
|
import { editFile, SKIP } from './lock.js';
|
|
@@ -192,6 +193,70 @@ export async function cmdWrapup({ dir }) {
|
|
|
192
193
|
return appendWrapup(root);
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Stop hook 收尾注入判定 —— CC/Codex 的"主动推"。
|
|
198
|
+
* 与 pi 插件四条件一致: ①本会话真改过文件 ②有 .brain ③log.md 今日无条目 ④每会话一次。
|
|
199
|
+
* 输出给 shell: `push:<json>` = 注入, 其它 = 放行。JSON 走 stdout 回宿主的 decision:block。
|
|
200
|
+
* 所有异常一律放行(`{}`), 绝不因判定失败而卡住用户会话。
|
|
201
|
+
*/
|
|
202
|
+
export async function cmdTeardownCheck({ dir, payload }) {
|
|
203
|
+
try {
|
|
204
|
+
let ev = {};
|
|
205
|
+
try { ev = JSON.parse(payload || '{}'); } catch { ev = {}; }
|
|
206
|
+
|
|
207
|
+
// 条件①: 本会话真改过文件。CC 的 Stop payload 不带工具足迹,
|
|
208
|
+
// 故以 transcript_path 为准; 取不到就退化为"本项目今日是否已有产物"判断。
|
|
209
|
+
const transcript = ev.transcript_path || ev.transcriptPath;
|
|
210
|
+
if (transcript) {
|
|
211
|
+
const txt = await readIfExists(transcript);
|
|
212
|
+
const wrote = /"(Write|Edit|MultiEdit|NotebookEdit)"/.test(txt);
|
|
213
|
+
if (!wrote) return '{}'; // 纯只读会话不打扰
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 条件②: 本项目有 .brain
|
|
217
|
+
const dir0 = dir || ev.cwd || process.cwd();
|
|
218
|
+
let root;
|
|
219
|
+
try { root = await requireBrain(dir0); } catch { return '{}'; }
|
|
220
|
+
|
|
221
|
+
// 条件③: log.md 今日尚无条目(条目头 '## [YYYY-MM-DD HH:MM]')
|
|
222
|
+
const logTxt = await readIfExists(brainPath(root, 'log.md'));
|
|
223
|
+
const stamp = localStamp(); // 'YYYY-MM-DD HH:MM'
|
|
224
|
+
const day = stamp.slice(0, 10);
|
|
225
|
+
if (new RegExp('^## \\[' + day + ' \\d{2}:\\d{2}\\]', 'm').test(logTxt)) return '{}';
|
|
226
|
+
|
|
227
|
+
// 条件④: 每会话一次。
|
|
228
|
+
// 双保险, 因为官方 stop_hook_active 有已知
|
|
229
|
+
// 不传播 bug(claude-code#54360): 同一 turn 内重复 fire 时它仍是 false。
|
|
230
|
+
// ① 官方契约: stop_hook_active=true = 本次 Stop 已是注入后的产物, 绝不再推(否则死循环)
|
|
231
|
+
if (ev.stop_hook_active === true) return '{}';
|
|
232
|
+
// ② 己方节流: 以 session_id 落 mark。缺 id 时退化为按项目+日期节流,
|
|
233
|
+
// 绝不"无节流"——否则一旦宿主张不到 id, decision:block 就会无限自激。
|
|
234
|
+
const sid = String(ev.session_id || ev.sessionId || '').replace(/[^\w-]/g, '');
|
|
235
|
+
const key = sid || 'nosession-' + day + '-' + root.replace(/[^\w]/g, '_');
|
|
236
|
+
const mark = join(homedir(), '.abs', 'log', `teardown-${key}.mark`);
|
|
237
|
+
try {
|
|
238
|
+
await fs.access(mark);
|
|
239
|
+
return '{}'; // 本会话(或本项目今日)已推过
|
|
240
|
+
} catch { /* 未推过 */ }
|
|
241
|
+
await fs.mkdir(dirname(mark), { recursive: true });
|
|
242
|
+
await fs.writeFile(mark, stamp).catch(() => {});
|
|
243
|
+
|
|
244
|
+
const msg = [
|
|
245
|
+
'[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:',
|
|
246
|
+
'1) 跑 abs load 看 Today 还有哪些未完成;',
|
|
247
|
+
'2) 实际做完漏登记的 abs task done <id>,做到一半的 abs task note <id> --note "断点";',
|
|
248
|
+
'3) 值得留的经验 abs note "..."(宁少勿滥,能从代码 grep 到的不记);',
|
|
249
|
+
'4) abs log "完成 X:..." 记一行工作成果,新页同步进 index。',
|
|
250
|
+
'简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句"无可沉淀"即可。',
|
|
251
|
+
].join('\n');
|
|
252
|
+
|
|
253
|
+
// Claude Code Stop hook 契约: {"decision":"block","reason":"..."} = 阻止结束并把 reason 回灌给 agent
|
|
254
|
+
return 'push:' + JSON.stringify({ decision: 'block', reason: msg });
|
|
255
|
+
} catch {
|
|
256
|
+
return '{}'; // 永不阻塞宿主
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
195
260
|
async function readIfExists(p) {
|
|
196
261
|
try { return (await fs.readFile(p, 'utf8')).trim(); } catch { return ''; }
|
|
197
262
|
}
|