# Agent Pool — 工具参考

> **本文档定位**：池本身的命令清单 / env 表 / 调试 / 测试。
> **机制原理 / 设计决策 / 不变量** 在 [`docs/standards/10-agent-pool.md`](../../../docs/standards/10-agent-pool.md)，不在这里。

agent pool 把 worktree 环境创建成本一次性前置（`pool-init` ~5 分钟），后续每次任务
claim 一个 slot 只要 ~0.5 秒。**实测加速比 ~140×**。

## 命名约定

- **池**：`agent pool`，对应目录 `.agent-pool/`，每个槽位称为 **slot**
- **脚本前缀**：
  - `pool-*` 操作整池：`pool-init` / `pool-resize` / `pool-destroy`
  - `agent-*` 操作单 slot：`agent-claim` / `agent-release` / `agent-status` / `agent-sweep` / `agent-heartbeat`
- **环境变量**：所有以 `FFOA_AGENT_*` 起头
- **lib**：`scripts/dev/lib/agent-pool-lib.sh`，函数前缀 `ap_`

## 目录布局

```
<repo-parent>/                       # 主仓库的父目录
├── <repo>/                          # 主仓库（git common dir 在这）
├── <repo>-wt/                       # worktree 父目录（pool-init 自动建）
│   ├── .agent-pool/                 # 池根
│   │   ├── slot-1/                  # 每个 slot 是一个常驻 worktree
│   │   ├── slot-2/                  # 自带独立端口段 / PG/Redis 容器 / .env / Caddy 域名
│   │   ├── slot-3/                  # 默认 3 slot；`pool-resize` 可调
│   │   ├── slot-N.lock              # 占用时存在；记录 agent / 心跳 / 任务分支
│   │   ├── slot-N.lock.flock        # claim 事务用 flock 句柄
│   │   └── sweep.log                # GC 日志
│   └── <feature-name>/              # 命名 worktree（手工长期 feature 沙箱）
└── <repo>.code-workspace            # VSCode multi-root 入口（pool 脚本自动维护）
```

**真实例子**（chentao 机器，主仓库 `workspace`）：
- 主仓库：`/home/chentao/Code/workspace`
- 池根：`/home/chentao/Code/ffworkspace-wt/.agent-pool`（legacy 命名，仍兼容）
- IDE 入口文件：`/home/chentao/Code/workspace.code-workspace`

## 工作流（AI 视角）

### 入口约定

每次开始一个新任务，AI 先尝试 `agent-claim`。**根据 exit code 决策**，不要把命令抛回给用户：

| Exit | 含义 | AI 应该做什么 |
|---|---|---|
| 0 | 成功，stdout 有 export 行 | `eval` 后开干 |
| 2 | 同分支已被别的 slot 占用 | 用 `AskUserQuestion` 问用户是否换分支或接手已占的 slot |
| 3 | 池满 | 问用户：等其他任务释放 / `pool-resize` 扩池 / 走旧路 `setup-worktree.sh` |
| 4 | 池未初始化（罕见，setup-project 默认初始化过） | 问用户：现在 `pool-init --size 3` 吗？（约 5 分钟） |
| 5 | 池被显式禁用 | 自动走旧路 `setup-worktree.sh`，不打扰用户 |

stderr 含结构化字段 `EXIT_REASON=...`、`EXISTING_SLOT=...` 等，可被 grep 出来作为决策输入。

### 一次完整任务

```bash
# 1. claim
eval "$(bash scripts/dev/agent-pool/agent-claim.sh feature/my-task)"
# 现在拥有：FFOA_AGENT_SLOT=1, FFOA_AGENT_DIR=..., BACKEND_PORT=3301, FRONTEND_PORT=3300, ...

# 2. 在 slot 里干活
cd "${FFOA_AGENT_DIR}"
# ... edit / test / commit / push ...

# 3. 释放（自动 stash 未提交改动到 wip/slot-N-<ts> 保命）
bash scripts/dev/agent-pool/agent-release.sh
```

### IDE 入口（VSCode multi-root + Remote-SSH）

`pool-init` / `pool-resize` / `agent-claim` / `agent-release` 自动维护
`<repo-parent>/<repo>.code-workspace`，列 main + 全部 slot；`pool-destroy` 删除文件。

```
1. 本地 VSCode → Ctrl+Shift+P → "Remote-SSH: Connect to Host" → 选开发机
2. File → Open Workspace from File... → 选 <repo-parent>/<repo>.code-workspace
3. 文件树看到 main + slot-1/2/3，每个 slot name 带当前分支
4. Ctrl+Shift+\` 开终端时弹 root 选择菜单，选哪个 root → 在那个 slot 干活
5. 在某 slot 终端跑 `claude` → Claude session 在那个 slot 上下文里启动
```

`python3` 缺失时 `.code-workspace` 维护静默跳过——不阻塞主流程。

## 命令速查

```bash
# 池级
bash scripts/dev/agent-pool/pool-init.sh [--size 3] [--minimal] [--force]
bash scripts/dev/agent-pool/pool-resize.sh --size N [--force]
bash scripts/dev/agent-pool/pool-destroy.sh [--force] [--yes]

# slot 级
eval "$(bash scripts/dev/agent-pool/agent-claim.sh <branch> [<base>])"
bash scripts/dev/agent-pool/agent-release.sh [<slot>] [--keep-changes] [--force]
bash scripts/dev/agent-pool/agent-status.sh
bash scripts/dev/agent-pool/agent-sweep.sh [--dry-run] [--reset]

# 自动释放（hook + cron 一次性安装）
bash scripts/dev/agent-pool/install-release-hooks.sh [--dry-run]
```

## 路径解析顺序

`ap_pool_root` 按以下优先级返回池根目录：

| # | 条件 | 返回 |
|---|---|---|
| 1 | `$FFOA_AGENT_POOL_ROOT` 显式设置 | env 值 |
| 2 | `<repo-parent>/<repo>-wt/.agent-pool` 已存在 | **新规则路径**（默认） |
| 3 | `<repo-parent>/ffworkspace-wt/.agent-pool` 已存在 | **legacy 路径**（向后兼容） |
| 4 | 都不存在 | 默认返回新规则路径（pool-init 会按这里建） |

## 环境变量

| 变量 | 默认 | 作用 |
|---|---|---|
| `FFOA_AGENT_POOL_ENABLED` | `true` | 设为 `false` 时所有 agent-* / pool-init 拒绝执行（CI 用） |
| `FFOA_AGENT_POOL_ROOT` | 见 §「路径解析顺序」 | 池根目录，覆盖默认解析 |
| `FFOA_AGENT_POOL_SIZE` | `3` | `pool-init` 默认 size |
| `FFOA_CODEWORKSPACE_FILE` | `<repo-parent>/<repo>.code-workspace` | VSCode multi-root 文件位置覆盖 |
| `FFOA_AGENT_LABEL` | `$USER` | claim 时写入 lock，标识谁占用 |
| `FFOA_AGENT_SESSION_ID` | `$$` | 同上，便于追溯 |
| `AP_HEARTBEAT_INTERVAL` | `30` | 心跳秒数 |
| `AP_STALE_TIMEOUT` | `180` | stale 阈值秒数 |
| `AP_ORPHAN_MIN_AGE` | `300` | agent-dead / orphan 判定的 claim 年龄下限（秒），低于此值不回收 |
| `FFOA_AGENT_PPID` | _空_ | 显式传入"agent 长命父进程 PID"。空时心跳自检和 sweep agent-dead 路径跳过（不误杀短命 `$()` 子 shell）。SessionEnd hook / driver shell 才负责 export |

claim 成功后**自动 export**：

| 变量 | 内容 |
|---|---|
| `FFOA_AGENT_SLOT` | slot 编号（数字） |
| `FFOA_AGENT_DIR` | slot 的 worktree 绝对路径 |
| `FFOA_AGENT_LOCK` | lock 文件绝对路径 |
| `FFOA_AGENT_BRANCH` | 当前任务分支 |
| `FRONTEND_PORT` / `BACKEND_PORT` / `POSTGRES_PORT` / `REDIS_PORT` | 该 slot 的端口 |

## Agent 自识别约定（CLAUDE.md / AGENTS.md 共同约定）

agent 启动时按优先级判断自己在哪：

1. `$FFOA_AGENT_SLOT` 已设 → 在某 slot 里，可放心做改动
2. `pwd` 匹配 `*/.agent-pool/slot-*` → 同上（fallback）
3. 都没有，且当前在主 worktree → **拒绝任何破坏性操作**（`db:reset`、merge、改 `.env`、跑 prisma migrate），先建议占一个 slot 或显式确认走旧路径

## 调试与排错

### lock 实现细节
- lock 文件用 key=value 格式（纯 awk 解析，无 jq 依赖）
- `flock + lockfile` 双层锁：`flock` 保护 claim 事务，lockfile 是业务状态
  - **释放 flock 必须在 fork daemon 之前**——否则 daemon 继承 FD 200 → flock 不释放 → 下次同 slot claim 拒锁。详见 `.learnings/ERRORS/ERR-20260509-002.md`
- 活性靠 `heartbeat_pid`，不是 `pid`：claim 进程是短命的（调用完就退）

### slot 状态机
- `free` —— 无 lock
- `claimed` —— lock 存在、心跳新鲜、agent_ppid 活着（或字段空）
- `stale` —— heartbeat_at 超时 或 heartbeat_pid 已死
- `abandoned` —— lock.state=abandoned（脏 / 未推送 commit）→ **claim/sweep 都跳过**

### claim 本地分支同步策略

- 本地分支不存在 → 从 `origin/<task>` 建，或从 `origin/<base>` 建（已是新鲜）
- 本地分支存在 + 远端也存在 → 看关系：
  - 本地 == 远端 → no-op
  - 本地是远端 ancestor → `git merge --ff-only` 自动拉新
  - 本地领先远端（有未 push commit）→ **不动** + 警告
  - 分歧（diverged）→ **不动** + 警告，提示手工 rebase/merge
- 不管哪条路径，最后都计算 `origin/<base>` 自 merge-base 以来领先 commit 数，提示用户

### release clean 路径会删本地 task 分支 ref

clean 路径下检查 `refs/heads/<task-branch>` 满足任一 → 删：
- 远端已无该分支（PR merged + auto-delete-on-merge 后的情况）
- 已被任一 protected base 吸收（L1 ancestor / L2 squash patch-id / L3 rebase patch-id）

不满足两个条件 → 保留本地 ref。
**abandoned / `--keep-changes` 路径永不删 ref**。

### release 三档（按 workspace 状态分级）
- `clean` + 全部 commit 已 push → 真释放（删 lock + reset worktree 到 park）
- `dirty`（uncommitted 改动）→ 标 `state=abandoned reason=dirty`，保留 lock + worktree
- `unpushed`（HEAD 领先 upstream 且未被 protected base 吸收）→ 标 `state=abandoned reason=unpushed`
- `--force` 强通过 clean 路径（脏文件自动 commit 到 `wip/slot-N-<ts>` 保命）
- `--keep-changes` 仅释放心跳，标 `abandoned reason=manual`，worktree 不动

### sweep 三路
- `stale` —— heartbeat 超时或心跳进程死 → 直接清 lock（或 `--reset` 走 release）
- `agent-dead` —— agent_ppid 已死 → 走 release 分级（clean→真释放；脏→abandoned）
- `orphan` —— 远端分支已删 + 工作区干净 + commit 已合并（兼容老 lock）→ 走 release

### 自动释放安装
- 一键装 SessionEnd hook + crontab：`bash install-release-hooks.sh [--dry-run]`
- 主路：Claude session 退出立即调 release（0 延迟）
- 兜底：crontab 每 5 分钟跑 sweep

## 测试

```bash
bash scripts/dev/agent-pool/tests/run-tests.sh
```

隔离在 `mktemp` 临时池根（export `FFOA_AGENT_POOL_ROOT`），不会污染真实池。
覆盖：池未初始化（exit 4）/ 显式禁用（exit 5）/ 同分支冲突（exit 2）/ 池满（exit 3）/
stale 检测 / sweep 回收 / release park / 自动 stash。**24/24 通过**。

## 相关文档

- 机制原理 / 设计决策 / 不变量：[`docs/standards/10-agent-pool.md`](../../../docs/standards/10-agent-pool.md)
- 池槽 vs 命名 worktree 选择：[`docs/standards/05-development-workflow.md` §「并行开发选择树」](../../../docs/standards/05-development-workflow.md)
- 入口 skill：`setup-project`（首次开池）/ `start-feature`（每次新任务）
