为了保证完全解耦,Agent-Bridge 所有的通信均依赖于项目根目录下的特定文件结构。任何支持读写本地文件的脚本或 Agent,只要遵循本协议,即可无缝接入本协作网络。
bridge.md 是整个系统的心脏。它必须位于项目的根目录下。
它的结构由顶部的 YAML Frontmatter 与下方的 Markdown 内容 组成。
每个字段都严格定义了当前的流水线状态:
status: PENDING_DEV # 当前状态,见下方七态说明
task_id: T1 # 对应 docs/impl.md 中的任务 ID
round: 1 # 当前处于第几次迭代(被打回则递增)
retry: 0 # 重试次数(预防 API 崩溃死循环用)
updated_at: 2026-09-16T12:00:00.000Z # 最后修改时间
last_commit: abc1234 # 与该状态绑定的 Git Commit Hash
error: null # 如果发生系统级错误,记录于此PENDING_DEV:施工方可获取控制权,准备开始开发。IN_DEV:施工方正在开发中,独占锁定工作区。PENDING_REVIEW:施工方开发完成,等待监工方审查。IN_REVIEW:监工方正在审查代码,独占锁定工作区。REJECTED:审查未通过。施工方在下一个 Tick 被唤醒进行修复。APPROVED:审查通过,当前task_id完成,进入下个任务的PENDING_DEV。COMPLETED:所有任务完成,守护进程自动关闭。
当监工方完成审查后,它必须在 .bridge/verdict.json 中写入裁决结果。看门狗会读取此文件进行状态翻转。
{
"verdict": "approve", // 只能是 "approve" 或 "reject"
"issues": [
// 如果 reject,这里必须填入详尽的打回原因,供 Builder 阅读
"Line 45: Missing TypeScript type definition.",
"Failed to pass 'npm run test'."
],
"task_id": "T1",
"round": 1
}系统会自动在项目下生成 .bridge/ 隐藏目录,用于存放信号文件、裁决文件与锁文件:
review-request.md:Watchdog 组装的审查材料,Supervisor 的唯一输入。verdict.json:Supervisor 输出的裁决结果。signal-builder.txt:GUI 模式下唤醒 Builder 的信号文件。internal-state.json:Watchdog 的内部持久化状态(已审批 commit、已完成任务列表)。logs/:历史审查意见归档。
Agent-Bridge 采用两层互斥机制:底层是操作系统保证的机制锁,上层是角色卡约束的约定锁。
| 文件 | 类型 | 作用 |
|---|---|---|
watchdog.lock |
独占锁(机制锁) | 防止启动多个 Watchdog。第二个实例启动时直接中止并报出持有者 PID。 |
bridge.md.lock |
互斥锁(机制锁) | 保护 bridge.md 的「读—改—写」临界区,防止状态被并发覆盖。 |
builder.pid |
租约(约定锁) | Builder 认领施工权的标记,见下方说明。 |
锁的创建使用 fs.openSync(path, 'wx') —— wx 标志表示「写 + 排他创建」。
该动作由操作系统保证原子性:要么成功创建全新文件,要么抛出 EEXIST。
这是本地文件锁中唯一不存在「先检查再创建」竞态窗口的部分。
陈旧锁的三段式接管(为什么不能简单覆盖)
进程崩溃会残留锁。若锁中记录的 PID 已不存在(通过 process.kill(pid, 0) 探测),
则需判定为陈旧锁并接管。但直接覆盖是错的 —— 多个进程可能同时判定为陈旧并都认为自己接管成功,互斥反而失效。
因此接管必须经过三阶段,每阶段都有一次内核级原子判定:
- 抢占凭证:创建
${lockPath}.reclaim.${token}排他凭证(名字含唯一 token)。 凭证名天然唯一,只有拿到凭证的进程才有资格接管,其余进程明确判负退出。 - 搬运陈旧锁:用
rename将陈旧锁原子搬到隔离区${lockPath}.stale.${tag}, 并校验搬走的字节与判定时完全一致(防 ABA:期间被合法释放并易主)。 - 归属仲裁:此时锁路径为空,对其再做一次
openSync(path,'wx')抢占 —— 这次原子判定是归属的唯一权威。抢到者进入临界区,抢不到者回滚隔离区并判负。
还需处理凭证残留:若凭证持有者在接管途中崩溃,凭证会变成新的陈旧锁, 后续进程需能识别并清理,否则形成新的永久死锁。
损坏锁的宽限期(0 字节问题)
openSync('wx') 成功但内容尚未落盘时进程崩溃,会留下 0 字节锁。
JSON.parse('') 失败后若一律判负 → 永久死锁;若一律判可抢 → 会误抢正在写入的锁。
因此按文件 mtime 年龄区分两种损坏态:
corrupt-fresh(mtime 在宽限期CORRUPT_LOCK_GRACE_MS= 3000ms 内):可能正被并发写入,保守拒绝。corrupt-stale(mtime 超出宽限期):确为崩溃残留,判定为陈旧,允许接管。
同进程重入
markTaskCompleted 会在 writeBridge 的临界区内调用 saveInternalState,后者需要获取另一把锁。
若不做重入处理会形成自死锁。
因此 withLock 使用 AsyncLocalStorage + Symbol.for('agent-bridge.lockQueues') 记录
「本异步上下文已持有哪些锁」:同上下文 + 同锁 → 直接执行临界区,不再排队也不再抢锁。
用 Symbol.for 而非普通 Symbol,是为了让同一 worker 内的多份模块副本命中同一注册表。
该重入按异步上下文判定。不同上下文或不同进程持同一把锁,仍会正常排队互斥。
Builder 通常是 GUI 形态的 LLM 会话(Cursor、WorkBuddy 等 IDE), 其宿主进程号不可稳定获取,因此这个文件在实现上被定义为**租约(Lease)**而非真正的 PID 探针:
{
"pid": 12345,
"type": "builder",
"task_id": "T1",
"claimed_at": "2026-09-16T12:00:00.000Z"
}claimed_at是主要价值:供 Watchdog 判断是否存在长期无人认领的挂死状态。pid是尽力而为的元数据,可能填会话进程号,也可能填unknown。- 该文件由 Builder 在开工时创建、挂起前释放,属于约定锁(依赖角色卡纪律),而非机制锁。
所有共享文件的写入必须经由「临时文件 + rename」完成,禁止直接覆盖写入:
写入 → .<文件名>.tmp.<pid>.<时间戳>.<计数器> (本进程唯一暂存区)
提交 → fs.renameSync(tmp, target) (同卷内原子替换)
失败 → 删除 tmp 文件,不留残留
两级保障缺一不可:
- 临时文件唯一命名保证多个并发写者各自拥有独立暂存区,不会互相覆盖对方的半成品。
rename原子性保证读者永远只能读到「完整旧版本」或「完整新版本」,不会读到半截内容。
注意:
rename只能保证单个文件不撕裂,无法保证「读旧值 → 修改 → 写回」这一组合动作的原子性。 因此bridge.md的状态写入额外由bridge.md.lock互斥锁包裹临界区。
状态机的解析正则依赖 \n 行尾。若 bridge.md 是 CRLF 行尾,正则会匹配失败、
parseBridge() 返回 null,而 tick() 首行直接 return —— 表现为
状态机彻底静摆、零日志、零报错,用户只会看到「什么都没发生」。
这是一个高危且隐蔽的陷阱:在 Windows 上 core.autocrlf=true 会把 LF 转成 CRLF,
因此任何 Windows 用户 clone 后都会触发。
三重防线:
- 仓库层:根目录
.gitattributes声明* text=auto eol=lf,从根源归一化行尾。 - 解析层:
parseBridge()在做任何正则匹配前统一调用normalizeEol(), 将CRLF / CR一律转为LF。 - 兜底层:所有派生正则加
\r?,逐行取值时再replace(/\r$/, '')。
已 clone 过的 Windows 用户需执行一次
git rm --cached -r . && git reset --hard让归一化对已有工作区文件生效。
看门狗要审查「用户项目」的 git 历史(git diff、git log -p、config.checks 里的
npm test 等)。但进程 cwd 取决于用户从哪个目录启动,这是一个隐蔽的故障源:
- cwd 在仓库内(哪怕深层子目录):git 会自动向上查找
.git,命令正常执行。 - cwd 完全不在仓库内:
git rev-parse抛fatal: not a git repository。
旧实现把异常吞掉返回 null,导致 diff 审查静默降级 —— 不报错、只是审查内容为空,
用户看到的是「Supervisor 说没问题」,而不是「审查根本没跑」。
因此所有外部命令统一走 execInProject(cmd):
- 启动期探测一次仓库根:
git rev-parse --show-toplevel,结果缓存。 - 探测成功:后续所有 git 命令与用户自定义 checks 都以该仓库根为
cwd, 不依赖用户从哪启动。 - 探测失败:明确 warn 到控制台,说明 diff 审查不可用、应在仓库内启动。
返回值契约:探测不到仓库根时返回 null(而非空字符串)。
调用方必须区分「项目不可用」(配置问题,需 warn)与「命令成功但无输出」(正常状态)——
旧代码把两者都变成空字符串,正是静默降级的成因。