# Issue Sweep

> Sweep open GitHub issues for unprocessed human input and act on each via the issue-review skill — delete-PRs for human-approved deprecated docs, doc-update PRs for approved drifted docs, fix-PRs for approved bugs, grounded analysis for feature/design requests followed by autonomous execution when no real human-only fork remains, close issues whose PR merged, and comment-only on conditional/security/needs-decision ones. The actionable signal may be a comment OR the issue body. With --autofix-green, also auto-fixes "green" issues that have no human reply yet (unambiguous + verifiable-here + low-risk + non-security) — reproduce→fix→test→PR, never auto-merge. Run manually (/agentloop:issue-sweep) or on a schedule. Designed for a doc-audit + spin-off + feature workflow.

- Skill: `arcblock/issue-sweep` (Agent Skill, multi-file: 13 files)
- Install (CLI): `npx skillmds@latest add arcblock/issue-sweep`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arcblock/issue-sweep/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: ArcBlock (https://skillmd.com/u/arcblock)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/arcblock/issue-sweep

---


# Issue Sweep — batch-process issues with new human replies

> **Repo profile — read `.claude/repo-profile.md` first.** This skill is repo-agnostic;
> **arc is the reference implementation.** Use the profile's values wherever this doc shows an
> arc default: `repo_slug` (the `gh -R <owner/repo>` target), `gate_mode` (arc = `scripts`: no
> CI on PRs), `verification_entry` / `pre_merge_entry` (gate commands),
> `kb_issue`, the UI Face Paths, and `plugin_root` (where issue-graph's scripts live). Arc's own provenance for the lessons below (issue numbers,
> war-stories) is not inlined here (fuller case narratives, where they exist, are under `.claude/case-law/`).

A **batch driver** over [`issue-review`](../issue-review/SKILL.md). `issue-review`
handles ONE issue (read → verify against code → act). `issue-sweep` finds *which*
issues need handling right now — the ones whose **latest comment is a human reply
the agent hasn't acted on yet** — and runs the per-issue engine on each.

This is the thing a cron should schedule: one run = scan + process a batch. In
environments without a working scheduler, run it by hand: `/agentloop:issue-sweep`.

> **★ 无人值守铁律(cron routine / /loop——本 skill 的默认运行形态):绝不调用任何会等待用户的工具——`AskUserQuestion`、`Workflow`(需交互式 opt-in 确认)、`EnterPlanMode`(退出需用户批准)。** 无人应答 → 整条 routine 永久挂死(实测:sweep routine 整夜卡在「是否运行 workflow」的提问上)。需要人拍板的问题,照 [`design-review` Autonomous escalation](../design-review/SKILL.md) 范式处理:把选项 + 你的推荐 + 被 block 的内容作为 comment(挂 `needs-human-confirm`)落到对应 issue,**然后继续处理下一项**。**禁止的是会等待确认的交互式编排,不是并行本身**:无人值守环境优先使用无需 opt-in、不会等待用户的 subagent/agent fan-out;若当前 runtime 没有这种能力才串行 inline fallback。repo hook(`.claude/hooks/deny-interactive-unattended.py`)会在无人值守 session 硬 deny 这三个工具兜底——被 deny 即说明你在无人值守环境,按本条纪律走,不要重试。

> **输出语言与写作规范(中文,信雅达),同 `issue-review`。** 所有面向团队的产出——issue/PR comment、**PR/issue 描述正文**、issue 标题、triage 说明——一律中文;代码标识符、路径、命令、`path:line`、测试输出保持原样。**PR 与 commit 标题必须全英文**——完整 Conventional Commits(`type(scope): english description`,冒号后的描述也用英文),不得留中文;issue 标题保持中文。**不堆砌**:先一句话结论,再最少但足够的证据(文档 / 代码 `path:line` / 真实测试输出,**UI 相关必附截图**);长日志折叠进 `<details>`。

## Usage

```
/agentloop:issue-sweep            # scan all candidate labels, process every unprocessed human reply
/agentloop:issue-sweep --dry-run  # report what WOULD be processed; do not post/PR/close
/agentloop:issue-sweep <label…>   # restrict the candidate set to specific labels
/agentloop:issue-sweep --autofix-green   # ALSO auto-fix "green" issues that have NO human reply yet (Step 3b)
/agentloop:issue-sweep --concurrency 2   # override this repo's configured active-issue limit
```

Repo is `<repo_slug>`. Use the `gh` CLI when it's available (as the `gh …`
examples below do); if it's absent, use the `mcp__github__*` tools instead
(load them via ToolSearch).

## Step 0 — Sync the local repo FIRST (do not skip)

Everything downstream — `git grep` safety checks, `check-types`, and every
branch you cut for a fix/deletion — must run against **the latest
`<default_branch>`** (some repos use `master` instead of `main`), not whatever
stale tree the container happened to clone. A prior
sweep may have merged PRs that moved `<default_branch>`; processing against a
stale checkout risks branching off old code, deleting a file someone else
already changed, or a safety grep that misses a freshly-added reference.

Before scanning, bring the local clone current. **⚠️ The checkout may be SHARED
with a human or another live agent session** (this repo runs several actors on
one machine) — uncommitted changes are possibly someone's in-flight work, NOT
necessarily "leftover junk from an aborted run". Never blind-`reset --hard`:
stash first (reversible), so a human can recover with `git stash list` / `pop`.

Run these as PLAIN commands and read each result — **you are the control flow**; do not
wrap them in shell loops/conditionals/`$(…)` (a sandboxed Bash guard refuses those, and
this step then hard-fails before the sweep even starts — seen live).

```bash
git fetch origin <default_branch>   # transient failure? just run it again (you are the retry loop)
git status --porcelain              # READ this: any output = dirty tree → stash on the next line
```

- **Do NOT `git checkout <default_branch>`.** In a git worktree (e.g. a fleet checkout) the
  branch is held by the PRIMARY worktree and checkout hard-fails; the fetch + reset below
  works whether you're on the branch or detached.
- **Dirty tree → stash (recoverable), never discard**: it may be a concurrent session's
  uncommitted work (a blind hard-reset here has previously destroyed in-flight edits from
  another session — arc case-law). Only if the `git status --porcelain` above printed
  something:

```bash
git stash push -u -m "issue-sweep preempted"
```

Then, tree clean:

```bash
git reset --hard origin/<default_branch>   # only moves the ref now
git log --oneline -1                       # confirm you're at the real tip
```

Then cut every fix branch from the freshly-synced `origin/<default_branch>`
(`git checkout -B <branch> origin/<default_branch>`), as Step 4 already requires. A sweep
starts from a clean, current tree — but "clean" is achieved by stashing, not
destroying.

## Step 0.5 — 确定性图计算（[`issue-graph`](../issue-graph/SKILL.md)，每轮必跑）

label 扫描之前先跑一次图计算（只读，REST-only，秒级）：

```bash
bun <plugin_root>/skills/issue-graph/scripts/graph-scan.ts --window-hours 2
```

消费它的三个输出：

- **`kicks` → 直接并入候选集，无需人类 comment。** 这是对 Step 2 谓词的结构性补丁：
  子 issue 关闭是状态变化、不产生人类 comment，旧谓词永远看不见「孩子做完了，父该
  收尾 / 兄弟被解锁」。kick 让关闭事件确定性传播。
- **`rollupCandidates`（全部孩子已关的 open 父）→ 走 [`issue-review` ★父级 rollup](../issue-review/SKILL.md)**
  （fencing 互斥 + 验收核对 + 综合 comment + close）。
  **`agent:hold` 一票否决 close**：带 hold 的父 issue 仍可综合 comment，但**绝不 close**——
  hold 禁止一切终态动作，优先级高于 rollup 的「全覆盖则 close」例外（实盘发现：#1104 同时是
  rollup 候选且带 hold，两条规则直接冲突）。留开，等人摘 label。
- **`blocked` → 确定性 SKIP**（有 open blocker 的连候选都不进，本轮记录原因即可）。
  被 block 的 issue 不再靠模型猜「是不是还没轮到」。

`ready` 的顺序已按 hostname 旋转（多机同分钟起跑时错峰，降低锁竞争）。图只决定
「谁进候选、谁跳过」——注入的候选照常走 Step 2 谓词、Step 3 分派、锁与认领检查，
不绕过任何既有纪律；无边的 issue（人手开的）= 图中孤立点，照常走 label/catch-all。

**`agent:ready` label 消费（producer routine 在维护它时）**：producer 定期跑
`producer.ts` 把 kick/rollup 事件物化成 `agent:ready` label（人可观察的 queue 视图）。
sweep 可以**优先**从 `gh api "repos/{owner}/{repo}/issues?state=open&labels=agent:ready"`
领取，但三条铁律：① **label 只是索引提示，领取后必回 GitHub 重验**（仍 open、图上
仍成立、无 hold、无未处理人类输入——重验就是本轮 graph-scan + Step 2 谓词）；
② **处理完（终态 disposition 落定）由消费方摘掉 `agent:ready`**——producer 只加和
清理失效（closed/hold/blocked），不知道"事做完没"；③ producer 挂了 = label 陈旧或
缺失,**退化回本节的 graph-scan 自算**,行为不变、无单点。

**`agent:ready` vs `needs-human-confirm` 矛盾——按【贴标签的先后】裁决(权威信号)。**
一个 issue 同时带 `agent:ready`(该做)和 `needs-human-confirm`(等人)时,**不要靠评论内容猜**
(人和 agent 的评论格式无法区分,见 Step 2)——读 **label 事件时序**:
`gh api "repos/{owner}/{repo}/issues/{n}/events"`,取 `labeled`/`unlabeled` 事件(带 actor +
`created_at`,GitHub 权威记录、不可伪造)。**谁最后贴的谁赢**:
- `agent:ready` 晚于 `needs-human-confirm`(尤其人类亲手贴)→ **人已确认/解锁 → 开干**(接手实现)。
- `needs-human-confirm` 晚于 `agent:ready` → **人在 ready 之后按下暂停 → 等人**,本轮不做终态动作。
- 只有其一 → 按其一;一个被后来的 `unlabeled` 摘掉 → 不再计入。
参考实现 `labelStance()`(`test/sweep-golden/lib.ts`)。**实盘 arc#1722**:Phase 4 方案已就绪、
带 `agent:ready`,但 `needs-human-confirm` 更晚贴 → 正确判为"等人拍板轴 B 决定",人一确认(摘标
或贴 ready)即接手。

## Why a windowed "recently updated" scan is NOT enough

The naive scan — "list issues by `UPDATED_AT` desc, take the top N" — **misses
early human replies that were never re-bumped**. Real misses this caused: one
issue whose only human reply came early and was never re-bumped (archetype:
`test/sweep-golden/fixtures/328-early-human-reply-sank.json`), and another
labeled only `P3` — no `doc-audit`/`bug` — so it wasn't even in the label
union. **Scan by label coverage + the unlabeled catch-all + last-comment,
not by an updated-at window.**

## Step 1 — Build the candidate set (by label, not by recency)

`mcp__github__list_issues` with `state: OPEN`, once per label, union the results.
Candidate labels = the **Label Vocabulary** in `.claude/repo-profile.md`
(Priority + Work-type rows; arc: `doc-audit`, `bug`/`P0`-`P3`,
`enhancement`/`feature`, `research`, `idea`). Case-law: a priority-only label
is exactly how a real issue slipped through; `research`/`idea` issues are
often created unlabeled entirely — Step 3 has the
per-work-type dispatch row for each, and the unlabeled catch-all below closes
the discovery hole by triaging and backfilling the label.

De-dup by issue number. Optional args override this default label list.

**Unlabeled / off-label catch-all(每轮必扫——label 并集对它们天然失明):**
founder/人随手开的 issue 常常 **0 label、0 comment**,actionable 信号全在 body。
真实 miss:一条 feature issue(无 label)、一条 research issue(无 label——创建后
2 小时内 hourly sweep 照常跑了,但 label 扫描永远看不见它,只能靠人手工 `/agentloop:issue-review`
点名)。上面 research/idea 行写的「扫到就补 label」在纯 label 扫描下是**循环依赖**
(没 label 就扫不到,扫不到就没人补 label)——这条 catch-all 通道打破它。**别依赖
人打 label**,每轮追加一次全量 open 列表,把「不带任何候选 label」的捞出来:

```bash
# 一次列全部 open issue,本地滤出没有任何候选 label 的。
# doc-audit-kb(KB/repo-map 基础设施 issue,即 kb_issue)不是工作项,一并排除;
# test-sweep-failure/test-sweep-report 同理排除——它们是 test-sweep skill 自动开的 QA
# 发现,不是「founder 随手开的」catch-all 目标,有自己专属的处理通道(见 Step 3b「test-sweep
# 发现的 issue」),混进这里会被误当人类工作项二次 triage。
# 下一节的 reserved 规则仍然适用——带 agent:hold 的冻结终态动作(人类新评论仍要响应)。
# ★ epic-managed / epic:<n> 必须在 catch-all 的 select 里就排除(Codex P2 on arc#3558):
#   conductor 子 issue 常常只有这两类 label、尚无 work-type label;若不在 jq 里滤掉,
#   下面「对捞出的每条补 label / triage comment」会在 reserved 检查之前就 mutation,
#   与 epic-managed「不 triage、不评论」矛盾。
# -R <repo_slug> from repo-profile.md.
gh issue list -R <repo_slug> --state open --limit 500 --json number,title,labels --jq '
  .[]
  | select(([.labels[].name] | any(. == "epic-managed" or startswith("epic:"))) | not)
  | select(([.labels[].name] | map(select(
    . == "doc-audit" or . == "bug" or . == "P0" or . == "P1" or . == "P2" or . == "P3"
    or . == "enhancement" or . == "feature" or . == "research" or . == "idea"
    or . == "doc-audit-kb" or . == "test-sweep-failure" or . == "test-sweep-report")) | length) == 0)
  | "#\(.number) \(.title)"'
```

> 这条命令稳定捞出两位数条对 label 扫描不可见的 open issue,全是 founder 直开的
> 工作项——系统性缺口,不是单条偶发个案(archetype:
> `test/sweep-golden/fixtures/869-research-no-labels.json`)。

**Before any catch-all mutation:** drop anything that still slipped through with
`epic-managed` (belt-and-suspenders; the jq above is the primary filter). Never
add work-type labels or post triage comments on epic-managed issues.

对捞出的每条:读 title+body 判 work-type(`调研`/`研究`/`[research]` → `research`;
`idea:`/提案语气 → `idea`;带 spec/验收标准 → `feature`;报错/复现步骤 → `bug`),
**先补上对应 label**(可逆 triage,自动做——这样它下轮起进入正常 label 扫描,且
Step 3 的分派行有了正确的 work-type),再并入本轮候选集走 Step 2/3。**判不出类型
的也不静默跳过**:挂 `needs-human-confirm` + 留一条 triage comment 列出你的猜测让
人一键确认——静默跳过 = 这条 issue 对自动化永久不可见,这正是本通道要消灭的状态。

**Then drop the reserved/locked ones (并发协调,见 [`issue-review` ★并发锁](../issue-review/SKILL.md)):**

- **`agent:hold`** — 人类保留 = **终态冻结,不是处理冻结**("没我反馈别做不可逆动作",不是"别理它")。hold 期间**绝不 close / 绝不代表它做终态处置**,直到人摘掉 label;但**人类新评论照常进 Step 2 候选**——人专门在 hold 的条目上说话,恰是最高优先级输入,必须读并响应(回 comment / 按人类明确要求干活)。无人类新输入的 hold 条目才跳过。可执行闸是 hook `deny-guarded-issue-close.ts`（#5426）；散文不是闸。
- **`agent:processing`(新鲜)** — 正被另一个 run 处理。每候选读它有没有这个 label;有就查锁龄(见下),**TTL 30min 内 → SKIP**(别人在做),**过期 → 不跳**(上一个 runner 崩了,Step 3 会重新 acquire 抢锁)。锁龄取该 label 最后一次 `labeled` 事件时间:
  ```bash
  gh api --paginate repos/{owner}/{repo}/issues/<N>/timeline \
    --jq '[.[]|select(.event=="labeled" and .label.name=="agent:processing")]|last|.created_at'
  ```
  (无 `gh` 时用 `mcp__github__*` 读 timeline。)`agent:processing` 是 advisory——**硬去重仍靠 Step 4 的确定性分支 + 认领检查**;这一步只是早点短路、省掉重复的读/核验/测试。
- **`epic-managed`** — 该 issue 属于一个 **epic-conductor 独占驱动**的 epic(见 [`epic-conductor`](../epic-conductor/SKILL.md))。**整条排除:不 triage、不认领、不做、不评论。** 这不是 `agent:hold`(冻结终态但仍响应人类评论)——epic 子 issue 由那个 conductor 端到端调度自己的一批 worker 处理,sweep 插手只会撞车、开重复 PR(archetype:#3407 撞 #3395)。`agent:hold` 是「人类保留」,`epic-managed` 是「另一个 agent 保留」;两者都跳过认领,但 `epic-managed` 连人类评论也不由 sweep 代答(那是 conductor 的活)。一条规则管所有现在与未来的 epic。**过滤某个具体 epic 的全部子项**:`gh issue list --label "epic:<父issue#>"`(issue+PR 通用)。

## Step 2 — Keep only "last comment = unprocessed human reply"

For each candidate, read comments (`mcp__github__issue_read method:get_comments`).
**Critical detail of this repo:** the AI audit/agent posts under the *same*
account as humans. So
distinguish by **content, not author**:

- A comment is **agent-authored** only if it carries a **machine-emitted marker**: a
  `<!-- sweep-trace: … -->` (every autonomous sweep/review comment MUST carry one — see
  §sweep-trace), **or** the `_Generated by [Claude Code]` footer, **or** its GitHub author is
  a **Bot** (`user.type == "Bot"` — cloud runners post as `claude[bot]` via the GitHub App
  token). Markers can arrive HTML-escaped (`&lt;!-- … --&gt;`, the #1278 double-escape bug) —
  **decode entities before matching**.
- **The `> 🤖 AI Agent` header and the `runner:… · skills@…` identity line are NOT sufficient
  on their own.** `<agent_identity_script>` generates that exact header for **both agents
  and humans**, so a human posting locally (e.g. a design/decision comment from your own
  machine) produces a byte-identical opener. Keying on the header misread **arc#1722**'s
  human Phase-4 directive as an agent verdict and skipped the issue for 19h. So a `> 🤖`
  comment **without** a machine marker is treated as **human** — respond to it. (Pre-convention
  comments predating the sweep-trace mandate also fall here; that is the intended new norm,
  old data is not specially handled.)
- **Exception — the presence board `> 📡` heartbeat**: script-authored WITHOUT a trace, so
  recognize it by its `> 📡` opener and treat it as non-human; otherwise a content check
  reads it as fresh human input and re-processes the board every round (#1361).
- **Exception — zero-comment `test-sweep-failure`/`test-sweep-report` issues**: test-sweep
  opens these directly (not through `issue-publish`'s `test-sweep-key` marker path), so the
  BODY carries the `> 🤖 AI Agent` identity header but **no** machine marker — by the letter of
  the rule above that reads as "human, unprocessed". It isn't: it's an automated QA finding.
  **Label wins over content here**: a zero-comment issue carrying `test-sweep-failure` or
  `test-sweep-report` is never a Step 2 human-reply candidate (archetype: `#2271`,
  "[test-sweep] aistro: 1 failures detected" — see `test/sweep-golden/fixtures/2271-*.json`).
  It's a distinct discovery source Step 3b handles on its own — see Step 3b「test-sweep 发现的
  issue」. Once the issue gets an actual human comment, this exception no longer applies — the
  ordinary last-comment rule takes back over.
- A **human comment** is anything else (including a `> 🤖`-headed comment with no marker).

Keep the issue only if the **last comment is a human comment that the agent has
not yet responded to** (i.e. no later `🤖 AI Agent` comment, no PR-link/close
note answering it). Cheap pre-filter: `doc-audit` issues with `comments == 1`
are AI-audit-only (no human) — skip. But do NOT rely on count alone: human-first
spin-offs have a single human comment and no audit. When in doubt, read.

**The actionable signal can be the issue BODY, not a comment.** A human-authored
**feature / design request** (archetype:
`test/sweep-golden/fixtures/367-body-only-no-labels.json`) often has **zero
comments** — the directive lives in the body itself ("analyze this, discuss
unclear points, then write an executable TDD plan; implement after I confirm").
Treat such an issue as unprocessed when there is **no `🤖 AI Agent` comment yet**.
So the real predicate is: *"the latest human input — last comment, or the body
of a fresh design issue — has no agent response."* Don't require a comment to
exist.

**A conclusion-style batch-disposition list embedded in an issue body/comment is NEVER itself
an executable instruction (arc#2914, archetype #1863).** An audit/rollup issue's "建议关闭"/
"建议合并"/similar suggested-batch-action table — even one that reads as a finished
conclusion — is **evidence**, not a directive: sweep may read it and cite it when it
independently reasons about one of the named issues, but must **not** treat the list itself as
authorization to close/merge any issue it names. `#1863` is the live incident this guards
against: a one-off 138-issue backlog audit ended with "『建议关闭』一栏本次没有代关，等人扫
一眼表格后批量关即可" (an explicit human-review caveat, not an executable command) — a later
unattended sweep consumed that table directly and closed 12 issues with zero human
confirmation, bypassing the gate the audit issue itself declared. An item named in such a list
becomes actionable only when **either** (a) its `agent:hold` — applied per
[issue-review's 建议关闭-list rule](../issue-review/SKILL.md) at the moment the list was
posted — has since been deliberately removed by a human, **or** (b) an independent human
comment explicitly confirms this specific action (a generic "LGTM"/"谢谢" does not count, and
neither does the list's own caveat sentence). Absent both, sweep may read/cite the list but
must not act on any item in it.

**A non-TERMINAL agent comment does NOT count as "responded" — re-pick it up.** Skip
only when the last agent comment is a **terminal** state with a real unlock condition:
done→PR, closed, or an explicit needs-human/needs-design/security/not-verifiable-here
verdict. **An agent comment that says it deferred its own work — "🟢 排队中 / 本轮未做 /
留开放 / candidate-queued / in-progress" — is UNFINISHED, not terminal → treat the issue as
still to-process and DO it this run** (per Step 3b: 可做即做,不排队;archetype:
`test/sweep-golden/fixtures/533-non-terminal-deferred.json`). This is the fix for the
self-freeze the user hit: a "我会晚点做" comment was making the next sweep skip forever, so a
whole class of doable issues never advanced. Cheap detection — **do these IN ORDER; the
order IS the rule** (a live arc sweep proved that getting it backwards inverts the intent):

1. **Strip fenced/inline code first.** A marker word inside a snippet is not a deferral —
   a live sweep called an issue "deferred" because the YAML key `cancel-in-progress: false`
   inside a code block matched `in-progress`.
2. **TERMINAL WINS.** If the comment carries a real unlock condition (`PR #…` / closed /
   `needs-human-confirm` / `needs-design` / `security-sensitive` / `not-verifiable-here`)
   → terminal → **skip**, even if some other word looks deferral-ish. A live sweep
   re-picked a *terminal security escalation* because its prose ("不在本轮自动修") tripped
   the deferral regex, which used to be evaluated first.
3. Only then: a last agent comment matching `排队中|本轮未做|留开放|不在.{0,8}范围|非单 PR|多.?phase.*(留开放|不在)|candidate-queued|queued|in-progress|稍后|will do|TODO`
   → re-process. Note **`排队中`, not bare `排队`** (bare 排队 matched prose describing
   GitHub Actions queueing), and **no bare `不在本轮`** (it matched the terminal
   `不在本轮自动修`).

The executable form of all three predicates lives in `test/sweep-golden/lib.ts` and is
unit-tested (`golden.test.ts`, incl. each live misclassification above as a regression).
(Going forward the skill no longer emits these; this clause also unsticks the backlog already
frozen by old runs.) The suggestion-list gate above has its own executable form —
`containsSuggestionList()` / `hasIndependentConfirmation()` / `suggestionListIsActionable()` in
the same `lib.ts` — exercised by the `#1863` archetype fixture
(`test/sweep-golden/fixtures/1863-audit-suggestion-list-not-instruction.json`).

## Step 3 — For each kept issue, run the issue-review engine + act

Hand the issue to `issue-review` (read issue + referenced docs + **verify each
claim against live code/tests**, `path:line` or NOT FOUND). Then act per the
human's latest comment — this is `issue-review`'s resolve phase:

### Bounded per-issue orchestration（无人值守默认并行）

候选集确定后,主控按以下契约运行 `issue-review`:

**并发配置（repo × skill）:**按优先级取值:显式 `--concurrency <N>` →
环境变量 `AGENTLOOP_SKILL_CONCURRENCY`（fleet driver 从 `repos.json` 当前 repo 的
`skillConcurrency["issue-sweep"]` 注入）→ 默认 `3`。值必须是 `1..16` 的整数,非法值
直接报配置错误;当前 runtime 可用 agent slot 更少时向下收敛,没有非交互 agent 能力时
降到 `1`。这个值限制 **active issue workers**,不是本轮最多处理多少 issue。

1. **主控分配,worker 即时 claim。** 主控只维护去重后的候选 queue 和空闲 slot,
   **不预加** `agent:processing`。某个 slot 准备立即开工时才把一条 issue 交给 worker;
   worker 的第一个动作是运行 `issue-review` Step 0,自行重验
   open/hold/blocked/`agent:processing` 并 acquire。抢锁失败就返回 `SKIP_LOCKED`,
   主控继续投递下一条。未进入 active slot 的候选保持未锁,避免排队超过 TTL。
2. **一个 worker 只拥有一个 issue。** worker 运行完整 `issue-review` engine,只写该
   issue 的 comment/label/PR/branch;不同 issue 之间不得共享可变任务状态。
3. **会改 repo 的 worker 必须使用独立 worktree。** comment-only / research / idea /
   triage 等只读代码的 worker 可共用主 checkout;任何会 edit/format/test/commit/push/
   open PR 的 worker,开工前从最新 `origin/<default_branch>` 建独立临时 worktree。
   主控把由已核验执行计划得到的结构化 `allowedPaths` 一并交给 `issue-review` worker；worker
   在 claim 成功后、创建/写入 worktree 前运行
   `bun <plugin_root>/scripts/check-pr-path-overlap.ts --run-args '{"allowedPaths":[...]}'`。
   不得从标题猜路径。checker 的 `overlap` 要报告 PR/文件，`clean` 才能继续；`unavailable`
   必须停止实现并显式回报，不能塌陷为“无重叠”。
   **worktree 必须建在 `$AGENTLOOP_WORKTREE_BASE` 下,禁止硬编码 `/tmp/...`**——fleet
   driver 已把这个变量注入 worker 环境(专属 agentloop 的固定目录,不是系统 `/tmp`、
   也不是部署方的 `TMPDIR`),字面照抄即可:
   ```bash
   git -C "$(pwd)" worktree add --detach \
     "$AGENTLOOP_WORKTREE_BASE/$(basename "$(pwd)")-issue-<N>.$$" \
     origin/<default_branch>
   # 本 issue 处理完(无论成功/失败/跳过)务必清理,别只指望 driver 下一轮的兜底清扫:
   git worktree remove --force "$AGENTLOOP_WORKTREE_BASE/$(basename "$(pwd)")-issue-<N>.$$" 2>/dev/null || true
   ```
   硬编码 `/tmp/...` 会绕开部署方的 `checkoutBase` 配置,在系统盘上越攒越多——实测:
   未做限制时一天在 `/private/tmp` 下堆了约 36G 孤儿 worktree,而配置的外置盘却几乎
   是空的。**driver 每轮都会兜底清扫一次 `$AGENTLOOP_WORKTREE_BASE` 下超过 15 分钟、
   且没有活跃进程的残留**,但那是安全网,不是借口——worker 自己清理不了的话,残留
   至少要撑到下一轮才会被回收,别指望它替代及时清理。

   > **★ 同理:不要用会在 `<repo>/.claude/worktrees/` 下建树的 harness 工具**(编码 harness
   > 自带的 worktree/隔离开关)。那个位置**不由 `$AGENTLOOP_WORKTREE_BASE` 管**,而且它建的树会
   > **一直 check out 着 `claude/issue-<N>` 分支**——于是下一次(任何机器、任何轮次)对同一个
   > issue 跑 Step 4 的确定性认领 `git checkout -B claude/issue-<N>` 会 **fatal: already used by
   > worktree at …,exit 128**,这个 issue 号就此永久锁死。实测(arc,2026-08-20):一个 base clone
   > 里攒了 16 棵,占 60G,横跨 15 天;已在隔离环境复现过 exit 128。driver 从 0.29.4 起也会兜底
   > 清扫这些位置(clean 的才删,脏的留下并报告),同样是安全网不是借口——**照上面的
   > `$AGENTLOOP_WORKTREE_BASE` 显式建、显式删**。

   分支仍严格使用 Step 4 的
   `claude/issue-<N>`（或 phase 变体）。禁止
   多个写 worker 在 sweep 主 checkout 中切分支或改文件。进入 worktree 后读取
   `AGENTLOOP_SETUP_COMMAND`（fleet driver 从当前 repo 的 `setupCommand` 注入）并在
   该 worktree 执行;未配置时按 repo profile/toolchain 完成等价 bootstrap。setup
   未成功不得编辑、测试或声称可验证。
4. **共享 KB 由主控单写,worker 仍贡献 KB。** 并行 sweep 调用 `issue-review` 时,
   本条显式覆盖其“收尾直接编辑 KB body”:worker 只返回结构化结果（issue、disposition、证据、
   PR/claim、KB 增量、错误）;共享 KB body、整轮汇总、presence heartbeat 由主控在
   barrier 后统一写,避免 read-modify-write 覆盖。worker 不直接编辑共享 KB body。
5. **失败隔离 + ownership-safe 清理。** 一个 worker 失败不取消其他 worker。正常
   收尾由 worker 按 `issue-review` Step 7 release 自己的 `agent:processing`;主控
   **绝不代删**无 owner token 的 label 锁,worker 崩溃留给 TTL 恢复。写 worker 成功且
   worktree clean 后移除临时 worktree;失败且有未提交内容时不 force-delete,返回路径、
   branch、status 和 blocker 供恢复。不得把半成品汇报为完成。
6. **嵌套 fan-out 也受 runtime 总 slot 限制。** `--concurrency` 只限制 active
   issues;Research 等 issue 内部要再开 subagent 时先看 runtime 剩余 slot,不足就
   在该 worker 内 inline/串行,不得因 3 个 issue 各自再扇出而突破 runtime 上限。

主控只在候选分配与最终共享写入处串行;issue 的取证、实现、验证和 per-issue GitHub
写入在上述边界内并行。并发上限是安全阀,不是本轮处理上限:worker 完成后继续从
unclaimed queue 取下一条,直到候选耗尽或触及真实的运行预算/外部限流。

| Human's latest reply | Action |
|---|---|
| Agrees to **delete** a `deprecated` doc-audit ("可以删除"/"同意删除") | **Delete PR**, but **safe-delete only** — `git grep` for live refs first. Live code/test/doc dependency, a still-needed sub-package, a pending third-party confirm, or a blocking precondition → **do NOT delete; leave a comment** explaining the blocker. Relabel `status:*`→`status:deprecated` if needed. |
| Asks to **update** a `drifted` doc ("update 文档"/"补齐发 pr") | First check doc kind: **`planning/`/`intent/` docs default to historical-archive, NOT doc-update** (2026-07-17 policy, see issue-review「historical 归档」— shipped planning docs are historical artifacts; syncing them to code is negative-ROI, they re-drift immediately). Tombstone banner PR, no content rewrite; extract still-valuable rationale into `docs/guides/` first if any. Only `docs/` living guides get doc-update: decide **doc-drift vs code-drift** (verify the shipped surface); if doc-drift, edit the doc to match shipped reality, each addition checked against `path:line`. **Doc-update PR**, no code change. If another human is already drafting a PR for the same cluster, **skip to avoid collision**. |
| Approves a **bug fix** ("同意"/"easy fix") | Implement the fix; verify locally where possible (typecheck / targeted test). **One PR per bug.** |
| **Feature / design request**(multi-phase / 架构 / feature) | **先判大小(match vehicle to size):小而明确的 feature(单点、验收清晰、代码可触达)= 直接 reproduce→fix→test→PR,不启动 design-review/build-phases(那套对 leaf 是杀鸡用牛刀);只有真正多阶段/架构级才走下面的 pipeline。** **评估 → 能做就做,绝不冻结。** 旧的「(1) 只发计划、不写码 →(2) 等 human 确认才执行」是 bug:卡在等确认,而那个确认基本会变成「你先评估试试」,于是永远不动。改为:**(a) 评估(必做)** —— 读 issue + 引用代码,判「本环境能否自主起步」:issue 自带 spec/验收标准 + 代码可触达 = 能起步(绝大多数 feature 属此)。**(b) 能起步就直接跑 pipeline,不等确认**:`/agentloop:design-review` 定/优化方案(精炼计划 post 回 issue;**post 的设计必须 grounded:现状断言 `path:line` 坐实、代码权威优先于文档并指出文档过时、数字实测或显式标注估计——design-review 的事实+数字 grounding 是 HARD GATE,别手 post 未审的设计**)→ `/agentloop:build-phases` 分阶段实现(phase = issue checkbox;每 phase 一 commit + 进度评论;PR 按耦合切——见 design-review/build-phases「Issue-driven plans」)。issue 即 source of truth;**drive 能做的 phase 到完成**;跨 hourly run 的用 **in-progress** 续做(round-aware 接力 phase N→N+1,不重做、不冻结)。**(c) 只有真正 human-only fork 才停**(无法判定的架构 A-vs-B、安全、不可逆),且停时给**评估结论 + 具体待决项 + 你的推荐 + 已完成的 phase**,**绝不写「不在本轮范围 / 留开放」**那种冻结性 disposition。**(d) 撞墙 → 给详细问题**:实现中卡住,贴**具体 blocker(试了什么、什么失败、确切缺哪个决定/信息)**作为 in-progress 续做点,不是含糊的「需人定方向」。Never skip 评估;never freeze。 |
| **Research 请求**(`research` label / `[research]` 标题,如「研究 perkeep 和 did space 的结合点」;actionable 信号常是 **body 本身、0 comment**) | 走 [`issue-review` ★ Research](../issue-review/SKILL.md):**调研这一轮不改 repo 代码、不开 PR**(转进 feature 管道后不受此限)。并行 fan-out 两个 subagent 双侧代码级调研(外部 repo shallow clone 到 scratchpad + 本 repo `path:line`)→ 综合成一条证据化 comment(TL;DR 逐条答 issue 问题 + 对照表 + 冲突面 + 结合点分档 ⭐/◐/✗ + **行动声明收尾** + 外部链接)→ 挂 `research`(`needs-human-confirm` 只在真分叉时加)。**默认只留 comment + 链接,不下载保存数据**;仅当 issue 明确要求收集数据入库时才在 `research/<task-slug>/` 开目录(走人签名 PR)。**首轮不自动开 spin-off**;人选定方向后下一轮按选项拆自足 feature issue 或转 `/agentloop:design-review`→`/agentloop:build-phases`。**例外(#1947 反馈):issue 同时含明确终局目标(「不可动摇的目标」式表述)→ research 只是 phase 0,调研 comment 后立即按上面 feature 行转执行管道(sub-issue 图 + 能做即做),绝不以「待拍板」收尾;拍板项必须互斥,非互斥的是依赖序直接做,真分叉用「推荐 + 默认执行的异议窗口」。** **★ ratchet 收尾(铁律 10,2026-08-20):纯调研也不许挂在「待人选方向」——comment 末尾必须是「下一轮我会做 X,除非你说不」,下一轮人没否决就直接执行 ⭐ 档,禁止再调研一遍。`needs-human-confirm` 只贴真分叉(互斥且不可逆)。** |
| **Idea 提案**(`idea` label / `idea:` 标题;actionable 信号常是 **body 本身、0 comment**) | 走 [`issue-review` ★ Idea](../issue-review/SKILL.md):**首轮不当指令,当提案——不改 repo 代码、不开 PR、不开 spin-off**。理解复述(价值主张分解)→ 对照代码找「地基已有/真实缺口/与现有矛盾」(每条 `path:line`)→ 价值分档 ⭐/◐/✗ → 澄清问题(**每条附「没人答时按哪个默认走」**)→ **以行动声明收尾**(「下一轮我会做 X,除非你说不」)→ 挂 `idea`。**★ ratchet(铁律 10,2026-08-20 老冒反馈):首轮只有一次——第二轮起人没否决也没改方向,就直接执行上一轮声明的 X(拆 spin-off + 写边 + 开工),禁止再写一篇「更完整的评估」;「仍需拍板」不是合法收尾。`needs-human-confirm` 只贴真分叉(互斥且不可逆),能给安全默认的一律不贴。****拿不准「指令还是想法」就按 idea 处理**(clarify 的代价远低于执行错方向)。**例外(#1949 反馈,镜像 Research 行):idea 作者已把需求/目标说清(只是路径/细节未定)→ 评估 comment 后立即按 feature 行转执行管道——可默认的决策直接选定(ratchet + 异议窗口)、终局验收清单写进父 issue(close 唯一条件,子 issue 全关 ≠ 完成)、全量拆自足 spin-off + 写边,能做即做;不出拍板菜单,拍板项必须互斥,详见 issue-review ★Idea 铁律 9。** |
| PR already **merged** but issue still open | **Close** it (`completed`). `Fixes #N` usually auto-closes; close manually if it didn't. **★ 例外(完整测试闸,Robert 拍板 2026-07-20)**:多 phase / 带 sub-issue 图 / 带终局验收的大块 issue,merge 齐 ≠ 可 close——close 前必须有真实 surface 的完整端到端场景测试报告(见 [`issue-review` ★父级 rollup](../issue-review/SKILL.md) 第 3 步测试闸);缺则先补测试再 close。 |
| **父级 rollup**（Step 0.5 `rollupCandidates`：open 父 issue 的孩子已全部关闭） | 走 [`issue-review` ★父级 rollup](../issue-review/SKILL.md)：`claim.ts` fencing 抢到才做 → 核对父 issue 验收标准/问题清单（逐条对应到子 issue/PR 证据）→ 综合 comment（带 rollup marker）→ **全覆盖则 close**（这是「自动 close」的显式例外，但 **`agent:hold` 一票否决 close**：hold 禁止一切终态动作，优先级高于本例外——照常综合 comment，但留开等人摘 label），有残留 gap 列出并留开。research/idea 类父 issue 同样综合后 close——结论已在子 issue 落地，父级只是收口。 |
| Conditional / asks a third party to confirm / **security-sensitive** (e.g. P0 security) / needs a **genuine** A-vs-B decision / "要人类 review 不要完全用 ai" | **Comment only** — surface the finding + the decision needed; do not act. **「A-vs-B」指真互斥且不可逆的分叉**——能给安全默认的不算(Step 5.5 硬前置 + ★Idea/★Research 铁律 10:给推荐 + 异议窗口,照做);**把依赖序包装成拍板项交给人是禁止的**。这一行的 comment-only 不覆盖那种情形。 |

Every finding/action carries reproducible evidence (`path:line`, grep hit, real
test output). One verdict/PR-link comment per issue — don't stack duplicates.

## Step 3b — Autonomous autofix (`--autofix-green`): no human reply needed

The default sweep only touches issues with an **unprocessed human reply**. With
`--autofix-green`, *also* consider open issues that have **no human input at all**
— e.g. the auto-generated audit spin-offs — and **fix the ones the
agent can fix end-to-end without a human in the loop**. The whole idea: many small,
unambiguous, *verifiable* gaps don't need a person to approve them — reproduce →
fix → test → PR, one at a time. But the bar for "no human needed" is high, and
**verifiability is the gate**, not cleverness.

### Triage every candidate into 🟢 / 🟡 / 🔴

A 🟢 issue must pass **all four** gates:

1. **Unambiguous** — the fix is determined; no design decision, no A-vs-B, no "should we even do this".
2. **Verifiable in THIS environment** — there is a test or repro you can *actually run* and watch go fail → pass. No runnable proof ⇒ not green. (This is the gate that disqualifies most things — see below.)
3. **Low blast radius** — a leaf fix (one handler, one wire field, a missing test, a polyfill). Not core-architecture, not a cross-cutting contract, not a public API shape.
4. **Not security-sensitive** — crypto, auth, token compare, access control, path-traversal guards stay human even when "obvious".

🟡 = mechanical but **fails gate 2 or 3**: e.g. native Swift/Kotlin code in a sandbox with no Xcode/Android SDK (can't build/test), or a change that touches CI/build config or a broad surface. → write the fix, open a **draft PR** with evidence, label `needs-human-review`, and **say plainly it is not verified here**. Never auto-merge, never claim a green check you didn't run.

🔴 = needs design direction / architecture / security → **comment only** (the existing Step 3 red row). Do not touch code.

### The verifiability gate is environment-dependent (and that's the leverage)

The same issue can be 🟡 in one environment and 🟢 in another. In a **TS-only sandbox**, `bun test`/`tsx` run, so TS-side issues with a conformance/unit test are 🟢 — but every Swift/Kotlin parity issue is 🟡 (can't compile). On a **machine that can build all platforms** (`swift test`, `./gradlew test`, YAML-runner-vs-native-server), those native parity issues move 🟡 → 🟢. So the realistic auto-fix coverage ≈ *the fraction of the backlog you can prove a fix for right here*. State which environment you're in and which gate it opens.

### Env-capability probe (multi-machine claiming)

Multiple machines run this sweep (cloud routine + one or more local checkouts), and they don't all
have the same toolchains — one local Mac may have a full Swift+Android native build/test
environment, a cloud sandbox is typically TS-only. Before claiming a candidate whose verifiability
depends on a toolchain, run `<capability_probe_script>` (probes actual usability, not just
binary presence — e.g. `native-android` checks the SDK dir + `java`, not just `$ANDROID_HOME` being
set, because that env var is commonly unset in a fresh shell even when the SDK is installed) and
compare against the issue's declared requirement, marked in its body as:

```
<!-- requires: native-ios,native-android -->
```

- **Capability present** → proceed normally (🟢/🟡 gates above still apply).
- **Capability declared but missing here** → this is a *this-environment* verifiability gap, not a
  design gap: leave the issue untouched and silent (per the "🟢 candidate, no capacity this round"
  silence rule below) rather than downgrading it to 🟡/`needs-human-review` — a different machine's
  next sweep run may have the capability and should still find it as a fresh candidate.
- **No `requires:` marker** → assume TS-only (the safe default); a human or a prior agent run can
  add the marker retroactively once a capability gap is discovered (as this issue-sweep run just did
  for several native-parity spin-offs it dispatched with a fully-available Swift+Kotlin toolchain).

#### A capability gap must be PROVEN first-hand THIS run — never inherited (hard rule)

The silent-skip and 🟡 "not verifiable here" dispositions are only sound once you have **actually
established the gap on the machine you are running on now**. A "capability-gap → leave silent" (or
🟡/`needs-human-review`) disposition is **inadmissible without a first-hand probe result from THIS
session** demonstrating the specific gap. The failure this prevents is real: a run declared "no live
environment here" and silently skipped a whole cluster **without probing** — the probe later showed
the live env was reachable over HTTP with valid creds; only the headless browser was blocked. Right
conclusion for part of the cluster, unsound derivation, and it left evidence-comment value on the
floor.

- **Never infer the gap from second-hand sources.** Not the environment banner, not the "cloud
  routine" framing, and — the trap — not *other machines' or prior rounds' comments on the very
  issue you are triaging* ("no CF creds", "can't start daemon", "browser can't reach it"). Those
  describe a **different runtime at a different time**; this fleet is heterogeneous (the whole
  premise of this section). A capability claim copied from a comment is not a capability check.
- **Probe the axis that actually gates the work, not a scalar prior.** Capability is
  high-dimensional: *live-HTTP reachable* ≠ *headless browser reachable*; *some creds present* ≠
  *the specific cred this task needs*; *`gh` binary exists* ≠ *`gh` REST works here*. A blanket
  "cloud sandbox = no live env" is wrong in exactly the details where the decision lives. Run the
  concrete check for the specific capability (`curl` the live endpoint, launch the headless browser
  once, test for the exact cred) before claiming it is absent.
- **Bias toward probing, because a false "can't" is self-concealing.** A false "I can't verify this"
  exits down the sanctioned silent-skip path and leaves **no trace that a judgment was even made**,
  so it is never caught or retried. A false "I can" fails loudly and self-corrects. When you do skip
  for a capability gap, **the probe result IS the required evidence** — cite it, exactly as any other
  disposition must carry its evidence.
- **A gap in one capability is not a pass on all analysis.** Browser-blocked ≠ un-analyzable: many
  "broken UI" issues root-cause cleanly from source + a live *HTTP/diagnostics* read that may be
  reachable even when the browser is not. Prefer a grounded evidence comment over a silent skip
  whenever *any* available first-hand signal grounds a root cause.

This is Phase 1 (the probe script + the marker convention). Wiring automatic skip/claim logic into
Step 1/2's candidate loop, and adopting the same convention in `pr-sweep`, is Phase 2 — not yet done.

### The 🟢 pipeline (one issue at a time, serial)

0. **Verify the issue's PREMISE first — this is a hard gate (a real
   false-premise trap; archetype: `test/sweep-golden/fixtures/535-false-premise-trap.json`).**
   AI-authored spin-off issues carry their own evidence (`grep`/`path:line`)
   and a stated framing ("this is dead code" / "pure placeholder" / "3 lines,
   just delete"). **Re-run the issue's own grep AND broaden it** before
   trusting the framing: search for *who else depends on the thing the issue
   wants to change* (snapshot fixtures, importers, callers, generated refs).
   If reality contradicts the premise, **STOP and downgrade to a comment**
   carrying the counter-evidence + safe options, do NOT execute. A false
   premise turns a "🟢 mechanical" task into a regression. The issue author
   (an agent) did not run the broader grep; you must.
1. **Reproduce first.** Write/locate a test that fails *for the reason the issue states*, run it, capture the real failing output. If you can't make it fail on demand, you can't prove a fix — downgrade to 🟡.
2. **Fix** minimally; prefer backward-compatible (`x ?? legacy`) over a swap.
3. **Verify**: the new test passes, the package's full suite shows **no regression**, `check-types` is clean. Paste the real before/after numbers into the PR. **Isolate pre-existing red from your red:** packages here are often already failing (e.g. afs-ui had 88 CSS-snapshot failures; runtimes/node had a pre-existing `auth/index.ts:151` type error). Before attributing a failure to "pre-existing", *prove* it — either `git stash` your change and re-run (count must be identical) or show it's logically untouchable by your diff (a one-line test-mock edit cannot break CSS snapshots). Then say so explicitly in the PR with the real numbers.
4. **One branch + one PR per issue**, body references the issue (`Part of #N`; use `Fixes #N` only if the PR fully closes it — partial fixes leave the issue open and say which part was handled).
5. **Never auto-merge.** 🟢 means auto-*PR*, not auto-*merge* — the `pre-merge` verification gate + a human gate the merge (there is no CI on the PR path). "No human intervention" is about the fix work, not the merge decision. This is a categorical, non-negotiable assertion — see `test/sweep-golden/fixtures/1025-forbidden-auto-merge.json` for the forbidden-action regression test.

### White-list, not black-list

Only auto-touch code for explicitly safe categories: TS/pure-function bug with a test, wire-format/field-name mismatch with a conformance spec, a missing-test addition, a doc/type/lint fix, a dependency-already-in-repo polyfill. Anything outside the list defaults to 🟡/🔴. When unsure, downgrade.

**Proven 🟢 patterns and run history:** see this repo's case-law appendix
(repo-profile Case Law References) for the white-list categories that have actually landed (dead not-found
branch → explicit error, empty `catch{}` in test mocks, env-gate cleanup with
a human directive) and the run-by-run track record.

### AI-agent spin-off issues (`<!-- spinoff-of: #N -->`) — the primary autofix target

The bulk of this backlog is **agent-authored spin-off issues**: their body opens with a `<!-- spinoff-of: #N … -->` HTML comment and follows a fixed shape (目标 / 现状证据 with grep / 参考实现 with `path:line` / 具体任务 / 验收标准). They are almost always **0-comment** (no human ever replied) — so they are invisible to the default sweep and only get picked up under `--autofix-green`. **This class is the whole reason `--autofix-green` exists; process it aggressively but gated.**

Per spin-off issue:

1. **Read the `spinoff-of` parent.** The parent (the original doc-audit) often carries the human directive that makes a child green (e.g. a parent issue that had already said "可以彻底清理"). A human "go" on the parent counts as approval for the unambiguous child.
2. **Run premise-verification (🟢 pipeline step 0).** The issue's own grep is necessary but not sufficient — broaden it.
3. **Triage by environment, then by the four gates.** In a TS-only sandbox the realistic green set is: TS/pure-function fixes, test-fi

…(truncated)
