SuperAPI AI Directory

issue-sweep

General & other

View repository

Updated 10-15 19:57

About this skill

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 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.

★ Unattended iron rule (cron routine / /loop — the default operating mode of this skill): never call any tool that waits for the user —AskUserQuestion、Workflow(requires interactive opt-in confirmation),EnterPlanMode(exiting requires user approval). With no one to respond → the entire routine hangs forever (observed in practice: the sweep routine got stuck all night on the "whether to run workflow" question). For questions that need a human decision, follow the design-review Autonomous escalation paradigm: post the options + your recommendation + the blocked content as a comment (attached to needs-human-confirm) on the corresponding issue,then continue processing the next item。What is forbidden is interactive orchestration that waits for confirmation, not parallelism itself: in an unattended environment, prefer subagent/agent fan-out that requires no opt-in and does not wait for the user; only if the current runtime lacks this capability, fall back to serial inline. The repo hook (.claude/hooks/deny-interactive-unattended.py) will hard-deny these three tools in unattended sessions as a backstop — being denied means you are in an unattended environment, so follow this discipline and do not retry.

Output language and writing standards (Chinese, faithful, expressive, elegant), same as issue-review。 All team-facing output — issue/PR comments,PR/issue description body, issue titles, triage explanations — is all in Chinese; code identifiers, paths, commands,path:line, and test output stay as-is.PR and commit titles must be entirely in English— full Conventional Commits (type(scope): english description, the description after the colon is also in English), with no Chinese left in them; issue titles stay in Chinese.Do not pile up: give a one-sentence conclusion first, then the minimum but sufficient evidence (docs / code path:line / real test output;screenshots required for anything UI-related); collapse long logs into <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).

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:
git stash push -u -m "issue-sweep preempted"

Then, tree clean:

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 — deterministic graph computation (issue-graph, must run every round)

Before label scanning, run the graph computation once (read-only, REST-only, takes seconds):

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

Consume its three outputs:

  • kicks → merge directly into the candidate set, no human comment needed. This is a structural patch to the Step 2 predicate: a child issue closing is a state change and produces no human comment, so the old predicate can never see "the child is done, the parent should wrap up / the sibling has been unblocked". kick makes close events propagate deterministically.
  • rollupCandidates(an open parent whose children are all closed) → go through issue-review ★parent rollup (fencing mutual exclusion + acceptance check + synthesis comment + close). agent:hold has veto power over close: a parent issue with hold can still get a synthesis comment, butnever close— hold forbids all terminal actions, with higher priority than rollup's "close if fully covered" exception (observed live: #1104 was simultaneously a rollup candidate and carried hold, so the two rules directly conflicted). Leave it open, and wait for someone to remove the label.
  • blocked → deterministic SKIP(anything with an open blocker does not even enter the candidate set; just record the reason this round). A blocked issue no longer relies on the model guessing "whether it is not yet its turn".

ready The order has been rotated by hostname (so multiple machines starting in the same minute stagger and reduce lock contention). The graph only decides "who enters the candidate set and who is skipped" — injected candidates still go through the Step 2 predicate, Step 3 dispatch, and lock and claim checks, bypassing none of the existing discipline; an issue with no edges (opened by hand) = an isolated vertex in the graph, and still goes through label/catch-all as usual.

agent:ready label consumption (when the producer routine maintains it): the producer runs periodically producer.ts to materialize kick/rollup events into agent:ready label (a human-observable queue view). sweep canprioritizeclaiming from gh api "repos/{owner}/{repo}/issues?state=open&labels=agent:ready" but three iron rules: ① label is only an index hint; after claiming you must go back to GitHub and re-verify(still open, still holds on the graph, no hold, no unprocessed human input—re-verification is this round's graph-scan + Step 2 predicates); ② once processed (final disposition settled) the consumer removes agent:ready—the producer only adds and cleans up invalid ones (closed/hold/blocked), it does not know "is the work done or not"; ③ producer dies = label stale or missing,degrade back to this section's graph-scan self-computation, behavior unchanged, no single point.

agent:ready vs needs-human-confirm conflict—adjudicate by [order of labeling] (authoritative signal). When an issue carries both agent:ready(should do) and needs-human-confirm(waiting for human),do not guess from comment content (human and agent comment formats are indistinguishable, see Step 2)—read the label event timeline: gh api "repos/{owner}/{repo}/issues/{n}/events", take the labeled/unlabeled event (with actor + created_at, GitHub authoritative record, unforgeable).whoever labeled last wins:

  • agent:ready later than needs-human-confirm(especially when a human labels it personally) → human has confirmed/unblocked → start work(take over implementation).
  • needs-human-confirm later than agent:ready → human pressed pause after ready → wait for human, no final-state action this round.
  • only one of them → follow that one; one that is later removed by unlabeled → no longer counted. Reference implementation labelStance()(test/sweep-golden/lib.ts)。live arc#1722: Phase 4 plan is ready, carries agent:ready, but needs-human-confirm was labeled later → correctly judged as "waiting for human to decide axis B", and once the human confirms (removes the label or labels ready) take over immediately.

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 (must scan every round—the label union is naturally blind to them): issues casually opened by founder/humans often 0 label、0 comment, the actionable signals are all in the body. Real miss: a feature issue (no label), a research issue (no label—the hourly sweep ran as usual within 2 hours of creation, but label scanning will never see it, only a human can manually /agentloop:issue-review name it). The "add label when scanned" written in the research/idea row above, under pure label scanning, is acircular dependency (no label means it can't be scanned, can't be scanned means no one adds a label)—this catch-all channel breaks it.do not rely on humans to add labels, append a full open list once per round, and fish out the ones "without any candidate label":

# 一次列全部 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)"'

this command stably fishes out two-digit-number open issues invisible to label scanning, all work items opened directly by the founder—a systemic gap, not a single sporadic case (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.

for each one fished out: read title+body to judge work-type(调研/研究/[research] → research; idea:/proposal tone → idea; with spec/acceptance criteria → feature; error/reproduction steps → bug), first add the corresponding label(reversible triage, do it automatically—so that from the next round it enters normal label scanning, and the Step 3 dispatch row has the correct work-type), then merge into this round's candidate set and go through Step 2/3.Even ones whose type can't be determined are not silently skipped:post needs-human-confirm + leave a triage comment listing your guesses so a human can confirm with one click — silent skipping = this issue is permanently invisible to automation, which is exactly the state this channel exists to eliminate.

Then drop the reserved/locked ones (concurrency coordination, see issue-review ★concurrency lock):

  • agent:hold — human reservation = terminal-state freeze, not processing freeze("don't take irreversible actions without my feedback", not "ignore it"). During holdnever close / never take terminal action on its behalf, until a human removes the label; butnew human comments still enter the Step 2 candidates— a human speaking specifically on a held item is precisely the highest-priority input, and must be read and responded to (reply to the comment / do the work the human explicitly asks for). Only held items with no new human input are skipped.
  • agent:processing(fresh) — currently being processed by another run. For each candidate, read whether it has this label; if so, check the lock age (see below),within TTL 30min → SKIP(someone else is doing it),expired → don't skip(the previous runner crashed, Step 3 will re-acquire the lock). Lock age is taken from the last labeled event time for that label:
    gh api --paginate repos/{owner}/{repo}/issues/<N>/timeline \
      --jq '[.[]|select(.event=="labeled" and .label.name=="agent:processing")]|last|.created_at'
    
    (no gh use mcp__github__* to read the timeline.)agent:processing is advisory —hard dedup still relies on Step 4's deterministic branching + claim check; this step just short-circuits early, saving redundant reads/verification/tests.
  • epic-managed — this issue belongs to an epic exclusively driven by epic-conductor(see epic-conductor)。exclude the whole thing: no triage, no claim, no work, no comment. This is not agent:hold(frozen terminal state but still responds to human comments) — epic sub-issues are handled end-to-end by that conductor scheduling its own batch of workers, and sweep interfering would only cause collisions and duplicate PRs (archetype:#3407 collided with #3395).agent:hold is "human reservation",epic-managed is "another agent's reservation"; both skip claiming, but epic-managed doesn't even have human comments answered by sweep on its behalf (that's the conductor's job). One rule covers all present and future epics.Filter out all sub-items of a specific epic:gh issue list --label "epic:<父issue#>"(applies to both issues and PRs).

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 discovered 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 "suggested close"/ "suggested merge"/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 "the 'suggested close' column was not auto-closed this time, just wait for a human to glance at the table and batch-close" (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 suggested-close-list rule 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"/"thanks" 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 — "🟢 queued / not done this round / left open / candidate-queued / in-progress" — is UNFINISHED, not terminal → treat the issue as still to-process and DO it this run (per Step 3b: do it if possible, don't queue; archetype: test/sweep-golden/fixtures/533-non-terminal-deferred.json). This is the fix for the self-freeze the user hit: a "I'll do it later" 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 ("not auto-fixed this round") 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 queued 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 (unattended defaults to parallel)

Once the candidate set is determined, the main control runs under the following contract issue-review:

**Concurrency configuration (repo × skill):** values are taken by priority: explicit --concurrency <N> → environment variable AGENTLOOP_SKILL_CONCURRENCY(injected by the fleet driver from repos.json of the current repo's skillConcurrency["issue-sweep"] ) → default 3. The value must be an integer 1..16 ; an invalid value is directly reported as a configuration error; when the current runtime has fewer available agent slots, converge downward; when there is no non-interactive agent capability, fall back to 1. This value limits active issue workers, not how many issues are processed at most in this round.

  1. The main control assigns, and the worker claims immediately. The main control only maintains the deduplicated candidate queue and idle slots, does not pre-add agent:processing. Only when a slot is ready to start work immediately is an issue handed to a worker; the worker's first action is to run issue-review Step 0, re-verify open/hold/blocked/agent:processing and acquire. If acquiring the lock fails, return SKIP_LOCKED, and the main control continues dispatching the next one. Candidates that have not entered an active slot remain unlocked, to avoid queuing beyond the TTL.
  2. A worker owns only one issue. A worker runs the full issue-review engine, and only writes that issue's comment/label/PR/branch; mutable task state must not be shared between different issues.
  3. Workers that modify the repo must use an independent worktree. Workers that only read code, such as comment-only / research / idea / triage, may share the main checkout; any worker that will edit/format/test/commit/push/open a PR must create an independent temporary worktree from the latest origin/<default_branch> before starting work. The worktree must be created under $AGENTLOOP_WORKTREE_BASE ; hardcoding /tmp/...is prohibited — the fleet driver has already injected this variable into the worker environment (a fixed directory dedicated to agentloop, not the system /tmp, and not the deployment party's TMPDIR); just copy it literally:
    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
    
    Hardcoding /tmp/... will bypass the deployment party's checkoutBase configuration and accumulate more and more on the system disk — measured: when no limit was imposed, about 36G of orphan worktrees piled up under /private/tmp in one day, while the configured external disk was almost empty.The driver will do a fallback cleanup once per round for leftover $AGENTLOOP_WORKTREE_BASE that are more than 15 minutes old and have no active processBut that is a safety net, not an excuse — if the worker cannot clean up after itself, the residue at least has to survive until the next round before it gets reclaimed; do not count on it to replace timely cleanup. Branches still strictly use Step 4's claude/issue-<N>(or the phase variant). It is forbidden for multiple writing workers to switch branches or modify files in the sweep's main checkout. After entering the worktree, read AGENTLOOP_SETUP_COMMAND(the fleet driver injects it from the current repo's setupCommand ) and execute it in that worktree; when it is not configured, complete the equivalent bootstrap according to the repo profile/toolchain. If setup does not succeed, you must not edit, test, or claim verifiability.
  4. The shared KB is written only by the controller; workers still contribute to the KB. When parallel sweep calls invoke issue-review , this clause explicitly overrides its "finish by directly editing the KB body": the worker only returns structured results (issue, disposition, evidence, PR/claim, KB delta, errors); the shared KB body, whole-round summary, and presence heartbeat are written uniformly by the controller after the barrier, to avoid read-modify-write overwrites. Workers do not directly edit the shared KB body.
  5. Failure isolation + ownership-safe cleanup. One worker failing does not cancel other workers. Normal finish is handled by the worker according to issue-review Step 7 releasing its own agent:processing; the controller never deletes on behalf of othersa label lock without an owner token; if a worker crashes, leave it for TTL recovery. After a writing worker succeeds and the worktree is clean, remove the temporary worktree; if it fails and has uncommitted content, do not force-delete; return the path, branch, status, and blocker for recovery. Do not report half-finished work as complete.
  6. Nested fan-out is also subject to the runtime's total slot limit. --concurrency only limits active issues; when Research and other issues internally want to spawn subagents again, first check the runtime's remaining slots, and if insufficient, go inline/serial within that worker; do not exceed the runtime cap because 3 issues each fan out again.

The controller serializes only at candidate assignment and final shared writes; an issue's evidence gathering, implementation, verification, and per-issue GitHub writes run in parallel within the above boundaries. The concurrency cap is a safety valve, not the processing cap for the round: after a worker finishes, it continues taking the next item from the unclaimed queue, until candidates are exhausted or the real run budget/external rate limit is reached.

Human's latest reply Action
Agrees to delete a deprecated doc-audit ("can delete"/"agree to delete") 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 docs"/"fill in and send PR") First check doc kind: planning//intent/ docs default to historical-archive, NOT doc-update (2026-07-17 policy, see issue-review "historical archiving" — 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 ("agree"/"easy fix") Implement the fix; verify locally where possible (typecheck / targeted test). One PR per bug.
Feature / design request(multi-phase / architecture / feature) First judge the size (match vehicle to size): a small and clear feature (single point, clear acceptance, code reachable) = directly reproduce→fix→test→PR, without starting design-review/build-phases (that set is overkill for a leaf); only truly multi-phase/architectural work goes through the pipeline below. Evaluate → do it if you can, never freeze. The old "(1) only send a plan, do not write code → (2) wait for human confirmation before executing" is a bug: it gets stuck waiting for confirmation, and that confirmation basically turns into "you evaluate and try first", so nothing ever moves. Change to:(a) Evaluate (mandatory) — read the issue + referenced code, and judge "can this environment start autonomously": the issue carries its own spec/acceptance criteria + code is reachable = can start (the vast majority of features fall into this).(b) If it can get started, just run the pipeline directly, without waiting for confirmation:/agentloop:design-review finalize/optimize the plan (refine the plan and post it back to the issue;the design of the post must be grounded: current-state assertions path:line must be nailed down, code authority takes precedence over docs and outdated docs must be pointed out, numbers must be actually measured or explicitly marked as estimates — design-review's fact + number grounding is a HARD GATE, do not hand-post an unreviewed design)→ /agentloop:build-phases implement in phases (phase = issue checkbox; each phase one commit + progress comment; PRs split by coupling — see design-review/build-phases "Issue-driven plans"). The issue is the source of truth;drive the phases it can do to completion; for those spanning hourly runs use in-progress to continue (round-aware relay phase N→N+1, no redo, no freeze).(c) Only stop for a genuinely human-only fork(an indeterminable architecture A-vs-B, security, irreversible), and when stopping providethe assessment conclusion + specific pending decisions + your recommendation + completed phases,Never write "out of scope for this round / left open"that kind of freezing disposition.(d) Hit a wall → give detailed questions: if stuck during implementation, post the **specific blocker (what was tried, what failed, exactly which decision/information is missing)** as an in-progress continuation point, not a vague "needs a human to set direction". Never skip assessment; never freeze.
Research request(research label / [research] title, e.g. "Research the intersection of perkeep and did space"; the actionable signal is often the body itself, 0 comments) Go through issue-review ★ Research:never modify repo code, never open a PR. In parallel fan out two subagents for code-level investigation on both sides (shallow clone external repos to scratchpad + this repo path:line) → synthesize into one evidence-backed comment (TL;DR answering each issue question + comparison table + conflict surfaces + intersection points graded ⭐/◐/✗ + options awaiting decision + external links) → attach research,needs-human-confirm。By default only leave a comment + links, do not download and save data; only when the issue explicitly requires collecting data into the repository do you open a directory in research/<task-slug>/ (via a human-signed PR).Do not automatically open spin-offs(research conclusions naturally need-decision); after a human selects a direction, the next round splits self-contained feature issues according to the options or switches to /agentloop:design-review→/agentloop:build-phases。Exception (#1947 feedback): if the issue also contains a clear end goal (an "immovable goal"-style statement) → research is only phase 0; after the research comment immediately switch to the execution pipeline per the feature line above (sub-issue graph + do it if you can), and never end with "awaiting decision"; decision items must be mutually exclusive, non-mutually-exclusive ones are dependencies done in order, and real forks use a "recommendation + objection window for default execution".
Idea proposal(idea label / idea: title; the actionable signal is often the body itself, 0 comments) Go through issue-review ★ Idea:do not treat as an instruction, treat as a proposal — never modify repo code, never open a PR, never open a spin-off. Restate understanding (decompose the value proposition) → compare against the code to find "foundation already exists/real gaps/contradictions with the existing" (each one path:line) → grade value ⭐/◐/✗ → clarifying questions specific enough to decide on + next-step options (without presupposing) → attach idea,needs-human-confirm. If information is insufficient, ask people to supply context in the next round and converge over multiple rounds; only after a human has decided the direction, split into self-contained feature issues or convert to /agentloop:design-review。If you are unsure whether it is an "instruction or an idea", treat it as an idea(the cost of clarifying is far lower than executing the wrong direction).Exception (#1949 feedback, mirrored in the Research row): the idea author has already stated the requirement/goal clearly (only the path/details are undecided) → after evaluating the comment, immediately follow the feature row into the execution pipeline — decisions that can be defaulted are chosen directly (ratchet + objection window), the final acceptance checklist is written into the parent issue (the sole close condition; all child issues closed ≠ done), fully split into self-contained spin-offs + write edges, do what can be done; do not produce a decision menu, decision items must be mutually exclusive, see issue-review ★Idea Iron Rule 9.
PR already merged but issue still open Close it (completed). Fixes #N usually auto-closes; close manually if it didn't. ★ Exception (full test gate, decided by Robert 2026-07-20): for large issues with multiple phases / a sub-issue graph / final acceptance, all merges ≠ can close — before close there must be a complete end-to-end scenario test report on a real surface (see issue-review ★Parent rollup step 3 test gate); if missing, add tests first, then close.
Parent rollup(Step 0.5 rollupCandidates: all children of an open parent issue are already closed) Go through issue-review ★Parent rollup:claim.ts fencing, do it only after winning the race → check the parent issue's acceptance criteria / problem list (map each item to child issue/PR evidence) → synthesize comment (with rollup marker) → if fully covered, close(this is an explicit exception to "automatically close", but agent:hold vetoes close: hold forbids all terminal actions, and its priority is higher than this exception — synthesize the comment as usual, but leave it open for someone to remove the label), list any remaining gaps and leave it open. research/idea-type parent issues are likewise closed after synthesis — the conclusions have already landed in the child issues, and the parent is just wrapping up.
Conditional / asks a third party to confirm / security-sensitive (e.g. P0 security) / needs an A-vs-B decision / "want human review, do not use AI entirely" Comment only — surface the finding + the decision needed; do not act.

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)

  1. 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.
  2. 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 🟡.
  3. Fix minimally; prefer backward-compatible (x ?? legacy) over a swap.
  4. 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.
  5. 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).
  6. 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 (goal / current-state evidence with grep / reference implementation with path:line / specific tasks / acceptance criteria). 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 "can be thoroughly cleaned up"). 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-file fixes, wire-format/conformance mismatches, doc/type/lint, env-gate cleanups. The rest of the standard backlog clusters stay non-green here:
    • Native parity ([parity], Swift/Kotlin) → 🟡 (can't build/test in TS sandbox; becomes 🟢 only on a full-platform host).
    • security / P0,security → 🔴 always (crypto/vault/ACL/const-time/path-traversal), comment-only even when "obvious".
    • Multi-phase feature/arch plans (feature, enhancement with Phase N) → not a single PR, butyou should also try to advance it automatically, not freeze it. Follow the Step 3 feature row's "evaluate → if it can get started, /agentloop:design-review → /agentloop:build-phases" pipeline,one issue, one in-progress relay line: each hourly run advances the phase it can advance (round-aware continuation), and only stops when it hits a genuinely human-only fork, giving the specific pending items.Never post "🟠 out of scope for this round / leave open" to freeze it to death— that category is exactly the root cause of the "never handled" cases that once really occurred.design-review/build-phases are themselves automated processes; feature issues should use them to run, not toss them back to humans.
  4. If doable, do it, do not queue — 🟢 candidate-queued delete this class (it is the root of the self-locking infinite loop). Once an issue is judged 🟢 (passes the four gates + verifiable in this environment), thenClaim on the spot + do it on the spot:cut deterministic branch claude/issue-<N>(Step 4's claim check) → reproduce→fix→test→PR. disposition = PR link(terminal state).
    • Never post comments like "🟢 queued / not done this round / left open / candidate-queued". It isunfinished work wearing the clothes of a terminal state: the next round's round-awareness sees "the last comment is an AI comment, the human hasn't replied," and judges it "already handled, skip," so this piece of workis never done. User-tested: a large number of issues were frozen to death by this kind of comment, leaving only a pile of "I'll do it later" with no further progress.If judged doable, do it right now; don't promise the future.
    • 🟢 that weren't reached due to insufficient capacity this round: leave no comment at all, keep them "unhandled" — the next round will naturally rediscover them and claim them to do. This isthe only correct "silence"(a 🟢 with no AI comment won't be frozen).
    • Only post "terminal disposition + skip" for those that truly have external unlock conditions: 🟡 not-verifiable-here (can't build/test in this environment → draft PR + needs-human-review), 🟠 needs-design, 🔴 security-human. These are only reasonable to skip to unlock (switch environment / human decides).
    • For each disposition, write clearly: class + specific blocker + unlock condition."silence is a failure mode" only applies to issues you judged "can't do it here / needs a human" yet said nothing about(those 31 without comments are exactly this kind that got missed); for 🟢 that are "doable but not reached this round," silence is instead correct —don't freeze it to death with a comment. Group those with the same reason (all native-parity, all security) and write them together, but still post one comment each.

issue found by test-sweep (test-sweep-failure/test-sweep-report label) — the second type of green candidate source with zero human input

test-sweep skill (post-deploy continuous QA) will directly open issues to record problems found on the live deployment:test-sweep-report is the summary parent issue of one round of walkthrough,test-sweep-failure is the child issue for each failure point (GitHub native sub-issue, attached under the parent issue —— not <!-- spinoff-of: #N body-marker convention). This type of issue, like the spin-off issue above, belongs to the same candidate pool of "opened by an agent, zero human input, possibly auto-fixable," but the discovery mechanism and premise-check requirements are both different, so handle them separately:

  1. Discovery method: label, not marker. scans all open issues, picks test-sweep-failure(preferred, specific to a single failure point) or test-sweep-report(summary parent issue, usually just an index, less often directly fixable),zero comments. Step 2's human-machine judgment has a dedicated exception for this type of issue (see the "Exception — zero-comment test-sweep-failure/test-sweep-report issues" above) — don't treat them as unhandled human input, and don't because they don't carry the spinoff-of marker, treat them as "invisible, skip".
  2. Premise-check is heavier than for spin-off issues: the verification target is the live deployment, not static code. The spin-off issue's premise-check is "re-grep a bit more broadly"; the premise-check for what test-sweep found isre-hit the same live site onceConfirm the phenomenon is still present—the test-sweep reports an observation from a certain review-time, which may have been fixed by a subsequent deployment, or may (as #2271 itself suggests in its recommendation) have been intentional design, not a regression.Do not just trust the report text and start changing things: If the URL/reproduction path given in the report can be hit, hit it (such as the raw.githack.com full report link and reproduction steps attached in the review report); if the phenomenon disappears → close the sub-issue directly (noting "retest passed; may have been fixed in a subsequent commit"), and do not treat it as 🟢.
  3. Before deciding on a disposition, first determine which side is wrong. The typical shape of a test-sweep report is doc-vs-code drift(.man/.aup declared behavior vs actual behavior mismatch) or a direct functional bug. After retesting and confirming the phenomenon is still present:
    • .man/.aup The documentation description is outdated and the code behavior is the correct one → doc-update PR(small change, corresponding to the handling in the Step 3 table row "asks to update a drifted doc", except the trigger is test-sweep rather than a human comment).
    • The code has regressed relative to the contract declared by the documentation → bug-fix PR, follow the standard 🟢 pipeline (reproduce→fix→test→PR).
    • Cannot tell which side is wrong, or product judgment is needed ("should this login wall really be here" — #2271's original words) → not 🟢; downgrade to 🟡/comment, list both possibilities, and do not make the product decision on their behalf.
  4. The whitelist is the same as above and is not relaxed. Only handle typical doc-drift or small bugs with a clear repro; findings involving permissions/security boundaries (even if they appear to be "just documentation not updated") are always 🔴 comment-only, and the threshold is not lowered just because the trigger is automation.
  5. One issue per PR,Part of #<test-sweep-failure 号>。 if multiple sub-issues under the same parent QA Report have the same source and same fix (such as the same gate component causing multiple blocklets to report at the same time), they can be fixed in one PR, but in the PR body list them one by one Part of #N every sub-issue covered, and do not miss any references.

Run history (first two --autofix-green runs, both TS-only sandbox — proof that the pipeline works end-to-end): see this repo's case-law appendix (repo-profile Case Law References).

Step 4 — Discipline (non-negotiable)

  • Deduplication: deterministic branch name + claim check before creation (eradicate duplicate PRs across machines). When multiple machines run this sweep in parallel, if the branch name is a descriptive slug invented by the model (old rule claude/fix-331-…), two machines forthe same issue will computedifferentbranch names → no collision → each opens a PR → duplication (see the case-law appendix in this repo, repo-profile Case Law References, for five real cases). There are two fixes, both indispensable:
    1. The branch name must be deterministic and derived only from the issue number (+ phase), without a model-invented slug: claude/issue-<N>(single PR); for multiple phases use claude/issue-<N>-p<phase>. Two machines compute the same namebranch → the second git push / gh pr create naturally collides and no longer double-opens.

    2. Run a claim check before opening the PR—if there is already an open PR pointing to #N, SKIP(someone else/another machine is doing it): claim check—run it as an ordinary command, then read the output; do not stuff it into a shell variable, do not wrap [ … ] && { …; exit 0; } (the sandbox guard will reject this kind of command substitution/compound structure, and this step will fail directly—this has been hit in live runs; and you can read the output anyway):

      gh pr list --state open --json number,headRefName,body --jq '.[] | select((.headRefName|test("(^|[-/])issue-<N>([-/]|$)|-<N>-")) or (.body|test("(Fixes|Part of) #<N>\\b"))) | .number'
      

      Replace <N> with the issue number and run it.Has output = there is already an open PR pointing to #N → SKIP(someone else / another machine is doing it); No output → claim, cut a deterministic branch from the latest tip:

      git checkout -B claude/issue-<N> origin/<default_branch>
      

      The early-layer advisory lock is already in place:issue-review acquire as soon as work starts agent:processing(TTL 30min, see its ★concurrency lock), Step 1 also uses this to skip candidates with fresh locks—collisions are short-circuited "before reading/verifying/testing". Here the deterministic branch + claim check is the hard dedup fallback at the end(when the lock is advisory and there are residual races, it steps in). The two layers are complementary, neither can be missing.

    3. Cleanup is handled by pr-sweep as a fallback: for duplicate pairs already produced, sweep's dedup close step keeps one and comments + closes the rest. Once the source is fixed, this kind of cleanup will trend toward zero.

  • Any derived/spin-off issue opened must have a native edge written (the source of graph accuracy). In addition to the first line of the body <!-- spinoff-of: #N --> marker,at the same timeexecute bun <plugin_root>/skills/issue-graph/scripts/link.ts --parent <N> --child <新号>(idempotent); if there is a hard ordering between phases, also add --issue <后> --blocked-by <前>. The marker is provenance, only native edges enter Step 0.5's deterministic graph computation—not writing the edge = this spin-off is permanently invisible to close-kick / rollup, returning to the old disease of "needing a human bump".
  • One issue, one PR (deterministic branch),body references the issue(Part of #N;完全闭合 才用 Fixes #N`, partial fixes leave the issue open and explain which part was handled). Don't stuff unrelated changes into one PR.
  • PR body ends with Fixes #N so merge auto-closes the issue.
  • The top of the PR body carries the standard identity header(the same set of identity-line conventions extended to PRs; i.e. agent_comment_marker): the whole line is generated by <agent_identity_script> --header "PR" --skill issue-sweep (→ > 🤖 AI Agent PR @ <hostname> · runner:<runner> · skills@<hash>), and cannot be hand-assembled / placeholdered. Attribution, environment, and skills versions are hence traceable from the PR body itself, without digging through comments.
  • Commit messages follow Conventional Commits. Never reach for git commit --no-verify as a default — the pre-commit hook here is simple-git-hooks (not husky; this repo has no husky dependency), wired via the root postinstall script. A hook that fails to spawn (biome: ENOENT / the command not found at all) almost always means <package_manager> install hasn't run yet in this checkout — run it (or, if that's impractical, node node_modules/simple-git-hooks/cli.js to just re-link the hook) and retry the real commit before ever bypassing it. Only fall back to --no-verify if the hook is confirmed broken after install (rare), and even then keep changes clean by hand via the <formatter> per CLAUDE.md's "don't casually --no-verify" rule — it also skips the formatter, so formatting/lint issues silently leak into the PR (there's no CI gate to catch them).
  • Safety before any deletion/edit: git grep confirms no external code importers; <package_manager> --filter <pkg> check-types (or a targeted test) shows no new errors from the change (pre-existing/unbuilt-dep errors don't count — call them out). Adding/removing a dep → update pnpm-lock.yaml with pnpm install --lockfile-only, confirm the diff is scoped, and git add it alongside package.json — staging the manifest without the lockfile breaks --frozen-lockfile everywhere else. Conversely, if you didn't touch deps but the lockfile still shows a diff, it's a concurrent/unrelated change → don't stage it.
  • Deletion provenance: content is recoverable via git history; the audit comment preserves it. AI never auto-merges; humans merge.
  • Push: git push -u origin <branch>; retry on network error with backoff.
  • ★ Verification hard constraint (proposing side, mechanism rather than discipline): PR pathno longer has any CI(ci.yml/pr-title.yml has been deleted), the verification script is the only pre-submit gate.
    • before pushmustrun <verification_entry>, if the hard gate is not passed must not push / open PR.
    • after opening the PR, use one command to weld "run + post" together——--comment let the script upsert the report to the PR itself, the agent cannot run without posting, nor hand-edit the numbers:
      <verification_entry> --comment <PR#>
      
      (report = status + elapsed time + collapsible full log, numbers measured by the script; marker sticky comment, reruns only edit the same one without spamming.) See CLAUDE.md "Self-Verification" + verification skill。
  • ★ Integration verification named in acceptance cannot be pre-excused: When the issue's acceptance criteria / human names /e2e-verify (the repo's companion, see repo-profile's Companion Skills; if none, stub or skip that step) and other integration verifications (blocklet render / mount / serve), the proposing sidemust actually run——<cli_binary> if the CLI is missing/stale, first run <cli_setup_command>,must notexcuse it with "needs a daemon / cannot execute in this environment",nor may ituse pre-pr.ts unit test replaces the named e2e. Missing dependency = one extra setup step (compile native plugin, link CLI), only when actually hitting a missing hard toolchain (no Xcode/Android SDK/Playwright) does it count as unrunnable, and paste theexact error + mark the skip tier. See e2e-verify skill。
  • ★ Screenshots of UI changes are shifted left (generated on the proposing side, not left to the review side): when the diff hits <UI Face Paths>(.claude/repo-profile.md),before opening the PR you must generate UI runtime screenshots——for renderer/widget level use <ui_shot_script>(real shipped bundle renders fixtures; parameter matrix / before-after comparison / state sequence, three types, multiple images as needed), for page-level flows use /ui-verify. Three hard requirements:
    1. Self-check before submitting: after generating, use vision to inspect the images against the <ui_shot_script> README checklist (bare styling = css.ts has no matching entry, hover/click before-after two images show no change = interaction broken, Unknown fallback box = type not registered, layout overlap)——if any one hits, fix first then submit. All-green unit tests can't catch these, don't leave them to the reviewer or a human.
    2. Screenshots embedded in the PR body(<ui_upload_script> upload, ASSET_CONTEXT=pr{N}; at the branch-opening stage when there's no PR number yet, first use issue-{N}), when opening the PR it carries the images, not a follow-up comment. The script has two built-in hard self-checks:exit 3 = script and origin/<default_branch> mismatch (stale checkout——the real root cause of past broken images) → git checkout origin/<default_branch> -- <ui_upload_script> then rerun;exit 4 = raw URL not anonymously reachable → embedding forbidden. General acceptance before embedding: the URL must credential-free curl 200(from camo's perspective; the script path is built in, the MCP path is verified manually).
    3. Post the same set of screenshots back to the related issue(one short comment: images + one sentence of explanation), so people can see it at a glance in the issue without clicking into the PR.
  • The PR inherits the source issue's milestone (+ labels/assignee provenance). Immediately after opening the PR, copy the issue's milestone to the PR——otherwise the PR won't enter the release/batch milestone view, and the board misses it. For milestone naming/categorization conventions see Milestone Conventions(.claude/repo-profile.md)。
    ms=$(gh issue view <N> --json milestone --jq '.milestone.title // empty')
    [ -n "$ms" ] && gh pr edit <PR#> --milestone "$ms"
    
    ForeveryPR derived from an issue, do it (treat fix / doc-update / delete / feature-phase PRs alike). If the issue has no milestone, skip it (don't set one randomly).
  • The PR's assignee/reviewer inherits the source issue's people. After opening the PR, set the source issue's author + assignees(deduplicated) as the PR's assignee——they are the informed parties and owners of this matter, and only when the PR shows up in their todo list will it not be missed.PRs that need human review(🔴 high risk / security / A-vs-B pending decision / 🟡 draft needs-human-review)also set these people as reviewers; if judgedno human confirmation needed(🟢 mechanical fixes, low-risk tier, auto-mergeable within the pr-sweep gate) may have no reviewer specified, to avoid creating meaningless review requests.
    people=$(gh issue view <N> --json author,assignees \
      --jq '([.author.login] + [.assignees[].login]) | unique | join(",")')
    [ -n "$people" ] && gh pr edit <PR#> --add-assignee "$people"
    # 仅当 PR 需要 human review 时:
    gh pr edit <PR#> --add-reviewer "$people"
    
    If assignment fails (reviewer happens to be the PR author themself / a bot / a non-collaborator), skip it and note one line, don't block—— consistent with issue-review's assignee discipline.

Step 5 — Be quiet when there's nothing

If no issue has an unprocessed human reply this round: post nothing, open no PR, message nothing. A no-op sweep is silent. Only speak when you acted.

--dry-run

Do Steps 1–2 and report the candidate list + what each would trigger. Make no outward writes (no comments, PRs, labels, closes). For previewing before a real run. Same --dry-run semantics as every loop skill — see the Dry-run contract in the plugin README.

Memory MCP (optional, when already configured)

If the runtime environment's MCP tool list includes the AFS namespace (afs_read / afs_write / afs_search, from the connected ARC instance MCP endpoint), at Step 0 of each sweep round (after sync <default_branch> ), add two steps:

Pre-scan recall (in parallel with reading the issue list):

afs_search /user/memory 关键词:<本轮重点 label / 子系统 / 常见问题域>

The returned observations / patterns / principles are added to the sweep's initial context ("fast warm start with known constraints/regularities").

After each issue is processed, store (append): cross-issue valuable findings (root causes of similar bugs, hidden code constraints, team decisions) are written to memory. Granularity has three levels:

  • observation: concrete facts + path:line
  • pattern: regularities induced across issues
  • principle: inferred working principles

Path:afs_write /user/memory/<memory_namespace>/<namespace>/<id>; caller identity is isolated, loop agents do not interfere with each other.

MCP not configured = this section is skipped, all other sweep behavior remains completely unchanged.

Key principles

  1. Sync <default_branch> before anything (Step 0). Safety greps, type checks, and branch-offs are only trustworthy against the latest tree; a stale clone silently invalidates them.
  2. Scan by label + last-comment, never by an updated-at window — that window is exactly what dropped issues with early, never-re-bumped human replies or priority-only labels before.
  3. Human vs AI is by content (🤖 AI Agent marker present near the top, not necessarily the literal first line — see Step 2), not author — both post under the same account here.
  4. issue-review does the per-issue work; this skill only decides what to feed it and enforces the resolve-action + discipline rules.
  5. Safe-delete only; comment on what can't be cleanly/safely done — live deps, pending third-party confirm, security, A/B decisions stay human.
  6. Two-layer concurrency coordination: advisory lock (early) + deterministic branch (hard, fallback). (a) agent:hold = human reserved = terminal state frozen(never close/terminal-state handling, only humans pick; but new human comments still require response);agent:processing = in-progress mutex lock (TTL 30min), Step 1 skips candidates with fresh locks,issue-review acquire at start, release at end — moving collisions ahead of reading/verification/testing. (b) Deterministic branch claude/issue-<N> + claim check before opening PR + one issue one PR + Fixes #N + never auto-merge is hard deduplication at the end: descriptive slug branch names are the root cause of duplicate PRs across machines, must be derived only from the issue number, and check for duplicates before creation SKIP. The lock is advisory (there are residual races), branch claim covers it; residual duplicates are handled by pr-sweep deduplication close as fallback.
  7. Silent no-op when nothing is pending. 6b. Graph computation determines candidates and propagation, the LLM is only responsible for doing. Each round Step 0.5 runs graph-scan: kicks/rollupCandidates injects candidates (no human comment needed), blocked deterministically SKIPs; opening a spin-off must write a native edge (link.ts); terminal-state actions without branch fallback (rollup) use claim.ts fencing mutex. The graph only enhances, never replaces, label scanning.
  8. --autofix-green: verifiability is the gate, never auto-merge. Auto-fix only issues that pass all four gates (unambiguous + verifiable-in-this-env + low-blast-radius + non-security); reproduce-first, one PR per issue, white-list categories only. Can't run a test that proves it here → 🟡 draft PR + human, not green. The set of 🟢 issues grows with the environment (TS-only vs full-platform build). 🟢 = auto-PR, not auto-merge — the pre-merge verification gate + a human still gate the merge (no CI on the PR path).
  9. Autonomous = ask on the issue, never block in-session. This sweep runs unattended — no human is babysitting. Any time the per-issue work (including design-review / build-phases escalations) would normally stop and wait for a human answer, post the question as a comment on that issue (options + recommendation + what's blocked) and move on; don't sit on a blocking inline prompt. The human answers async on the issue and the next sweep resumes it.

★ sweep-trace instrumentation (L2 observability layer)

At the end of every AI comment emitted by this skillmustappend one line of sweep-trace HTML comment (invisible to humans, grep-able, reused by L1 eval as golden baseline data source):

<!-- sweep-trace: {"ver":1,"issue":N,"gate":"<gate>","val":"<val>","run":"<ISO8601>","runner":"<runner>","skills":"<hash>"} -->

Fields:

  • ver: schema version, currently 1
  • issue: corresponding issue number (numeric)
  • gate: decision gate name, from the controlled vocabulary:disposition / skip
  • val: decision value, from the disposition controlled vocabulary:pr / comment / close / skip / research / idea / feature / needs-human-confirm
  • run: UTC time,new Date().toISOString() format
  • runner / skills(provenance extension, v1-compatible optional): take <agent_identity_script> the corresponding segment in the output — routine owner + .claude/skills/ tree version hash, used to split golden baseline by version and locate the output of low-version routines

trace is appended only at the end of comments actually emitted by this skill; in dry-run mode no comment is emitted, so no trace is appended.