supervisor-design-comparison.md 12.3 KB

Deploy Supervisor 设计方案对比:自建 vs 复用 Hermes CLI

日期: 2026-07-30(同日更新:完成命名消歧,见下方说明;结论已回填正式 SDD) 性质: 方案选型分析,回应用户 "都不确定,先看两种方案的详细对比",已回填 SDD/contract.json(见 supervisor-auto-rollback-design.md §8)。 输入: 对 graphloop(同一项目前身,/Users/yibo/Documents/lemonAI/graphloop)的实地代码/任务记录调查。

命名说明:本文档讨论的"Deploy Supervisor"是独立于 Graph Engine 之外、负责 graphlemon 自我修改后蓝绿部署与自动回滚的进程,和架构文档里已有的"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"是两个不同的概念,详见 supervisor-auto-rollback-design.md 开头的命名说明。本文档下文提到"supervisor"泛指这个待选型的组件本身,涉及 graphloop 自己代码里的 hermes_watchdog.py/phase10_controller.py 时沿用 graphloop 原始命名,不额外改名。

0. 结论先说

问题不是"自建 vs 用 Hermes CLI 当 Deploy Supervisor 的大脑"这个二选一——graphloop 自己的实现已经给出了答案,而且答案跟两个选项字面意思都不完全一样:

  • graphloop 的 supervisor 层(hermes_watchdog.py + phase10_controller.py)自身的监督/裁决逻辑是纯 stdlib 确定性 Python,完全不调用任何 LLM(Hermes 或 Claude 都不调)。LLM 只用来执行"被监督的那份工作",监督决策(存活判断、是否 blocked、evidence 是否齐全)是 if/else,不是 prompt。
  • 但 graphloop 的监督哲学是 "fail-closed 后停下等人工",不是用户要的 "自动回滚后继续跑"。这是个真实的哲学缺口,两个候选方案都没有现成覆盖。
  • graphloop 已经有一套经过真实运行验证(含真实失败案例)的隔离/manifest/选择性写回机制,直接复用价值很高,且这部分代码同样是纯 stdlib、和"用不用 Hermes CLI"这个问题正交。

所以推荐路径不是"选 A 还是选 B",而是"复用 C(graphloop 的隔离/manifest/apply 库)+ 在其之上新写一个自动回滚层",详见第 4 节。

1. graphloop 里跟 supervisor 直接相关的既有实现

1.1 hermes_watchdog.py(109 行,已读全文)

纯 stdlib,无 LLM 调用。核心逻辑:

inspect(state_dir, stall_seconds=600.0, action="report"|"terminate"):
  扫描 state_dir 下 status=="running" 的 *.json 状态文件
  用 last_output_at/updated_at 或日志 mtime 算 silent_for
  if stalled and action=="terminate":
      os.kill(pid, SIGTERM)
      原子写 {"status": "blocked", "blocked_reason": "worker dead or silent beyond watchdog threshold"}

模块 docstring 原话:"The watchdog never restarts a task. A dead or silent worker is terminated once, marked blocked, and left for an explicit recovery decision." —— 这是关键:graphloop 的哲学是"发现异常 → 杀掉 → 标记 blocked → 停下等人",不是"发现异常 → 自动回滚 → 继续跑"。

1.2 phase10_controller.py(自我演进调度器,已读核心结构)

同样纯 stdlib,无 LLM 参与调度决策本身:

main():
    acquire_tick_lock()  # 单飞锁,防止并发 tick
    读 progress.json 找 current_task
    if 上一个任务的 worker PID 不存活: mark_stale() → blocked_stale,停
    if task_id 在 blocked_steps 或历史记录里是 failed/blocked: blocked_task,停
    completion_is_admissible(): 校验 evidence dict 是否包含必需 key(specialized_test/py_compile/full_regression),
        blockers 字段非空则直接判不可采信
    只有 evidence 完整且无 blocker 才推进 phase,否则原地 blocked,绝不自动重试/自动回滚

调度器本身只负责"要不要放行这一次 tick",真正干活的是它 shell 出去的 hermes chat --yolo ...。

1.3 隔离 / manifest / 选择性写回(CLAUDE.md 架构章节 + 已读代码路径)

这是最值得直接复用的部分,和"用不用 Hermes CLI"完全无关,纯 stdlib:

  • isolated_coding_workspace.isolate_workspace(source_dir):tempfile.mkdtemp 复制,源目录永不直接改动
  • workspace_security.validate_artifacts():拒绝绝对路径、..、逐段检查 symlink(防 TOCTOU 逃逸)、最终 realpath+commonpath 校验
  • workspace_security.validate_subprocess_command():只接受 [python可执行文件, 已存在脚本, ...],拒绝 shell 元字符(shell=False 之外的纵深防御)
  • change_manifest.py:源 vs 隔离副本的 SHA-256 diff,生成 changed_files manifest
  • selective_apply.apply_manifest():唯一写回真实源码的入口,校验每个 approved path、双向拒绝 symlink 逃逸、写入前先读全部字节(stage-before-commit)、tempfile.mkstemp+os.replace 原子写
  • run_graph_task.py 的 SafetyArgumentParser:--apply 必须搭配显式 --approve-path/--approve-all,--approve-all 不带 --apply 直接拒绝——这是人工审批卡点,不是自动的

这套机制直接对应 graphlemon self-development-feasibility.md 缺口 1(worktree 隔离强制)的解法,而且是已经在真实失败场景里验证过边界的实现(见下一节)。

2. 真实失败证据(tasks/phase10-progress.json)及其含义

graphloop 自己跑 Phase 1-10 自我演进时卡在 Phase 2(phase10_ready: false),留下三个具体故障,每个都直接对应 graphlemon 架构里的一个假设:

故障 现象 对 graphlemon 的启示
plan-schema-001 超时 Hermes worker 300s 未完成,判 failed 03-C 里 max_step_timeout/c_work_node_run 超时参数不能拍脑袋定小了;graphloop 用的是 300s 默认值,实测不够
step-fanout-001 隔离被击穿 Hermes worker 把实现文件直接写进了源工作区而不是隔离工作区;隔离 manifest 里没有任何变更,controller 判定"证据不可采信",清理了源目录里两个未授权文件,恢复快照 最关键的一条:即使明确要求"在隔离工作区里改",执行 agent(无论 Claude 还是 Hermes)仍可能不遵守,隔离必须由 supervisor/controller 层做外部校验("我检查你改了哪些文件、改在哪"),不能只靠 prompt 里一句"请在隔离环境里改"。这直接印证了 self-development-feasibility.md 缺口 1 的判断,而且不是理论推测,是实测复现
worker 状态陈旧(PID 不存活但状态仍是 running) active_run.status="stale",controller 判定 stale_reason="recorded worker PID is not alive" 印证了需要独立于被监督进程之外的存活探测(os.kill(pid,0) 这类外部判活),不能依赖被监督进程自己上报"我还活着"

3. 关键缺口:graphloop 的模型是"停下",graphlemon 要的是"回滚后继续"

用户在缺口 3 的更正原话:"改坏了的 validator 没有人工评审倒不是问题,坏了由 supervisor 能够监督回滚即可"——这要求 Deploy Supervisor 具备检测到问题后自动执行回滚动作、并让系统恢复到可继续工作的状态的能力。

graphloop 现有两处监督实现(hermes_watchdog.py、phase10_controller.py)都止步于"标记 blocked,等人工",没有"自动回滚到上一个已知良好版本并恢复运行"这一步。这不是实现细节缺失,是设计哲学的差异:

  • graphloop:安全边界 = 停下(fail-closed to a halt)
  • graphlemon 要求:安全边界 = 回滚到已知良好状态后继续(fail-closed to a rollback, then resume)

后者严格来说更难——"回滚"在 graphloop 的场景里相对简单(还没 apply,源码没动),但在 graphlemon 的蓝绿实例场景里,"回滚"意味着:停正在跑坏的新实例 → 把主分支 revert 或切回上一个 tag → 重新拉起旧版本实例 → 确认健康 → 恢复调度。这一段目前两个项目都没有代码。

4. 方案对比

维度 方案 A:全新自建 Deploy Supervisor 方案 B:Hermes CLI 直接当"大脑" 方案 C(推荐,已采纳):复用 graphloop 隔离/manifest/apply 库 + 新写确定性回滚层
隔离机制 从零写,风险是重新踩 graphloop 已经踩过的坑(symlink 逃逸、TOCTOU) 同左,Hermes CLI 本身不提供这层,还是要自己写 直接复用 isolated_coding_workspace.py/workspace_security.py/change_manifest.py/selective_apply.py,已验证、纯 stdlib、零外部依赖
存活/异常检测 从零写 同左 复用 hermes_watchdog.py 的判活逻辑(os.kill(pid,0) + staleness),改 action 分支
决策逻辑是否含 LLM 看怎么设计 是(如果真把 Hermes CLI 当决策者,等于让一个不确定性的 LLM 做安全关键判断) 否——沿用 graphloop 的判断:安全关键的"要不要回滚"必须是确定性代码,不能是 LLM 判断
自动回滚能力 需要新写(工作量:中,蓝绿切换脚本 + 健康检查 + revert 逻辑) 需要新写,且如果决策链路里有 LLM 调用,回滚这种要求"确定性、可审计、不能出错"的动作反而更不适合让 LLM 参与 需要新写(同方案 A 工作量),但隔离/apply 部分零重复造轮子,净新增工作量最小
与 Graph Engine 的独立性 完全独立,符合用户要求"第三方 agent" 完全独立(Hermes CLI 本来就在另一套工具链) 完全独立;graphloop 的 watchdog/controller 本来就是独立进程,架构上现成符合"third-party supervisor"要求
已验证程度 零(全新代码) 部分(graphloop 已用 Hermes CLI 跑了 Phase 1,但 Phase 2 卡住暴露隔离被击穿的问题——说明"用 Hermes CLI 执行任务"这条路径本身有已知未解决的可靠性问题) 隔离/apply 部分已验证(含真实失败复现);回滚部分未验证(新代码,但范围小)
与既有决策的一致性 中性 有冲突:graphlemon 已明确决定 MVP 只用 Claude Code ACP 做 Org Agent 执行后端,不引入 Hermes CLI 执行任务;若 supervisor 内部又跑 Hermes CLI 去执行"诊断/回滚"这类任务,等于变相引入了第二个执行后端,需要重新论证 一致:只复用 graphloop 里不调用任何 LLM 的确定性 Python 模块(隔离/manifest/apply/watchdog 判活逻辑),不引入 Hermes CLI 作为执行后端,符合"只用 Claude Code ACP"的既有决策边界

5. 推荐

方案 C:把 graphloop 的 isolated_coding_workspace.py / workspace_security.py / change_manifest.py / selective_apply.py 四个模块(纯 stdlib,无 Hermes/Claude 依赖)作为库直接搬到 graphlemon 里复用或轻改,解决 self-development-feasibility.md 缺口 1;hermes_watchdog.py 的判活/staleness 检测逻辑作为参考实现复用,但把它的终态从 blocked(停下等人)改造成新的 blocked → 尝试自动回滚 → 回滚成功则恢复 running → 回滚失败才真正 blocked_pending_human 状态机,解决缺口 2(蓝绿实例切换)并落实用户对缺口 3 的更正(自动回滚代替强制人工评审)。

不建议把 Hermes CLI 作为 supervisor 决策链路的一部分:graphloop 自己的实践已经证明——(a) 安全关键判断在其自身架构里也是纯确定性代码,从不假手 LLM;(b) 用 Hermes CLI 执行被监督的任务这条路径本身有实测的可靠性问题(隔离被击穿);(c) 引入 Hermes CLI 会和"MVP 只用 Claude Code ACP"这条已确认决策产生冲突,需要重新开一次论证。

6. 决策记录(2026-07-30,用户已逐条确认)

  1. 同意方案 C 方向:"同意方案 c 方向,只在这一个环节复用,问题不大"——只复用隔离/apply 库这一部分,不引入 Hermes CLI 到决策链路。
  2. 复制而非依赖:"复制一份到 graphlemon 仓库改造,那边已经不维护了"——四个模块已复制到 supervisor/isolation/ 并按 graphlemon 自身约定改造(详见该目录 __init__.py 的来源说明),不对 graphloop 仓库产生 import 依赖。
  3. 自动回滚状态机现在就设计:"自动回滚机制现在就设计一下,省得到 ph5 阶段有那个阶段的问题"——已完成,见 supervisor-auto-rollback-design.md,并已回填正式 SDD。