一套让 AI Agent 自己写代码、自己审查、自己归档的自动化协作协议
一、背景:为什么要做这个?
单个 AI Agent 编写代码时缺少审查环节,写完直接提交,逻辑漏洞、安全隐患和边界情况处理完全依赖其自身,难免疏漏。
很自然的想法是引入两个 Agent 协作:一个负责编码,一个负责审查。但这里存在一个核心矛盾:
-
如果让同一个 Agent 既写代码又自审,由于认知偏差,它无法有效审查自己的工作;
-
如果人工在两个窗口之间拷贝代码,又会丧失自动化的意义。
这套架构给出的答案:三次提交、两个 Agent、零人工介入。
| 角色 | 做什么 | 工具 |
|---|---|---|
| Hermes | 拆需求 → 创建工作间 → 归档 | Hermes Agent |
| CC (Claude Code) | 写代码、commit | Claude Code CLI |
| Codex (Codex CLI) | 审查代码、commit | OpenAI Codex CLI |
二、架构总览
整个系统分为两层:
text
┌─────────────────────────────────────────────────┐
│ 内容层(skills/) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Hermes │ │ CC │ │ Codex │ │
│ │ skills │ │ skills │ │ skills │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ 谁做什么、不做什么、怎么提交 │
├─────────────────────────────────────────────────┤
│ 稳定层(scripts/) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ git hook │→│ route- │→│ inject │ │
│ │ │ │ agent │ │ target │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ 路由硬编码,一次写好不动 │
└─────────────────────────────────────────────────┘
核心原则:稳定层与内容层分离
为什么要这样分层?
Agent 推理并不可靠------会遗漏步骤、跳转逻辑甚至产生幻觉。如果把流转逻辑写在技能文件里让 Agent 自行推理,每次执行都可能出现不同的行为。
因此,我们将路由从技能中剥离,写死在稳定层(hook → route-agent → inject)。Agent 只需回答"我是谁、我要干什么、我不能干什么",不参与流程决策。
| 纯技能方案 | 分层方案 |
|---|---|
| 流转逻辑写在技能里,依赖 Agent 推理 | 路由硬编码在 route-agent |
| Agent 可能遗忘或跳过步骤 | hook + 代理机制,不会遗漏 |
| 一个文件混入身份、流程和路由 | skill 只描述身份和能力边界 |
三、角色定义
Hermes --- 规划者 + 归档者
Hermes 是交互式 CLI(也就是你正在使用的这个 Agent),负责三件事:
-
Plan(拆需求):接收大任务,分析需求,拆分成独立的工作间(workspace)
-
Create-workspace(搭建):创建目录、编写 STATUS.md、建立 Git 分支
-
Archive(归档):在 CC↔Codex 循环结束后,将最终成果写入存档
Hermes 不介入 CC↔Codex 的编码与审查循环。
CC(Claude Code)--- 写代码的人
-
读取 STATUS.md 了解任务与约束
-
编写代码,执行
git commit(提交信息格式:[CC-WSxxx] 做了什么) -
更新 STATUS.md 状态为
waiting-review -
如果接到 REVIEW-FAIL,修复后重新提交
CC 不负责审查代码、归档结果,也不调度其他 Agent。
Codex(Codex CLI)--- 审查代码的人
-
读取 STATUS.md +
git diff了解变更内容 -
审查:功能完整性、逻辑正确性、安全性、约束满足情况
-
给出结论:
-
REVIEW-PASS → 状态改为
completed -
REVIEW-FAIL → 状态改为
waiting-fix,并写明具体问题
-
Codex 不编写业务代码(可进行少量修补),不执行归档,也不创建工作间。
四、工作间生命周期(完整流程)
text
Hermes: 大任务 → ws001 / ws002 / ws003
│
├── create-workspace
│ ├── 建目录 ~/.project-archive/projects/<项目>/workspaces/<wsxxx>/
│ ├── 写 STATUS.md(任务 + 约束 + 状态 = created)
│ └── git checkout -b ws/ws001-xxx
│
├── CC 开始写代码
│ ├── 状态 = in-progress
│ ├── 写代码 → git commit -m "[CC-WS001] 实现JWT"
│ └── 状态 = waiting-review
│
├── [git hook 触发 → route-agent → 注入 Codex 窗口]
│
├── Codex 审查
│ ├── 状态 = in-progress
│ ├── git diff HEAD~1 HEAD
│ ├── REVIEW-FAIL "token过期校验缺失"
│ │ ├── git commit -m "[Codex-WS001] REVIEW-FAIL: ..."
│ │ ├── 状态 = waiting-fix
│ │ └── [hook 触发 → 注入 CC 窗口 → 回到修复循环]
│ │
│ └── REVIEW-PASS "审查通过"
│ ├── git commit -m "[Codex-WS001] REVIEW-PASS"
│ └── 状态 = completed
│
└── [hook 触发 → 通知 Hermes 归档]
└── Archive
├── 读 STATUS.md 迭代记录
├── git log --oneline / git diff --stat
└── 写归档文件到 archive/YYYY-MM-DD--feat--xxx.md
五、STATUS.md --- 工作间通信中枢
CC 和 Codex 不直接对话,只读取 STATUS.md 中的「状态」字段来决定行动,路由工作由 route-agent 负责。
text
# ws001 — JWT 认证
## 任务
实现 JWT 登录签发
## 约束
使用 pyjwt,token 有效期 24h
## 状态
in-progress / waiting-review / waiting-fix / completed
## 迭代记录
| 轮次 | 提交者 | 结果 | 详情 |
|------|--------|--------------|--------------------------|
| 1 | CC | 完成 | 实现 JWT 登录签发 |
| 2 | Codex | REVIEW-FAIL | token 过期校验缺失 |
| 3 | CC | 完成 | 修复: 增加过期校验 |
| 4 | Codex | REVIEW-PASS | 审查通过 |
状态流转图
text
created → in-progress → waiting-review → waiting-fix → in-progress → waiting-review → completed
↑ ↑ CC 开始写 ↑ CC commit ↑ Codex FAIL ↑ CC 修复 ↑ CC commit ↑ Codex PASS
|
Hermes 初始
话筒规则很简单:状态字段是唯一的指示器,谁看到状态就知道该谁干活。
六、路由机制------事件驱动,不依赖推理
最精巧的设计在于:整个 CC↔Codex 循环完全不依赖任何 Agent 的推理能力。
text
CC commit -m "[CC-WS001]..."
│
▼
post-commit hook(.git/hooks/post-commit)
│
▼
curl POST /event → route-agent(HTTP 后台服务)
│
▼
route-agent 硬编码路由决策:
[CC-WSxxx] → 找 Codex 窗口 → inject-target.ps1 → Codex 终端
REVIEW-FAIL → 找 CC 窗口 → inject-target.ps1 → CC 终端
REVIEW-PASS → 找 Hermes 窗口 → 注入归档提示
hook 不做任何逻辑处理,只负责发送 curl(<100ms,不影响 Git 体验),所有路由逻辑集中由 route-agent.py 处理。
七、关键技术细节
7.1 终端注入方案
把消息"打"进另一个终端窗口,比看上去复杂得多。经历了三次方案迭代:
| 方案 | 结果 | 原因 |
|---|---|---|
| SendInput | ❌ 失败 | prompt_toolkit 不走 Console API |
| WriteConsoleInput | ❌ 失败 | Windows Terminal 不是传统控制台 |
| AttachThreadInput + keybd_event | ✅ 成功 | 绕过前台限制,模拟键盘硬件事件 |
最终方案:
-
AttachThreadInput将当前线程附加到目标窗口线程(绕过前台限制) -
设置剪贴板文本
-
keybd_event发送 Ctrl+V(粘贴) -
keybd_event发送 Enter(提交) -
分离线程
并发注入通过命名互斥量 Global\HermesInjectMutex 排队,超时 60s 保护。
7.2 HWND 管理
Claude Code 启动后会将窗口标题自行更改为 ✳ Claude Code,覆盖 wt.exe --title 参数,因此无法依靠标题查找窗口。
应对策略:启动时捕获,后续直接读取
text
spawn-agent.py 创建窗口
→ find-window.ps1(UIAutomation)捕获 HWND
→ 保存至 ~/.hermes/hwnd-{name}-{workspace}.txt
→ POST /register 到 route-agent
后续注入(route-agent 收到事件后):
→ 从 ~/.hermes/hwnd-{target}-{workspace}.txt 读取 HWND
→ inject-target.ps1 -HWND <hwnd> -Message "prompt"
→ 背景注入,不切换焦点
7.3 Windows Terminal 启动命令的坑
不要用 powershell -Command "& 'exe'" ------PowerShell 的 & 语法在 Windows Terminal 的命令解析中会被当作可执行路径名,导致 0x80070002 错误。
正确写法:
text
wt.exe new-tab --title "claude - ws001" -- cmd /k "cd /d <project> && claude --permission-mode bypassPermissions"
八、文件清单(脚本层)
所有脚本位于 project-archive/scripts/:
| 文件 | 作用 |
|---|---|
route-agent.py |
HTTP 代理后台服务(仪表盘 localhost:8765) |
spawn-agent.py |
创建 Agent 终端窗口并注册 HWND |
inject-target.ps1 |
通用注入脚本(HWND 注入 + 剪贴板 + 互斥量) |
find-window.ps1 |
UIAutomation 窗口查找 |
post-commit.py |
git hook 脚本,curl POST /event |
install-hook.sh |
将 hook 安装到项目 |
start-task.sh |
启动入口 |
hermes-archive-prompt.txt |
归档提示文本 |
技能文件位于 project-archive/skills/:
| 文件 | 作用 |
|---|---|
hermes/hermes-plan.md |
拆需求规范 |
hermes/hermes-create-workspace.md |
创建工作间规范 |
hermes/hermes-archive.md |
归档规范 |
hermes/hermes-recall.md |
历史检索规范 |
claude-code/SKILL.md |
CC 开发规范 |
codex/SKILL.md |
Codex 审查规范 |
workspace.md |
STATUS.md 格式规范 |
九、实测验证
已验证的链路
-
spawn → 捕获 HWND → 注入 hello ✅
-
spawn-agent.py 创建 CC 窗口
-
find-window.ps1 捕获 HWND(如
5507670) -
保存至
~/.hermes/hwnd-CC-ws001.txt -
inject-target.ps1 通过 HWND 直接注入成功
-
-
后台注入 ✅
-
AttachThreadInput 背景注入,不激活窗口
-
窗口收到消息并正常响应
-
-
并发排队 ✅
-
3 个注入同时启动
-
命名互斥量排队,顺序执行
-
待验证
-
CC → Codex 完整对证循环(commit → hook → route-agent → inject → Codex 窗口)
-
Codex REVIEW-FAIL → CC 修复循环
-
Codex REVIEW-PASS → Hermes 归档
十、Pitfalls 与教训
Windows 平台特殊问题
-
PS1 文件必须使用 CRLF 行尾 ------PowerShell 5.1 对纯 LF 解析会报
MissingCatchOrFinally -
Add-Type 的
"@定界符必须在列 0------不能有缩进 -
PowerShell 函数不能在 if/else 内定义------必须写在脚本顶层
-
MSYS bash 向 PowerShell 传参 ------注意
/路径解析,使用临时 JSON 文件避免 UTF-8 编码问题
架构层面
-
STATUS.md 是唯一的通信渠道------CC 和 Codex 不应通过其他方式沟通
-
hook 只做 curl,不处理逻辑------所有路由由 route-agent 统一处理
-
route-agent 需要后台常驻 ------仪表盘位于
localhost:8765,便于调试 -
Agent 启动后必须 POST /register------否则 route-agent 不知道注入目标
-
不要使用 --resume------每次 spawn 都创建新的交互式会话,进入项目目录后再启动
十一、总结
这套架构的核心设计决策可归纳为三句话:
-
路由硬编码,不走 Agent 推理------hook → route-agent → inject 构成的稳定层固定不变,Agent 技能不参与流程决策
-
STATUS.md 即为话筒------状态字段是唯一指示器,谁看到状态就知道该谁行动
-
Hermes 负责头尾,不介入中间循环------Plan → Create-workspace → Archive,中间的 CC↔Codex 循环自主运转
剩下的部分全部是机械执行:Claude Code 写代码,Codex 审查,git hook 触发,route-agent 路由,注入脚本传递消息------没有人为判断,没有 Agent 推理,每一步都确定无疑。