fresh-eyes-loop · 质量循环定义
一个循环 = 一轮又一轮的"独立审查 → 修复 → 验证",直到干净为止。
这不是检查清单,是一套让独立性可被重复执行的机制。每一轮都用全新 session 跑(零上下文),所以"作者自己看不出问题"这个人类弱点被结构性消解。
这是什么
一套可复用的质量循环定义。它描述:谁来做(单盲四角色:A 审 / B 修 / C 验 / D 复核)、每一轮怎么走(审查 → 修复 → 验证 → 复核)、什么时候停(连续 2 轮无 P0/P1)、产物放哪(runs/YYYY/MM/DD/run-NN/)。
- A = 审查者(单盲):独立跑 12 视角审查;发现的质量由下游 C 验收 / D 复核把关(legacy 双盲 B 并行审查走
FORGE_ENABLE_B_CHECK=1逃生门)。 - B/C/D = 工程师执行修复(现行 B 侧为复核模式——独立复核 A 的 P0/P1,可推翻可补充)/ 验收者逐条实测验收(不采信修复自报)/ 复核者对 P0/P1 裁决 CONFIRM / DOWNGRADE / REOPEN。
- driver(编排进程,非 agent):在角色间中转、维护
runs/文件、判定停止条件。由用户手动新开的执行 session 启动(见下「执行载体铁律」)。
怎么用
- 读
loop.md拿到完整 SOP(角色 / 轮次协议 / 产物 schema / 停止条件)。 - 12 视角的定义见
FORGE/playbook/fresh-eyes-review.md(A 按它跑)。playbook 共 22 视角六层:1-12 常规发版(driver 循环标准配置)、13-14 文档治理(手动)、15-16 全文档通读(手动/草稿工具)、17-19 动态面(跨组件契约/构建产物/执行证据——需跨包追踪或 build/实跑取证,DSH worker 无工具面暂不纳入 driver,发版审查建议手动追加)、20-21 深度专项(季度全仓体检)、22 发现面(门面改动时)。loop 与工具的视角边界以 playbook 分层表为准——playbook 演进(如新增视角/调整分层)时,本文件与下游工具同步对齐。 - 四角色的行为指令在
prompts/(a-check / a-consolidate / b-fix / b-audit / c-verify / d-review;b-check 为 legacy 双盲逃生门FORGE_ENABLE_B_CHECK=1时启用)。 - b-audit 步骤:b-fix 改完代码后 driver 自动跑
sofagent-audit --diff——审计每次变更,dogfooding 铁律。audit FAIL(exit 2)打回 b-fix 重修,不进 c-verify。 - 跨 run 的永久索引在
FORGE/LEDGER.md(被 git 跟踪);每轮正文在runs/(不进 git)。
实现载体
A/B 由 Node driver(FORGE/src/fresh-eyes-driver.mjs)驱动——每个 step 独立子进程(真零上下文),LangGraph createReactAgent 编排。driver 由用户手动新开的执行 session 启动并监控(见下「执行载体铁律」)。
🔴 执行载体铁律:driver 必须由「独立 session」直跑,禁止主 session 内开子代理代跑
fresh-eyes driver 的执行 session 必须是用户手动新开的独立 session(与主 session 平行、互不嵌套),不是主 session 里 spawn 的 subagent。
原因(与 release-gate-loop 实证同机理):主 session 内子代理 → 后台 shell → driver 三层嵌套,用户打断主 session 时级联 SIGTERM 会杀掉整棵进程树——fresh-eyes 一轮多轮循环跑 1-2 小时,中途被级联中止的代价更大。此外子代理自带 token 开销与误诊风险。
正确分工:
- 主 session(审查/决策 session):三查 → 修复环境问题 → 产出「交接 prompt」交给用户(直接在对话中输出可复制的 prompt 文本块,禁止落盘成文件——2026-08-30 用户拍板) → 用户在新 session 粘贴执行 → 等回报 → 零信任复验。主 session 全程不 spawn driver。
- 执行 session(用户新开):粘贴交接 prompt → 按下方「Session 监控协议」启动 driver 并轮询到终态 → 回报结果(轮数 / 停止原因 / 最终 P0/P1/P2 计数 / runDir)。
Session 监控协议(CRITICAL · 适用于执行 session)
启动 driver 后,session 不是傻等,而是进入 sleep 轮询模式——保持 working 状态,让用户感知"后台在干活"(每 120 秒一轮,读 status.json 输出一行状态——session 一直活跃 = 用户界面持续可见「在跑」,硬要求非可选)。
🔴 前台/后台分界铁律:
run_in_background: true只属于启动 driver 的那一条 Bash 命令——启动之后的每一轮轮询(sleep + cat status.json)都是前台短命令,直接在 session 正常工作流里执行。严禁把轮询循环本身挂到后台(run_in_background / nohup 均禁)——挂后台 = session 空闲等通知 = 用户界面看不到任何进展反馈,轮询的全部意义(session 可见性)即被摧毁。
🔴 启动前独占窗口检查
启动 driver 前,必须确认本仓库当前没有其他写操作会话在跑——审查 worker 与主仓共享工作目录,git 基线被并发改写(restore 重建 / 回补 / 大批量 commit)会直接杀死进程树,且无终态事件可查。
检查项(30 秒):
- 问用户:「现在有没有别的会话在这个仓库做 restore / 回补 / 批量提交?」
git status --porcelain | head -5——大量未预期改动 = 有并发写,暂停启动- 确认无人动 git 后再启动 driver
git worktree 隔离落地后本检查降级为提醒项——worker 届时跑在隔离副本上,主仓并发写不再致命。
执行方式
1. Bash(⚠️ 必须加 run_in_background: true + dangerouslyDisableSandbox: true,否则三层进程嵌套会被 sandbox SIGKILL):
node FORGE/src/fresh-eyes-driver.mjs --target <版本号> --max-rounds 10
并发自适应:未显式设置 FORGE_MAX_CONCURRENCY 时 driver 自动
探测物理内存取并发(<12GB→1 / 12-23GB→2 / 24-47GB→4 / ≥48GB→6)——
8GB 机器自动取 1(防 OOM),无需手动设。运行中 worker OOM(SIGKILL)
自动熔断降级(本批剩余串行,连续 2 批回退 1,不中止 run)。
🔴 铁律一:必须 dangerouslyDisableSandbox。
原因:driver(spawn) → worker(spawn) → run_bash(execSync) = 三层子进程嵌套。
sandbox 对进程嵌套层数有限制,第 4 层进程返回时整棵进程树被 SIGKILL。
🔴 铁律二:禁止用 nohup+disown 启动——WorkBuddy 会清理脱离 session 的后台进程。
必须用 Bash 工具的 run_in_background: true(安全替代方案)。
2. 记住 runDir(driver 启动日志第一行会打印)
3. 循环(最多 60 次,防 turn 超限——fresh-eyes 一轮可跑 1-2 小时,20 次×5 分钟容量不足。
🔴 本循环是前台操作:session 直接依次执行 sleep/cat——不包 run_in_background、不包任何后台化包装):
sleep 300 # 等 5 分钟(前台)
cat <runDir>/status.json # 读进度(前台)
判断:
- phase === "completed" 或 "error" → 汇报最终结果,退出循环
- heartbeat 超 90s 未更新 → ⚠️ 疑似 driver 死亡,检查进程存活(见下)
- phase 跟上次相同(无变化) → 静默,继续下一轮 sleep
- phase 有变化 → 一句话汇报,继续 sleep
🔴 Heartbeat 死亡检测
driver 被 SIGKILL(sandbox 回收 / OOM / 环境冲突)时,所有 Node handler 都来不及执行,status.json 停在上一次状态,监控端无法区分"在跑"和"已死"。
解法:driver 每 15s 更新 status.json 的 heartbeat 字段。监控端发现 heartbeat 超过 90s 未更新 → 大概率 driver 已死,用 pgrep 确认:
pgrep -f "fresh-eyes-driver" # 有输出=活着,无输出=已死
如果确认已死:读 latest.json 的 stopReason(若有)+ roundDir 内 worker-alive.json 的停更时间(区分 driver 死 / 整树死),汇报后退出监控。
🔴 产物真实性抽验(防占位报告冒充进度)
报告数量增长 ≠ 有效产出。占位报告(崩溃降级占位、骨架未回填)历史上出现过,监控时用一条命令抽验:find <roundDir> -name 'check-*.md' -size -1k(1KB 以下 = 疑似占位),命中即 cat 验内容。若收口时骨架仍未回填,该视角发现已丢失——按修复批协议补跑对应视角的 check worker(不必全量重跑)。
🔴 中止 run 的 LEDGER 归档铁律
任何原因中止的 run(进程死亡 / 人工 kill / 环境冲突)也必须在 LEDGER 留一行——「没有终态记录」的 run 是审计黑洞,事后只能靠时间线推理死因。监控端在确认 driver 死亡后人工补行(driver 侧 SIGTERM handler 兜底归档属 FORGE 隔离加固范围;落地前靠监控端人工补行,本节即 SOP):
日期 | <runId> | fresh-eyes | <实际轮数>* | <P0> | <P1> | <P2> | aborted-<死因简述>(有效产出说明) | <runDir 绝对路径>
汇报规则
- 只在 phase 变化时说话——同一状态不重复汇报
- 一句话——不展开 details,用户想看细节自己读 status.json
- 格式示例:
📊 Round 2 完成 — ❌ P0=1 P1=3,进入下一轮 - 最终结果用 2-3 行收尾:轮数 + 停止原因 + 最终 P0/P1/P2 计数
为什么不用 CLI 推送
driver 写 status.json 就够了——session 自己来读。推变拉,codebuddy-reporter 适配器已废弃。driver 不需要知道 session 的存在。
循环级演化
evolution.md 记录对这套循环本身的改进建议(人类门控的"加一减一"),防止 specs 越长越烂。