# ERR-20260514-001: agent-pool orphan 检测识别不出 squash merge 后的 zombie slot

## 现象

`agent-sweep.sh` 跑完返回「回收 0 个 slot」，但池里明明有 slot：
- 工作区干净（`git status --porcelain` 空）
- 远端分支已删（PR 合并后 auto-delete-on-merge）
- 心跳还在更新（detached 守护进程没退）

人眼看就是 PR 早已 merge + 远端分支没了的"假占用"，应该被 sweep 自动收掉，结果一直占着位子，pool 越用越满。

## 根因

`scripts/dev/lib/agent-pool-lib.sh` 的 `ap_lock_is_orphan` 条件 4 用 `git rev-list HEAD --not origin/develop|staging|production --count == 0` 判断「分支已合并」。

这对 **fast-forward merge** 有效（HEAD 是 base 祖先），对 **squash merge 永远 false negative**：

- squash merge 把本地 N 个 commit 折叠成 develop 上 1 个全新 SHA
- 新 SHA 跟本地 HEAD 没祖先关系
- `rev-list HEAD --not develop` 仍然把本地 N 个原始 commit 算成"未合并"

而 CLAUDE.md 明确 `feature/* → develop` 默认 squash merge —— 等于「对自己最常用的合并策略永远漏判」。

## 二次坑：单 commit patch-id 也救不了 squash zombie

`scripts/ops/sweep-remote-stale.py` 已经为类似场景写过 L2 patch-id 检测，但它的实现是**逐 commit 比对**：

```python
# branch_patch_ids(): 本地分支每个 commit 的 patch-id
# develop_patch_ids: develop 最近 N 个 commit 的 patch-id 集合
# 命中条件：本地每个 patch-id 都在集合里
```

对 **rebase merge (1→1 cherry-pick) zombie 有效**，但对 **squash merge (N→1) 同样失败**：

- N 个原始 commit 的 N 个 patch-id ≠ squash 后那 1 个 commit 的 1 个 patch-id
- squash commit 的 patch-id 是 N 个 diff 的累积，不会等于其中任何一个

实测验证（slot-4, 9 commits squash-merged 进 PR #371）：单 commit patch-id 命中率 0/9。

## 解决

新 helper `ap_branch_absorbed`，三层兜底：

1. **L1**：`git merge-base --is-ancestor HEAD <base>` —— FF merge
2. **L2（新增）**：`git diff <merge-base> HEAD | git patch-id --stable` 算**累积 diff** 的 patch-id，跟 base 最近 N 条 commit 的 patch-id 集合对比 —— squash merge zombie
3. **L3**：每个本地 commit 单 patch-id 全在集合里 —— rebase merge zombie 兜底

L2 是关键。squash 后 develop 上的 1 个 commit 包含**完全相同的累积变更**，patch-id 相等。

## 验证

```bash
# 改完 lib 后从 slot-3 跑（用新版 lib）
$ bash /home/chentao/Code/ffworkspace-wt/.agent-pool/slot-3/scripts/dev/agent-pool/agent-sweep.sh --dry-run
agent-sweep: 开始扫描
  [dry-run] 将回收 (orphan): slot-4 orphan (branch=feature/gitea-cli-331 ...)
  [dry-run] 将回收 (orphan): slot-5 orphan (branch=feature/sap-tunnel-autossh ...)
```

之前是 "回收 0 个"，现在 slot-4/5 被正确识别。

## 启发 / 适用范围

1. **patch-id 等价检测要分场景**：rebase 是 1→1（单 commit patch-id 命中），squash 是 N→1（累积 diff patch-id 命中），FF 是 0→0（祖先关系命中）。仓库混用合并策略时，三层都要有。
2. `scripts/ops/sweep-remote-stale.py` 同样有这个 squash 盲区。**但它针对的是远端 stale 分支**，本仓库 squash zombie 远端早被 auto-delete-on-merge 删了不会出现在它扫描列表里，所以症状没暴露；不过 squash 默认关掉 auto-delete 的仓库会踩。
3. **bash 实现 patch-id 等价比对的坑**：`git diff-tree -p --stdin` 会静默丢掉没有结尾换行的最后一行，必须确保 stdin 以 `\n` 结尾——`sweep-remote-stale.py` 第 153-159 行专门加了注释解释。我 lib 实现里用 `git rev-list | diff-tree -p --stdin` 管道，rev-list 默认每行带 `\n`，安全。
4. **共享 .git，独立 worktree 的 lib 文件**：slot 是 worktree，跟主仓库共享 git objects 但 working tree 独立。改 slot 里的 lib 文件不会污染主仓库 working tree；同 worktree 里 source lib 用的是该 worktree 路径的 lib（脚本 source 用 `${BASH_SOURCE[0]}` 相对路径）。
