# 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。