feat: dual execution model (SSH CLI + HTTP pull)

- ExecutionMode enum: SshCli (orchestrator dispatches) | HttpPull (agent pulls)
- SSH CLI executor: spawn remote agents via ssh + CLI template
- Local subprocess as SSH special case (localhost)
- HostConfig with capability matching and load-based selection
- Dispatch loop: scan created tasks → select host → execute → update
- CliAdapterConfig: CLI templates for Codex and Claude Code
- Structured prompt construction (Issue → goal/constraints/validation)
- Output parsers: Codex JSON, Claude Code JSON, raw fallback
- TaskStatus::ReviewPending + review_count loop limit
- Forgejo webhook: pull_request (opened→review_pending, merged→completed)
- Forgejo webhook: push events (task/* branch → last_activity_at)
- HTTP API: dequeue only returns http_pull tasks
- HTTP API: status update only for http_pull mode
- Token auth config for http_pull agents
- Adapter module rewritten: AgentAdapter trait removed → config-driven CLI templates
- New fields: execution_mode, assigned_host, branch_name, pr_title, last_activity_at, review_count
- 30/30 tests pass
This commit is contained in:
Zer4tul 2026-05-12 14:07:56 +08:00
parent 1bc7580ecc
commit e39a16498c
34 changed files with 2541 additions and 1555 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-12

View file

@ -0,0 +1,107 @@
## Context
当前 adapter interface 基于 spawn 本地进程模式无法满足跨机协同的核心目标。实际使用中暴露了通知机制不可靠的问题Codex 完成任务后 Jeeves 收不到通知)。
核心认知转变:
- **Orchestrator 不调用 AgentAgent 调用 Orchestrator**pull 模型)
- **Git/Forgejo 是状态追踪的 source of truth**PR 生命周期 = 任务生命周期)
- **通知 = Git 事件**push → 进度信号PR opened → 完成通知PR merged → receipt 验证)
## Goals / Non-Goals
**Goals:**
- Agent 通过 HTTP API 主动拉取任务、更新状态、提交 receipt任何机器、任何语言
- 利用 Forgejo PR webhook 作为可靠的状态追踪和通知机制
- 消除"Agent 完成但无人知道"的问题
- Agent client 可以是任何语言实现curl 就能交互)
**Non-Goals:**
- 不实现 Agent 侧的 client SDKAgent 自己决定用什么语言/方式调用 HTTP API
- 不实现 Orchestrator → Agent 的主动推送pull 模型足够Phase 2 可加 SSE
- 不修改 Forgejo webhook 已有的实现(复用现有 `POST /api/v1/webhooks/forgejo`
## Decisions
### Decision 1: Pull 模型Agent 主动拉取任务)
**选择**: Agent 通过 `POST /api/v1/tasks/dequeue` 主动拉取
**理由**:
- 跨机场景下 Orchestrator 无法主动连接 Agent防火墙、NAT、离线
- Agent 最清楚自己什么时候有空
- Pull 模型天然负载均衡——空闲 Agent 自然拉更多任务
- 无需长连接、无需消息队列
**替代方案**:
- Push 模型Orchestrator 推送):需要 Agent 暴露端点,复杂度高
- 消息队列NATS/Redis增加基础设施依赖
### Decision 2: Forgejo PR lifecycle 作为通知机制
**选择**: 利用 Git 事件push → PR opened → PR merged作为任务状态追踪
**理由**:
- Forgejo webhook 已经实现,可靠性由 Forgejo 保证
- PR 本身就是 code review 流程——天然对应任务的"完成→验证"流程
- 解决了实际遇到的通知丢失问题:即使 Agent 直接通知失败Forgejo webhook 仍然会触发
- PR merged = 自动 receipt 验证PR 存在且被 merge = 代码确实被接受)
**替代方案**:
- SSE/WebSocket 推送:需要保持长连接,跨网络不可靠
- Agent 回调 URL需要 Agent 暴露端点
- 轮询:可行但延迟高,且不解决"Agent 完成后通知谁"的问题
### Decision 3: Registry token 认证
**选择**: Agent 注册后获得 token后续请求需携带
**理由**:
- 轻量级,不需要 OAuth 或 JWT
- 防止未授权的 Agent 领取任务
- token 在重新注册时刷新
## Risks / Trade-offs
- **[Pull 延迟] Agent 需要定期 dequeue 轮询** → Agent 可以在 heartbeat 周期内顺便 dequeue不增加额外开销
- **[PR 必需] 强制要求 Agent 创建 PR** → 非 PR 任务通过 `/complete` 端点完成,两条路径在 receipt 验证层汇合
- **[Forgejo 单点] Forgejo 挂了通知就断了** → Forgejo 本身是 Git source of truth挂了整个流程都停可接受
- **[死循环] Review 循环可能无限** → max_retries 限制 + review_count 跟踪
## Key Design Principles (from community best practices)
基于 ChatGPT 讨论和社区最佳实践的几个核心原则:
1. **Agent 是纯函数 worker**:输入 task + artifact输出 artifact + state change。不保留跨任务状态。
2. **状态机驱动,不对话驱动**:所有 Agent 只对共享状态SQLite + Forgejo读写由 Orchestrator loop 推进流程
3. **结构化 handoff**Agent 之间不传 chat history只传结构化 artifactreceipt、plan、review
4. **Git worktree 隔离**:每个 task 独立分支,避免并发冲突
5. **Deterministic verification**:不依赖 Agent 自我判断,用 `cargo test` / `npm test` 等确定性验证
6. **80/20 原则**80% 自动运行20% 人类介入(失败、架构决策、优先级调整)
7. **外部调度 + 内部自治**agent-fleet 只管理任务状态和 artifact 流转Agent 的执行 runtimehooks、compaction、sandbox、tool loop、session 管理)由 Agent 自己负责。agent-fleet 是 KubernetesAgent 是 Pod。
8. **不替代 Agent runtime**Claude Code / Codex / OpenCode 的价值在于它们的 runtime不是模型本身。agent-fleet 不重新实现 tool loop、context compaction、patch management 等。
基于 ChatGPT 讨论和社区最佳实践的几个核心原则:
1. **Agent 是纯函数 worker**:输入 task + artifact输出 artifact + state change。不保留跨任务状态。
2. **状态机驱动,不对话驱动**:所有 Agent 只对共享状态SQLite + Forgejo读写由 Orchestrator loop 推进流程
3. **结构化 handoff**Agent 之间不传 chat history只传结构化 artifactreceipt、plan、review
4. **Git worktree 隔离**:每个 task 独立分支,避免并发冲突
5. **Deterministic verification**:不依赖 Agent 自我判断,用 `cargo test` / `npm test` 等确定性验证
6. **80/20 原则**80% 自动运行20% 人类介入(失败、架构决策、优先级调整)
## Migration Plan
1. 新增 `POST /api/v1/tasks/dequeue` 端点
2. 新增 `POST /api/v1/tasks/{task_id}/status` 端点
3. 新增 `GET /api/v1/tasks/{task_id}` 端点
4. 扩展 Forgejo webhook 处理:支持 `pull_request``push` 事件
5. 重写 `src/adapters/mod.rs`:从 AgentAdapter trait 改为 Agent Protocol 描述文档
6. 新增 token 认证中间件
7. 更新测试
## Open Questions
_(resolved — 见下方)_
- ~~非 PR 任务research、review如何触发完成通知~~ → 使用 `POST /api/v1/tasks/{id}/complete` + receipt与 PR 路径在 receipt 验证层汇合
- ~~Token 认证是否需要在 Phase 1 实现?~~ → 必须在 Phase 1 实现,否则无法安全对接远程 Agent

View file

@ -0,0 +1,37 @@
## Why
当前 adapter interface 设计基于 **spawn 本地进程** 模式(`execute(task)` 由 orchestrator 主动调用 Agent这无法满足跨机协同的核心目标
1. **spawn 只能本机执行**:无法调度远程机器上的 Agent
2. **ACP 只能同实例通信**:无法跨 OpenClaw 实例
3. **主动推送模式不可靠**Orchestrator 无法主动通知远端 Agent 执行任务(可能离线、网络不通)
此外,**通知机制缺失**是一个实际遇到的生产问题Codex 写完代码后Jeeves 没有可靠的方式收到完成通知,导致任务卡住直到人工干预。
agent-fleet 的核心价值是**跨机 Agent 协同**,需要一个基于 HTTP 的、可靠的 pull + push 混合模式。
## What Changes
- **BREAKING**: `AgentAdapter` trait 中的 `execute(task)` 方法移除。Orchestrator 不主动调用 Agent 执行,而是 Agent 主动拉取任务
- 新增 **Agent pull 模式**`POST /api/v1/tasks/dequeue` — Agent 主动请求领取任务
- 新增 **Git-based 状态追踪**Agent 执行完成后推送代码到 Forgejo → Forgejo PR webhook 触发状态更新 → 替代 unreliable 的直接通知
- 新增 **任务状态查询**`GET /api/v1/tasks/{task_id}` — Agent 可轮询任务状态
- 保留 receipt 提交(`POST /api/v1/receipts`)作为最终确认
- Adapter config 从"本地执行参数"改为"远程 Agent 连接信息"
## Capabilities
### New Capabilities
- `task-assignment-protocol`: Agent 任务拉取协议dequeue + 状态更新 + receipt 确认)
- `notification-via-forgejo`: 基于 Git/Forgejo 的状态追踪和通知机制PR webhook → 状态同步)
### Modified Capabilities
- `agent-adapter`: 从"本地 spawn 执行"改为"远程 Agent 通过 HTTP API 自主交互"。Adapter interface 不再包含 execute(),改为 Agent 侧的客户端 SDK 协议
- `agent-registry`: 补充 Agent 认证机制registry token确保只有注册的 Agent 能领取任务
## Impact
- **代码**`src/adapters/mod.rs` 重写AgentAdapter trait → AgentClient SDK 协议描述)
- **API**:新增 `POST /api/v1/tasks/dequeue`,新增 `GET /api/v1/tasks/{task_id}`
- **通知机制**:利用现有 Forgejo PR webhook 作为状态追踪和通知通道,无需新增基础设施
- **配置**adapter config 从本地参数改为 Agent 连接信息

View file

@ -0,0 +1,36 @@
## MODIFIED Requirements
### Requirement: Unified adapter interface
系统 SHALL 定义统一的 Agent 客户端协议(非 trait描述远程 Agent 如何通过 HTTP API 与 Orchestrator 交互。Agent 可以运行在任何机器上,只要能访问 Orchestrator 的 HTTP 端点。
协议 SHALL 包含:
- `POST /api/v1/agents/register` — 注册到 Registry
- `POST /api/v1/agents/heartbeat` — 发送心跳
- `POST /api/v1/tasks/dequeue` — 主动拉取任务(替代被动的 execute
- `POST /api/v1/tasks/{task_id}/status` — 更新任务状态
- `GET /api/v1/tasks/{task_id}` — 查询任务详情
- `POST /api/v1/receipts` — 提交 receipt
- `POST /api/v1/agents/deregister` — 注销
#### Scenario: Remote Agent on different machine
- **WHEN** Agent `worker-03` 运行在 host-worker-02与 Orchestrator 不同机器)
- **THEN** Agent SHALL 通过 HTTP 调用 Orchestrator API 完成注册、领取任务、提交 receipt
- **AND** 无需 SSH、无需共享文件系统、无需同一 OpenClaw 实例
#### Scenario: OpenClaw-managed Agent
- **WHEN** Agent 由 OpenClaw 管理(如 Jeeves 调度 Codex
- **THEN** OpenClaw Agent SHALL 作为 Orchestrator 的客户端,通过 HTTP API 调用
- **AND** 任务的实际执行由 OpenClaw 内部的 ACP 机制完成
### Requirement: Adapter configuration
每个 Agent 实例 SHALL 通过配置文件指定 Orchestrator 连接信息、自身身份、工作参数。
#### Scenario: Remote Agent configuration
- **WHEN** Agent 在远程机器上配置
- **THEN** 配置 SHALL 包含:`{orchestrator_url: "http://arm0:9090", agent_id: "worker-03", token: "xxx", capabilities: ["code:rust"], work_dir: "/path/to/repo"}`
## REMOVED Requirements
### Requirement: Adapter health check
**Reason**: Orchestrator 不再主动连接 Agent。健康检查通过心跳机制实现——Agent 主动发心跳Orchestrator 检测超时。
**Migration**: 已有心跳机制(`POST /api/v1/agents/heartbeat` + TimeoutChecker覆盖此需求。

View file

@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Agent self-registration
每台机器上的 Agent 启动时 SHALL 向 Orchestrator Registry 注册自身信息。注册成功后 SHALL 返回 registry token后续 API 调用需携带此 token。
#### Scenario: New agent starts and registers
- **WHEN** 一个远程 Agent 在 host-worker-02 上启动
- **THEN** 它 SHALL 调用 `POST /api/v1/agents/register`
- **AND** Orchestrator 记录该 Agent 信息并返回 `{agent_id, registry_token}`
- **AND** 后续所有 Agent API 调用 SHALL 在 header 中携带 `Authorization: Bearer {registry_token}`
#### Scenario: Duplicate registration with same agent_id
- **WHEN** 已注册的 Agent 重启后再次注册(相同 agent_id
- **THEN** 系统 SHALL 更新该 Agent 的信息并返回新的 registry token

View file

@ -0,0 +1,44 @@
## ADDED Requirements
### Requirement: Git branch as task execution unit
每个任务 SHALL 关联一个 Git 分支。Agent 在该分支上工作,通过 PR 提交结果。分支命名约定:`task/{task_id}`(例如 `task/org%2Frepo%2342`)。
#### Scenario: Agent creates branch for task
- **WHEN** Agent 领取任务 org/repo#42
- **THEN** Agent SHALL 在目标仓库创建分支 `task/org%2Frepo%2342`(基于 master/main
#### Scenario: Agent pushes commits to task branch
- **WHEN** Agent 执行过程中产生代码变更
- **THEN** Agent SHALL 推送 commit 到对应的 task 分支
### Requirement: PR webhook as completion notification
Agent 完成任务后 SHALL 在 Forgejo 创建 Pull Request。Forgejo 的 PR webhook 触发 Orchestrator 状态更新,替代不可靠的直接通知。
#### Scenario: Agent creates PR → Orchestrator receives webhook
- **WHEN** Agent 为任务 org/repo#42 创建 PR
- **AND** Forgejo 触发 `pull_request.opened` webhook
- **THEN** Orchestrator SHALL 收到 webhook识别 PR 标题或分支名中的 task_id
- **AND** 将任务状态更新为 `review_pending`(等待 receipt 验证)
#### Scenario: PR merged → receipt auto-validated
- **WHEN** PR 被 merge
- **AND** Forgejo 触发 `pull_request.merged` webhook
- **THEN** Orchestrator SHALL 自动将任务状态转为 `completed`,生成 receipt
- **AND** 在对应 Issue 添加评论:`✅ Task completed — PR #15 merged`
### Requirement: Task branch and PR naming convention
Orchestrator SHALL 在任务详情中返回预期的分支名和 PR 标题格式,供 Agent 使用。
#### Scenario: Task detail includes branch info
- **WHEN** Agent 查询 `GET /api/v1/tasks/org/repo#42`
- **THEN** 返回 JSON SHALL 包含 `branch_name``pr_title` 字段
- **AND** `branch_name` 格式为 `task/{url_encoded_task_id}`
- **AND** `pr_title` 格式为 `feat: {issue_title} (#{issue_number})`
### Requirement: Push events as progress tracking
Forgejo push webhook SHALL 作为 Agent 工作进度的间接信号。
#### Scenario: Agent pushes to task branch
- **WHEN** Forgejo 触发 `push` webhook目标分支匹配 `task/*` 模式
- **THEN** Orchestrator SHALL 记录该 push 事件作为任务进度信号
- **AND** 更新任务的 `last_activity_at` 时间戳

View file

@ -0,0 +1,67 @@
## ADDED Requirements
### Requirement: Agent task dequeue (pull model)
Agent SHALL 通过 `POST /api/v1/tasks/dequeue` 主动拉取任务。Orchestrator 根据 Agent 声明的 capabilities 匹配最优任务,原子性地分配给该 Agent。
#### Scenario: Agent dequeues a matching task
- **WHEN** Agent `worker-03` 发送 `POST /api/v1/tasks/dequeue`body 包含 `{agent_id: "worker-03", capabilities: ["code:rust", "review"]}`
- **THEN** Orchestrator SHALL 在单个事务中找到 status=created 且匹配 capabilities 的最高优先级任务
- **AND** 将该任务状态转为 `assigned`assigned_agent_id 设为 `worker-03`
- **AND** 返回任务详情 JSON
#### Scenario: No matching task available
- **WHEN** Agent 发送 dequeue 但无匹配任务
- **THEN** Orchestrator SHALL 返回 204 No Content
### Requirement: Agent task status update
Agent 执行过程中 SHALL 通过 `POST /api/v1/tasks/{task_id}/status` 更新任务状态。
#### Scenario: Agent starts execution
- **WHEN** Agent 开始执行任务,发送 `POST /api/v1/tasks/org/repo#42/status` body `{status: "running"}`
- **THEN** Orchestrator SHALL 将任务状态更新为 `running`,记录 started_at
#### Scenario: Agent reports progress
- **WHEN** Agent 发送状态更新但任务已不在 assigned 给该 Agent
- **THEN** Orchestrator SHALL 返回 403 Forbidden
### Requirement: Single task detail query
Orchestrator SHALL 提供 `GET /api/v1/tasks/{task_id}` 返回单个任务详情。
#### Scenario: Query existing task
- **WHEN** 发送 `GET /api/v1/tasks/org/repo#42`
- **THEN** 返回任务完整信息 JSON包含所有字段和事件历史
#### Scenario: Query non-existent task
- **WHEN** 发送 `GET /api/v1/tasks/nonexistent`
- **THEN** 返回 404 Not Found
### Requirement: Agent authentication
Agent 调用任务相关 APIdequeue、status update、receipt时 SHALL 携带注册时获得的 token。Orchestrator SHALL 验证 token 有效性。
#### Scenario: Valid token
- **WHEN** Agent 携带有效 token 调用 dequeue
- **THEN** 请求正常处理
#### Scenario: Invalid or missing token
- **WHEN** Agent 不携带 token 或 token 无效
- **THEN** 返回 401 Unauthorized
### Requirement: Non-PR task completion endpoint
对于不产生 PR 的任务research、review 等Agent SHALL 通过 `POST /api/v1/tasks/{task_id}/complete` 显式提交完成,并附带 receipt。
#### Scenario: Agent completes a non-PR task
- **WHEN** Agent 发送 `POST /api/v1/tasks/org/repo#42/complete`,附带 receipt
- **THEN** Orchestrator SHALL 验证 receipt`POST /api/v1/receipts` 相同验证逻辑)
- **AND** 任务状态转为 `completed`
#### Scenario: Non-owner attempts to complete
- **WHEN** 非 assigned Agent 尝试 complete
- **THEN** 返回 403 Forbidden
### Requirement: Review loop limit
任务在 `running``review_pending` 之间循环 SHALL 有最大次数限制,防止死循环。
#### Scenario: Review loop exceeds limit
- **WHEN** 任务的 review 循环次数超过 `max_retries`
- **THEN** Orchestrator SHALL 将任务标记为 `failed`
- **AND** 在对应 Issue 添加评论说明超限原因

View file

@ -0,0 +1,48 @@
## 1. API 端点新增
- [ ] 1.1 实现 `POST /api/v1/tasks/dequeue`Agent 主动拉取任务,根据 capabilities 匹配,原子分配(复用 EventStore::dequeue_and_assign
- [ ] 1.2 实现 `POST /api/v1/tasks/{task_id}/status`Agent 更新任务状态assigned→running 等),验证 agent 归属
- [ ] 1.3 实现 `GET /api/v1/tasks/{task_id}`:返回单个任务详情 JSON
- [ ] 1.4 实现 `POST /api/v1/tasks/{task_id}/complete`:非 PR 任务显式完成 + receipt 验证
- [ ] 1.5 在 EventStore 中添加 `read_task_with_events(task_id)` 方法(任务详情 + 事件历史)
- [ ] 1.6 在 `src/main.rs` 注册新路由
## 2. Token 认证
- [ ] 2.1 在 register 响应中生成并返回 registry_token随机 UUID 或 HMAC
- [ ] 2.2 在 EventStore 中存储 agent_id → token 映射
- [ ] 2.3 实现 axum middleware 验证 `Authorization: Bearer {token}` header
- [ ] 2.4 将 middleware 应用于任务相关端点dequeue、status、complete、receipt
- [ ] 2.5 heartbeat 和 deregister 端点也要求 token 认证
## 3. Forgejo webhook 扩展
- [ ] 3.1 扩展 Forgejo webhook handler 支持 `pull_request` 事件opened、merged
- [ ] 3.2 PR opened → 从分支名解析 task_id → 更新任务状态为 `review_pending`
- [ ] 3.3 PR merged → 从分支名解析 task_id → 自动生成 receipt → 任务状态转为 `completed` → Issue 评论
- [ ] 3.4 扩展 Forgejo webhook handler 支持 `push` 事件:匹配 `task/*` 分支 → 更新 `last_activity_at`
- [ ] 3.5 在 Task 模型中添加 `branch_name``pr_title``last_activity_at` 字段
## 4. State machine 扩展
- [ ] 4.1 添加 `review_pending` 状态running → review_pending → completed/running
- [ ] 4.2 添加 `review_count` 字段到 Task跟踪 review 循环次数
- [ ] 4.3 review_count 超过 max_retries 时自动标记 failed
## 5. Adapter 模块重写
- [ ] 5.1 重写 `src/adapters/mod.rs`:移除 `AgentAdapter` trait 和 `AdapterRunner`,改为 Agent Protocol 文档
- [ ] 5.2 保留 `AdapterInstanceConfig``AdapterKind`(用于配置和文档生成)
- [ ] 5.3 移除 `AdapterRunner`Agent 自行管理生命周期)
## 6. 测试与验证
- [ ] 6.1 `cargo check` 通过
- [ ] 6.2 `cargo test` 全部通过
- [ ] 6.3 新增 dequeue 测试(匹配成功、无匹配任务、并发 dequeue
- [ ] 6.4 新增 status update 测试(正常更新、非所属 Agent 被拒)
- [ ] 6.5 新增 token 认证测试(有效 token、无效 token、缺失 token
- [ ] 6.6 新增 complete 端点测试(正常完成、非 owner 被拒)
- [ ] 6.7 新增 Forgejo PR webhook 测试PR opened → review_pending、PR merged → completed
- [ ] 6.8 新增 push webhook 测试task 分支 → last_activity_at 更新)
- [ ] 6.9 新增 review loop limit 测试(超过 max_retries → failed

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-12

View file

@ -0,0 +1,83 @@
## Context
`adapter-cross-machine-revision` change 设计了纯 Pull 模型Agent 主动调 HTTP API但分析后发现 Pull 模型不是 subprocess CLI 的跨机等价——控制权、上下文传递、生命周期管理都不同。
真正的跨机等价是 **SSH + CLI**Orchestrator 通过 SSH 在远程主机上 spawn Agent CLI与本地 subprocess 完全一致的执行模型。
同时HTTP Pull 对外部 AgentOpenClaw/Jeeves、Hermes仍有价值。因此需要双执行模型。
## Goals / Non-Goals
**Goals:**
- SSH + CLI 作为主执行模式Orchestrator 主动调度、构造上下文、控制生命周期)
- HTTP API 保留给外部 Agent 自主接入
- 每种 Agent 类型定义为 CLI 模板 + 输出解析器
- 远程主机管理SSH 连接、CLI 可用性检查)
**Non-Goals:**
- 不实现 Agent daemonPhase 1 用 SSH + CLI 足够)
- 不实现动态主机发现Phase 1 静态配置)
- 不实现容器化执行(不用 Docker/Kubernetes
## Decisions
### Decision 1: SSH + CLI 是 subprocess 的跨机等价
**选择**: Orchestrator 通过 SSH 执行远程 Agent CLI
**理由**:
- 控制流与本地 subprocess 完全一致
- 上下文由 Orchestrator 构造,通过 CLI 参数传入
- Agent 不需要预运行,不需要 daemon
- 唯一前提SSH 免密登录 + 目标机器装了 CLI
**替代方案**:
- HTTP Pull控制权反转上下文传递难解决
- Agent daemon复杂度高每台机器多一个服务
- 容器化:更复杂,需要 container runtime
### Decision 2: HTTP Pull 作为补充模式
**选择**: 保留 HTTP API 给外部 Agent
**理由**:
- OpenClaw/Jeeves、Hermes 有自己的调度,不需要 Orchestrator 启动
- 它们只需要查询/更新任务状态
- 两种模式通过 `execution_mode` 字段区分
### Decision 3: Adapter = CLI 模板 + 输出解析器
**选择**: 不实现 AgentAdapter trait而是配置驱动
**理由**:
- CLI 模板可以通过配置文件定义,无需编译
- 不同 Agent 的差异主要在 CLI 参数和输出格式
- 新增 Agent 类型只需要加配置,不需要写代码
### Decision 4: 本地执行作为 SSH 的特例
**选择**: Orchestrator 所在机器的 Agent 用本地 subprocess不经过 SSH
**理由**:
- 避免 SSH loopback 的开销和配置
- subprocess 是 SSH 的本地特例,逻辑统一
## Risks / Trade-offs
- **[SSH 密钥管理] 需要配置免密 SSH** → 用 SSH agent forwarding 或 deploy key标准运维实践
- **[长时间运行] SSH 连接可能超时** → 用 `ssh -o ServerAliveInterval=60`,或改用 SSH multiplexing
- **[CLI 版本差异] 不同机器可能装不同版本** → health check 时验证版本
## Migration Plan
1. 新增 `src/execution/` 模块SSH executor、CLI template、output parser
2. Task 模型添加 `execution_mode``assigned_host` 字段
3. 新增 `[hosts]` 配置 section
4. 实现 Orchestrator dispatch loopssh_cli → SSH 执行http_pull → 等待 Agent dequeue
5. 保留并调整 HTTP API 端点dequeue 仅限 http_pull 任务)
6. 更新测试
## Open Questions
- SSH 库选择:`ssh2` crate vs `tokio::process::Command` + 系统 `ssh` 命令?
- 是否需要支持 SSH jump host通过跳板机连接目标机器

View file

@ -0,0 +1,46 @@
## Why
当前 `adapter-cross-machine-revision` change 设计了纯 Pull 模型Agent 主动调 HTTP API 拉取任务),但这不是 subprocess CLI 的真正跨机等价:
- **subprocess CLI**Orchestrator 主动拉起 Agent构造上下文控制生命周期。Agent 跑完退出。
- **HTTP Pull**Agent 必须自己已经在运行,自己来拉任务,自己管生命周期。控制权反转。
Pull 模型无法解决:
1. **上下文传递**Agent 拉到 task JSON 后要自己构建 prompt、加载仓库、决定怎么执行
2. **Agent 不在运行**Orchestrator 无法启动远程机器上的 Agent
3. **生命周期管理**Orchestrator 无法控制 Agent 启停
**SSH + CLI 才是 subprocess 的真正跨机等价**:控制流、上下文传递、生命周期管理与本地 spawn 完全一致。
同时HTTP Pull 模式对**外部 Agent**OpenClaw/Jeeves、Hermes 等)仍有价值——这些 Agent 有自己的调度和运行时,只需要通过 API 查询/更新状态。
因此需要设计**双执行模型**。
## What Changes
- 新增 **SSH + CLI 执行模式**Orchestrator 通过 SSH 在远程机器上 spawn Agent CLI传入结构化 prompt收集输出
- **保留 HTTP API**:供外部 AgentOpenClaw/Jeeves、Hermes 等)自主接入
- Task 模型新增 `execution_mode` 字段:`ssh_cli` | `http_pull`
- Orchestrator 根据执行模式选择调度策略:
- `ssh_cli`Orchestrator 主动 spawn → 等待完成 → 解析输出 → 生成 receipt
- `http_pull`Agent 自主 dequeue → 自行执行 → 提交 receipt
- 新增 **SSH host 配置**:每台远程机器的 SSH 连接信息
- 新增 **CLI 命令模板**:每种 Agent 类型的 CLI 调用模板claude、codex、opencode
## Capabilities
### New Capabilities
- `ssh-cli-execution`: Orchestrator 通过 SSH + CLI 在远程机器上执行 Agent是 subprocess 的跨机等价
- `host-management`: 远程主机管理SSH 连接、Agent CLI 可用性检查)
### Modified Capabilities
- `task-assignment-protocol`: 补充双执行模式——`ssh_cli`Orchestrator 主动调度)和 `http_pull`Agent 自主拉取)
- `agent-adapter`: Adapter 定义为 CLI 命令模板 + 输出解析器,不再是 trait 或 protocol 描述
- `notification-via-forgejo`: 通知机制在两种模式下都适用SSH CLI 模式的 Agent 也会创建 PR
## Impact
- **代码**:新增 `src/execution/` 模块SSH executor + CLI template + output parser
- **配置**:新增 `[hosts]` section远程机器 SSH 信息)和 agent CLI 模板
- **Task 模型**:新增 `execution_mode` 字段
- **依赖**:新增 `ssh2``tokio-process` + SSH 相关 crate

View file

@ -0,0 +1,41 @@
## MODIFIED Requirements
### Requirement: Agent adapter as CLI template
每个 Agent 类型 SHALL 定义为 CLI 命令模板 + 输出解析器,而非代码 trait。Orchestrator 根据模板构造命令、通过 SSH 执行、解析输出。
#### Scenario: Codex CLI adapter definition
- **WHEN** Agent 类型为 `codex-cli`
- **THEN** adapter 定义 SHALL 包含:
- `cli_template`: `codex exec --json '{prompt}'`
- `work_dir`: `{repo_path}`
- `output_format`: `json`
- `timeout`: 3600
- `output_parser`: codex_json_parser
#### Scenario: Claude Code adapter definition
- **WHEN** Agent 类型为 `claude-code`
- **THEN** adapter 定义 SHALL 包含:
- `cli_template`: `claude -p '{prompt}' --output-format json --dangerously-skip-permissions`
- `work_dir`: `{repo_path}`
- `output_format`: `json`
- `timeout`: 3600
- `output_parser`: claude_json_parser
#### Scenario: Custom adapter with custom template
- **WHEN** 用户定义新的 Agent 类型
- **THEN** SHALL 能通过配置文件指定 CLI 模板和输出格式
#### Scenario: http_pull mode — no adapter needed
- **WHEN** Agent 使用 http_pull 模式(如 OpenClaw/Jeeves
- **THEN** 不需要 CLI adapter 定义Agent 通过 HTTP API 自行交互
### Requirement: Adapter configuration
Agent 实例配置 SHALL 关联到具体主机,包含连接信息和执行参数。
#### Scenario: Remote Codex on host-worker-02
- **WHEN** 配置 host-worker-02 上的 Codex
- **THEN** 配置 SHALL 包含:`{host: "host-worker-02", agent_type: "codex-cli", max_concurrency: 2, model: "gpt-5.5", capabilities: ["code:rust"]}`
#### Scenario: Local Codex on same machine
- **WHEN** Agent 运行在 Orchestrator 同一台机器
- **THEN** SSH 可替换为本地 subprocess无需 SSH 开销

View file

@ -0,0 +1,37 @@
## ADDED Requirements
### Requirement: Remote host configuration
Orchestrator SHALL 支持配置多台远程主机,每台主机包含 SSH 连接信息和可用 Agent 列表。
#### Scenario: Host configuration format
- **WHEN** 配置远程主机
- **THEN** 配置 SHALL 包含:`{host_id, hostname, ssh_user, ssh_port, ssh_key_path, work_dir, agents: [{agent_type, max_concurrency}]}`
#### Scenario: Host with multiple agents
- **WHEN** 一台主机配置了多个 Agent例如同时有 Codex 和 Claude Code
- **THEN** Orchestrator SHALL 跟踪每个 Agent 的并发数,不超过 max_concurrency
### Requirement: Host health check
Orchestrator SHALL 能检查远程主机的 SSH 连通性和 Agent CLI 可用性。
#### Scenario: SSH connectivity check
- **WHEN** Orchestrator 检查 host-worker-02
- **THEN** SHALL 尝试 SSH 连接并执行 `echo ok`
- **AND** 连接失败时标记主机为 `unreachable`
#### Scenario: Agent CLI availability check
- **WHEN** Orchestrator 检查 host-worker-02 上的 Codex
- **THEN** SHALL 执行 `which codex``codex --version`
- **AND** CLI 不存在时标记该 Agent 为 `unavailable`
### Requirement: Host selection for task assignment
当任务的执行模式为 `ssh_cli`Orchestrator SHALL 选择合适的主机执行。
#### Scenario: Select host by capability and availability
- **WHEN** 任务需要 `code:rust` 能力
- **THEN** SHALL 选择配置了对应 Agent 且当前并发数未满的主机
- **AND** 多个候选主机时优先选择负载最低的
#### Scenario: No available host
- **WHEN** 没有可用主机匹配任务需求
- **THEN** 任务保持 `created` 状态,等待主机可用

View file

@ -0,0 +1,33 @@
## MODIFIED Requirements
### Requirement: Git branch as task execution unit
每个任务 SHALL 关联一个 Git 分支无论哪种执行模式。Agent 在该分支上工作,通过 PR 提交结果。
#### Scenario: ssh_cli mode — Orchestrator creates branch
- **WHEN** ssh_cli 模式任务开始执行
- **THEN** Orchestrator SHALL 在目标仓库创建分支 `task/{task_id}`(通过 SSH 在远程主机执行 `git checkout -b`
- **AND** 将分支名传入 Agent prompt
#### Scenario: http_pull mode — Agent creates branch
- **WHEN** http_pull 模式任务被 Agent 领取
- **THEN** Agent SHALL 自行创建分支 `task/{task_id}`
### Requirement: PR webhook as completion notification
无论哪种执行模式Agent 完成任务后 SHALL 通过 Forgejo PR 触发状态更新。
#### Scenario: ssh_cli mode — Agent creates PR via CLI
- **WHEN** ssh_cli 模式的 Agent 执行完成
- **THEN** Agent或 Orchestrator 通过 SSH SHALL push 到 task 分支并创建 PR
- **AND** Forgejo PR webhook 触发状态更新
#### Scenario: http_pull mode — same flow
- **WHEN** http_pull 模式的 Agent 执行完成
- **THEN** Agent SHALL push 到 task 分支并创建 PR
- **AND** Forgejo PR webhook 触发状态更新(与 ssh_cli 模式相同)
### Requirement: Push events as progress tracking
无论哪种执行模式Forgejo push webhook SHALL 作为进度信号。
#### Scenario: ssh_cli mode — push detected
- **WHEN** ssh_cli 模式执行中Agent push 到 task 分支
- **THEN** Orchestrator 更新 `last_activity_at`(与 http_pull 模式相同)

View file

@ -0,0 +1,63 @@
## ADDED Requirements
### Requirement: SSH CLI execution mode
Orchestrator SHALL 支持通过 SSH 在远程主机上执行 Agent CLI 命令。这是 subprocess CLI 的跨机等价Orchestrator 构造上下文、主动启动 Agent、等待完成、收集输出。
#### Scenario: Execute Codex on remote host
- **WHEN** 任务分配给 host-worker-02 上的 Codex Agent
- **THEN** Orchestrator SHALL 通过 SSH 连接 host-worker-02
- **AND** 执行 `codex exec --json '{structured_prompt}'`
- **AND** 等待命令完成,解析 JSON 输出为 receipt
#### Scenario: Execute Claude Code on remote host
- **WHEN** 任务分配给 host-worker-03 上的 Claude Code Agent
- **THEN** Orchestrator SHALL 通过 SSH 连接 host-worker-03
- **AND** 执行 `claude -p '{structured_prompt}' --output-format json --dangerously-skip-permissions`
- **AND** 等待命令完成,解析 JSON 输出为 receipt
#### Scenario: SSH connection fails
- **WHEN** Orchestrator 无法 SSH 连接到目标主机
- **THEN** 任务 SHALL 标记为 `failed`,记录连接错误
- **AND** 如果 retry_count < max_retries SHALL 自动重试
#### Scenario: Agent CLI returns non-zero exit code
- **WHEN** 远程 CLI 命令返回非零退出码
- **THEN** 任务 SHALL 标记为 `failed`,记录 stderr 输出
### Requirement: Structured prompt construction
Orchestrator SHALL 为每个任务构造结构化 prompt通过 CLI 参数传入 Agent。Prompt 内容包括:任务目标、约束条件、影响文件范围、验证命令。
#### Scenario: Prompt for code task
- **WHEN** 任务为代码实现类型
- **THEN** prompt SHALL 包含Issue 标题和描述、任务约束(从 Issue labels 提取)、预期输出格式、验证命令(`cargo test` / `npm test`
#### Scenario: Prompt for review task
- **WHEN** 任务为代码审查类型
- **THEN** prompt SHALL 包含PR diff、审查要点、审查结果格式要求
### Requirement: CLI command templates
每种 Agent 类型 SHALL 有可配置的 CLI 命令模板。模板支持变量替换:`{prompt}``{work_dir}``{task_id}``{branch}`
#### Scenario: Codex CLI template
- **WHEN** Agent 类型为 `codex-cli`
- **THEN** 命令模板 SHALL 为 `codex exec --json '{prompt}'`
- **AND**`{work_dir}` 目录下执行
#### Scenario: Custom template
- **WHEN** 用户配置自定义 CLI 模板
- **THEN** SHALL 支持变量替换:`{prompt}``{work_dir}``{task_id}``{branch}`
### Requirement: Output parsing
Orchestrator SHALL 解析 Agent CLI 的 JSON 输出为 receipt。
#### Scenario: Parse Codex JSON output
- **WHEN** Codex CLI 输出 JSON
- **THEN** SHALL 提取statuscompleted/failed、summary、artifactschanged files、PR URL、duration
#### Scenario: Parse Claude Code JSON output
- **WHEN** Claude Code CLI 输出 JSON
- **THEN** SHALL 提取status、summary、artifacts、duration
#### Scenario: Malformed output
- **WHEN** Agent 输出无法解析为有效 JSON
- **THEN** 任务 SHALL 标记为 `failed`,记录原始输出

View file

@ -0,0 +1,72 @@
## MODIFIED Requirements
### Requirement: Dual execution mode
Orchestrator SHALL 支持两种任务执行模式:
1. **ssh_cli**Orchestrator 主动通过 SSH 在远程主机执行 Agent CLI。控制权在 Orchestrator上下文由 Orchestrator 构造并传入。
2. **http_pull**Agent 自主通过 HTTP API 拉取任务、执行、提交 receipt。控制权在 Agent适用于外部 AgentOpenClaw/Jeeves、Hermes 等)。
每个任务 SHALL 有 `execution_mode` 字段,由任务来源决定(默认 `ssh_cli`)。
#### Scenario: Forgejo Issue → ssh_cli task
- **WHEN** 任务从 Forgejo Issue 创建
- **THEN** execution_mode SHALL 为 `ssh_cli`
- **AND** Orchestrator SHALL 选择主机并主动执行
#### Scenario: External Agent using http_pull
- **WHEN** 外部 AgentJeeves需要执行任务
- **THEN** 任务 execution_mode SHALL 为 `http_pull`
- **AND** Agent 通过 `POST /api/v1/tasks/dequeue` 拉取并自行执行
### Requirement: Agent task dequeue (pull model — for http_pull mode only)
`POST /api/v1/tasks/dequeue` SHALL 仅适用于 `http_pull` 模式的任务。`ssh_cli` 模式的任务 SHALL 由 Orchestrator 直接调度,不经过 dequeue。
#### Scenario: Agent dequeues an http_pull task
- **WHEN** Agent 发送 `POST /api/v1/tasks/dequeue`
- **THEN** SHALL 仅返回 execution_mode = `http_pull` 的任务
- **AND** 排除已被 Orchestrator 调度的 ssh_cli 任务
### Requirement: Agent task status update (for http_pull mode)
`POST /api/v1/tasks/{task_id}/status` SHALL 适用于 `http_pull` 模式。`ssh_cli` 模式的状态 SHALL 由 Orchestrator 直接管理。
#### Scenario: ssh_cli task status managed by Orchestrator
- **WHEN** 任务 execution_mode = `ssh_cli`
- **THEN** 状态变更 SHALL 由 Orchestrator 在 SSH 执行过程中自动更新
- **AND** Agent 不需要调用 status update API
### Requirement: Single task detail query
Orchestrator SHALL 提供 `GET /api/v1/tasks/{task_id}` 返回单个任务详情,两种执行模式通用。
#### Scenario: Query task detail
- **WHEN** 发送 `GET /api/v1/tasks/org/repo#42`
- **THEN** 返回任务完整信息 JSON包含 execution_mode、assigned_hostssh_cli或 assigned_agent_idhttp_pull
### Requirement: Agent authentication (http_pull mode)
`http_pull` 模式的 Agent 调用 API 时 SHALL 携带 token。`ssh_cli` 模式不需要 Agent 认证(由 Orchestrator 直接管理)。
#### Scenario: ssh_cli mode — no Agent auth needed
- **WHEN** 任务 execution_mode = `ssh_cli`
- **THEN** Agent 不参与 API 调用,无需认证
#### Scenario: http_pull mode — token required
- **WHEN** 任务 execution_mode = `http_pull`
- **THEN** Agent SHALL 携带有效 token 调用 API
### Requirement: Non-PR task completion endpoint
对于不产生 PR 的任务research、review 等),无论哪种执行模式,都 SHALL 可通过 `POST /api/v1/tasks/{task_id}/complete` 显式完成。
#### Scenario: ssh_cli mode — auto-complete from CLI output
- **WHEN** 任务 execution_mode = `ssh_cli` 且 Agent CLI 输出包含成功 receipt
- **THEN** Orchestrator SHALL 自动解析输出并完成任务
#### Scenario: http_pull mode — Agent calls complete
- **WHEN** 任务 execution_mode = `http_pull`
- **THEN** Agent SHALL 调用 `POST /api/v1/tasks/{task_id}/complete` + receipt
### Requirement: Review loop limit
无论哪种执行模式,任务的 review 循环 SHALL 有最大次数限制。
#### Scenario: ssh_cli review loop
- **WHEN** ssh_cli 模式下 review 不通过
- **THEN** Orchestrator SHALL 自动重新调度 Agent 修复
- **AND** 超过 max_retries 时标记 failed

View file

@ -0,0 +1,62 @@
## 1. 数据模型扩展
- [ ] 1.1 Task 模型新增 `execution_mode` 字段(`ssh_cli` | `http_pull`,默认 `ssh_cli`
- [ ] 1.2 Task 模型新增 `assigned_host` 字段ssh_cli 模式下的目标主机 ID
- [ ] 1.3 Task 模型新增 `branch_name``pr_title``last_activity_at``review_count` 字段
- [ ] 1.4 TaskStatus 新增 `review_pending` 状态
## 2. 主机管理
- [ ] 2.1 新增 `HostConfig` structhost_id, hostname, ssh_user, ssh_port, ssh_key_path, agents
- [ ] 2.2 `config.toml` 新增 `[[hosts]]` section
- [ ] 2.3 实现 SSH 连通性检查(`ssh {host} echo ok`
- [ ] 2.4 实现 Agent CLI 可用性检查(`ssh {host} which codex`
## 3. SSH CLI 执行器
- [ ] 3.1 创建 `src/execution/mod.rs` 模块
- [ ] 3.2 实现 `SshExecutor`:通过 SSH 执行远程 CLI 命令,处理超时和错误
- [ ] 3.3 实现 `CliTemplate`:命令模板 + 变量替换(`{prompt}`, `{work_dir}`, `{task_id}`, `{branch}`
- [ ] 3.4 实现结构化 prompt 构造Issue 内容 → 结构化 prompt目标、约束、文件范围、验证命令
- [ ] 3.5 实现 output parser解析 Codex JSON 输出 → Receipt
- [ ] 3.6 实现 output parser解析 Claude Code JSON 输出 → Receipt
- [ ] 3.7 支持本地 subprocess 作为 SSH 的特例hostname = localhost 时)
## 4. 调度循环
- [ ] 4.1 实现 dispatch loop扫描 created 状态的 ssh_cli 任务 → 选择主机 → SSH 执行 → 更新状态
- [ ] 4.2 主机选择逻辑:按能力匹配 + 并发数限制 + 负载最低优先
- [ ] 4.3 执行结果处理:成功 → assigned → running → review_pending/completed失败 → failed + retry
- [ ] 4.4 Review 循环review_pending + PR feedback → 重新调度 → 检查 review_count ≤ max_retries
## 5. HTTP API 调整
- [ ] 5.1 `POST /api/v1/tasks/dequeue` 仅返回 execution_mode = `http_pull` 的任务
- [ ] 5.2 `POST /api/v1/tasks/{task_id}/status` 仅 http_pull 模式可用
- [ ] 5.3 `GET /api/v1/tasks/{task_id}` 返回 execution_mode 和 assigned_host
- [ ] 5.4 `POST /api/v1/tasks/{task_id}/complete` 两种模式通用
- [ ] 5.5 Token 认证中间件(仅 http_pull 模式的 API 需要)
## 6. Adapter 模块重写
- [ ] 6.1 重写 `src/adapters/mod.rs`:移除 `AgentAdapter` trait 和 `AdapterRunner`
- [ ] 6.2 保留 `AdapterKind`,新增 `CliAdapterConfig`cli_template, output_format, timeout, output_parser
- [ ] 6.3 内置 Codex 和 Claude Code 的默认 CLI 模板
## 7. Forgejo webhook 扩展
- [ ] 7.1 支持 `pull_request` 事件opened → review_pending, merged → completed + auto receipt
- [ ] 7.2 支持 `push` 事件task/* 分支 → last_activity_at 更新)
## 8. 测试与验证
- [ ] 8.1 `cargo check` 通过
- [ ] 8.2 `cargo test` 全部通过
- [ ] 8.3 SSH executor 测试mock SSH 或本地 localhost 测试)
- [ ] 8.4 CLI template 变量替换测试
- [ ] 8.5 Output parser 测试Codex JSON、Claude Code JSON、malformed
- [ ] 8.6 Prompt 构造测试
- [ ] 8.7 主机选择逻辑测试
- [ ] 8.8 Dispatch loop 测试ssh_cli 调度流程、http_pull 排除)
- [ ] 8.9 Review 循环 limit 测试
- [ ] 8.10 Forgejo PR/push webhook 测试