# 设计文档：acpx Backend 迁移

## 概述

本次迁移将 claude-teams MCP server 中所有 backend 替换为统一使用 `acpx` CLI 工具，完全去除 tmux 和 opencode HTTP API 依赖。迁移后，系统通过 subprocess 调用 `acpx` CLI 来管理 AI agent session 的完整生命周期（创建、发送 prompt、状态检查、关闭）。

核心变化：
- 新增 `acpx_client.py` 替代 `opencode_client.py`
- 重写 `spawner.py`，去掉 tmux/opencode，改用 acpx
- 更新 `models.py`，简化 TeammateMember 字段
- 更新 `server.py`，去掉所有 opencode/tmux 相关逻辑
- 删除 `tmux_introspection.py`

## 架构

### 迁移前架构

```
MCP Server (server.py)
    ├── spawner.py
    │   ├── tmux (spawn 进程)
    │   └── opencode_client.py (HTTP API)
    └── tmux_introspection.py (状态检查)
```

### 迁移后架构

```
MCP Server (server.py)
    └── spawner.py
        └── acpx_client.py (subprocess CLI)
```

### 数据流

```mermaid
sequenceDiagram
    participant Lead as Team Lead (MCP Client)
    participant Server as MCP Server
    participant Spawner as spawner.py
    participant Acpx as acpx_client.py
    participant CLI as acpx CLI

    Lead->>Server: spawn_teammate(name, prompt, subagent_type)
    Server->>Spawner: spawn_teammate(...)
    Spawner->>Acpx: create_session(agent, session_name)
    Acpx->>CLI: acpx <agent> sessions new --name <session_name>
    CLI-->>Acpx: 成功
    Spawner->>Acpx: send_prompt(agent, session_name, wrapped_prompt)
    Acpx->>CLI: acpx <agent> -s <session_name> '...' --no-wait
    CLI-->>Acpx: 成功
    Spawner-->>Server: TeammateMember
    Server-->>Lead: SpawnResult

    Lead->>Server: check_teammate(name)
    Server->>Acpx: get_session_status(agent, session_name)
    Acpx->>CLI: acpx <agent> status -s <session_name>
    CLI-->>Acpx: "alive" | "dead"
    Server-->>Lead: {alive: true/false, ...}
```

## 组件与接口

### acpx_client.py（新建）

替代 `opencode_client.py`，封装所有 `acpx` CLI subprocess 调用。

```python
class AcpxError(Exception):
    """acpx CLI 调用失败时抛出"""
    pass

def create_session(agent: str, session_name: str, acpx_binary: str = "acpx") -> None:
    """执行: acpx <agent> sessions new --name <session_name>"""

def send_prompt(agent: str, session_name: str, prompt: str, acpx_binary: str = "acpx") -> None:
    """执行: acpx <agent> -s <session_name> '<prompt>' --no-wait"""

def close_session(agent: str, session_name: str, acpx_binary: str = "acpx") -> None:
    """执行: acpx <agent> sessions close <session_name>"""

def get_session_status(agent: str, session_name: str, acpx_binary: str = "acpx") -> str:
    """执行: acpx <agent> status -s <session_name>，返回状态字符串"""
```

所有函数在 subprocess 返回非零退出码时抛出 `AcpxError`。

### spawner.py（重写）

去掉所有 tmux/opencode 代码，保留核心 spawn 逻辑：

```python
def spawn_teammate(
    team_name: str,
    name: str,
    prompt: str,
    acpx_binary: str,
    lead_session_id: str,
    *,
    model: str = "sonnet",
    subagent_type: str = "general-purpose",
    cwd: str | None = None,
    plan_mode_required: bool = False,
    base_dir: Path | None = None,
) -> TeammateMember:
    """使用 acpx 创建 session 并发送初始 prompt"""
```

移除的函数：
- `build_tmux_spawn_args`
- `kill_tmux_pane`
- `use_tmux_windows`
- `build_spawn_command`
- `build_opencode_attach_command`
- `discover_opencode_models`

新增的函数：
- `kill_acpx_session(agent: str, session_name: str, acpx_binary: str) -> None`

### models.py（更新）

**TeammateMember 变更：**

| 旧字段 | 新字段 |
|--------|--------|
| `tmux_pane_id: str` | 删除 |
| `opencode_session_id: str \| None` | 删除 |
| `backend_type: str` | 删除 |
| — | `acpx_session_name: str`（新增） |

**LeadMember 变更：**
- 删除 `tmux_pane_id` 字段

**ShutdownApproved 变更：**

| 旧字段 | 新字段 |
|--------|--------|
| `pane_id: str` | 删除 |
| `backend_type: str` | 删除 |
| `session_id: str \| None` | 删除 |
| — | `acpx_session_name: str`（新增） |

### server.py（更新）

**移除的导入：**
- `from claude_teams import opencode_client`
- `from claude_teams.opencode_client import OpenCodeAPIError`
- `from claude_teams.spawner import kill_tmux_pane, use_tmux_windows`
- `from claude_teams.tmux_introspection import peek_pane, resolve_pane_target`

**新增的导入：**
- `from claude_teams import acpx_client`
- `from claude_teams.acpx_client import AcpxError`

**移除的函数：**
- `_push_to_opencode_session`
- `_push_to_lead`
- `_cleanup_opencode_session`
- `_discover_lead_opencode_session`
- `_get_lead_session`

**移除的全局变量/逻辑：**
- `KNOWN_CLIENTS`
- `_VALID_BACKENDS`
- `_parse_backends_env`
- `_build_spawn_description` 中的 backend 选择逻辑
- `_update_spawn_tool`
- `HarnessDetectionMiddleware` 中的 backend 检测逻辑

**简化的工具：**
- `spawn_teammate_tool`：移除 `backend_type` 参数
- `check_teammate`：移除 `include_output`、`output_lines`、`notify_after_minutes` 参数，改用 acpx status
- `force_kill_teammate`：改用 `acpx_client.close_session`
- `process_shutdown_approved`：改用 `acpx_client.close_session`
- `send_message`：移除 opencode push 调用

## 数据模型

### TeammateMember（迁移后）

```python
class TeammateMember(BaseModel):
    model_config = {"populate_by_name": True}

    agent_id: str = Field(alias="agentId")
    name: str
    agent_type: str = Field(alias="agentType")
    model: str
    prompt: str
    color: str
    plan_mode_required: bool = Field(alias="planModeRequired", default=False)
    joined_at: int = Field(alias="joinedAt")
    acpx_session_name: str = Field(alias="acpxSessionName")
    cwd: str
    subscriptions: list = Field(default_factory=list)
    is_active: bool = Field(alias="isActive", default=False)
```

### LeadMember（迁移后）

```python
class LeadMember(BaseModel):
    model_config = {"populate_by_name": True}

    agent_id: str = Field(alias="agentId")
    name: str
    agent_type: str = Field(alias="agentType")
    model: str
    joined_at: int = Field(alias="joinedAt")
    cwd: str
    subscriptions: list = Field(default_factory=list)
```

### ShutdownApproved（迁移后）

```python
class ShutdownApproved(BaseModel):
    model_config = {"populate_by_name": True}

    type: Literal["shutdown_approved"] = "shutdown_approved"
    request_id: str = Field(alias="requestId")
    from_: str = Field(alias="from")
    timestamp: str
    acpx_session_name: str = Field(alias="acpxSessionName")
```

### Session Name 格式

Session name 与 `agent_id` 保持一致，格式为 `<name>@<team_name>`：

```python
session_name = f"{name}@{team_name}"  # 例如: "worker-1@my-team"
```

## 正确性属性

*属性（Property）是在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*

### 基于属性的测试概述

属性测试（PBT）通过在大量生成的输入上验证通用属性来确保软件正确性。每个属性都是一个形式化规范，应对所有有效输入成立。

---

**属性 1：AcpxClient 错误传播**

*对于任意* acpx subprocess 调用，当 CLI 返回非零退出码时，AcpxClient 应始终抛出 `AcpxError`，而不是静默失败或返回错误结果。

**验证：需求 1.5**

---

**属性 2：Session Name 与 agent_id 一致性**

*对于任意* name 和 team_name 组合，`TeammateMember.acpx_session_name` 应始终等于 `TeammateMember.agent_id`（两者均为 `<name>@<team_name>` 格式）。

**验证：需求 2.1、2.2、2.3**

---

**属性 3：Spawn 失败时的 session 清理**

*对于任意* spawn 操作，若在 `create_session` 成功后的任意步骤失败，`close_session` 应被调用以清理已创建的 session，确保不留下孤立 session。

**验证：需求 4.5**

---

**属性 4：check_teammate 状态映射**

*对于任意* session，`get_session_status` 返回 `"alive"` 时 `check_teammate` 应报告 `alive=True`；返回 `"dead"` 或抛出 `AcpxError` 时应报告 `alive=False`。

**验证：需求 8.2**

---

**属性 5：Prompt 包装完整性**

*对于任意* name、team_name 和 prompt 组合，包装后的 prompt 应包含原始 prompt 内容、team_name 和 name，确保 teammate 收到完整的上下文信息。

**验证：需求 4.7**

## 错误处理

### AcpxError 处理策略

| 场景 | 处理方式 |
|------|----------|
| `create_session` 失败 | 抛出 `AcpxError`，spawn 中止，不留残留状态 |
| `send_prompt` 失败 | 调用 `close_session` 清理，然后抛出异常 |
| `close_session` 失败 | 记录警告日志，不抛出异常（best-effort 清理） |
| `get_session_status` 失败 | 视为 `alive=False`，记录警告日志 |
| `acpx` 二进制不存在 | 启动时 `FileNotFoundError`，阻止 server 启动 |

### 向后兼容性

现有的 `config.json` 文件中可能包含旧字段（`tmuxPaneId`、`backendType`、`opencodeSessionId`）。Pydantic 模型默认会忽略未知字段，因此旧配置文件可以被新代码读取，但写入时会丢弃旧字段。这是预期行为。

## 测试策略

### 单元测试

针对具体示例和边界条件：

- `AcpxError` 在各种失败场景下正确抛出
- Session name 格式正确（`<name>@<team_name>`）
- `check_teammate` 在 `"alive"`/`"dead"`/`AcpxError` 三种情况下的返回值
- `spawn_teammate` 在 `send_prompt` 失败时调用 `close_session`

### 属性测试

使用 `hypothesis` 库进行属性测试，最少 100 次迭代：

- **属性 1**：对任意 agent/session_name 组合，模拟 CLI 返回非零码，验证 `AcpxError` 被抛出
- **属性 2**：对任意 name/team_name 组合，验证 `acpx_session_name == agent_id`
- **属性 3**：对任意 spawn 参数，模拟中途失败，验证 `close_session` 被调用
- **属性 4**：对任意状态字符串，验证 `check_teammate` 的 `alive` 字段映射正确
- **属性 5**：对任意 name/team_name/prompt，验证包装后的 prompt 包含所有必要信息

每个属性测试注释格式：
```python
# Feature: acpx-backend-migration, Property N: <property_text>
```

### 测试工具

- **属性测试库**：`hypothesis`（Python）
- **Mock 工具**：`unittest.mock.patch` 用于 mock subprocess 调用
- **测试框架**：`pytest`
