Repo profile — read
.claude/repo-profile.mdfirst. This skill is repo-agnostic; arc is the reference implementation. Use the profile's values wherever this doc shows an arc default:repo_slug(thegh -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, andplugin_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 thedesign-reviewAutonomous escalation paradigm: post the options + your recommendation + the blocked content as a comment (attached toneeds-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 / codepath: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 --porcelainabove 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 throughissue-review★parent rollup (fencing mutual exclusion + acceptance check + synthesis comment + close).agent:holdhas 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:readylater thanneeds-human-confirm(especially when a human labels it personally) → human has confirmed/unblocked → start work(take over implementation).needs-human-confirmlater thanagent: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 implementationlabelStance()(test/sweep-golden/lib.ts)。live arc#1722: Phase 4 plan is ready, carriesagent:ready, butneeds-human-confirmwas 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 lastlabeledevent time for that label:
(nogh api --paginate repos/{owner}/{repo}/issues/<N>/timeline \ --jq '[.[]|select(.event=="labeled" and .label.name=="agent:processing")]|last|.created_at'ghusemcp__github__*to read the timeline.)agent:processingis 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(seeepic-conductor)。exclude the whole thing: no triage, no claim, no work, no comment. This is notagent: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:holdis "human reservation",epic-managedis "another agent's reservation"; both skip claiming, butepic-manageddoesn'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 asclaude[bot]via the GitHub App token). Markers can arrive HTML-escaped (<!-- … -->, the #1278 double-escape bug) — decode entities before matching. - The
> 🤖 AI Agentheader and therunner:… · 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-reportissues: test-sweep opens these directly (not throughissue-publish'stest-sweep-keymarker path), so the BODY carries the> 🤖 AI Agentidentity 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 carryingtest-sweep-failureortest-sweep-reportis never a Step 2 human-reply candidate (archetype:#2271, "[test-sweep] aistro: 1 failures detected" — seetest/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):
- 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: falseinside a code block matchedin-progress. - 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. - 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.
- 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 runissue-reviewStep 0, re-verify open/hold/blocked/agent:processingand acquire. If acquiring the lock fails, returnSKIP_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. - A worker owns only one issue. A worker runs the full
issue-reviewengine, and only writes that issue's comment/label/PR/branch; mutable task state must not be shared between different issues. - 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'sTMPDIR); just copy it literally:
Hardcodinggit -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/...will bypass the deployment party'scheckoutBaseconfiguration and accumulate more and more on the system disk — measured: when no limit was imposed, about 36G of orphan worktrees piled up under/private/tmpin one day, while the configured external disk was almost empty.The driver will do a fallback cleanup once per round for leftover$AGENTLOOP_WORKTREE_BASEthat 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'sclaude/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, readAGENTLOOP_SETUP_COMMAND(the fleet driver injects it from the current repo'ssetupCommand) 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. - 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. - Failure isolation + ownership-safe cleanup. One worker failing does not cancel other workers. Normal
finish is handled by the worker according to
issue-reviewStep 7 releasing its ownagent: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. - Nested fan-out is also subject to the runtime's total slot limit.
--concurrencyonly 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:
- Unambiguous — the fix is determined; no design decision, no A-vs-B, no "should we even do this".
- 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.)
- 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.
- 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;
ghbinary exists ≠ghREST 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 (curlthe 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)
- 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. - 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 🟡.
- Fix minimally; prefer backward-compatible (
x ?? legacy) over a swap. - Verify: the new test passes, the package's full suite shows no regression,
check-typesis 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-existingauth/index.ts:151type error). Before attributing a failure to "pre-existing", prove it — eithergit stashyour 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. - One branch + one PR per issue, body references the issue (
Part of #N; useFixes #Nonly if the PR fully closes it — partial fixes leave the issue open and say which part was handled). - Never auto-merge. 🟢 means auto-PR, not auto-merge — the
pre-mergeverification 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 — seetest/sweep-golden/fixtures/1025-forbidden-auto-merge.jsonfor 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:
- Read the
spinoff-ofparent. 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. - Run premise-verification (🟢 pipeline step 0). The issue's own grep is necessary but not sufficient — broaden it.
- 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,enhancementwith 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-phasesare themselves automated processes; feature issues should use them to run, not toss them back to humans.
- Native parity (
- If doable, do it, do not queue —
🟢 candidate-queueddelete 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 branchclaude/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:
- Discovery method: label, not marker. scans all open issues, picks
test-sweep-failure(preferred, specific to a single failure point) ortest-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 thespinoff-ofmarker, treat them as "invisible, skip". - 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 🟢.
- 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/.aupdeclared behavior vs actual behavior mismatch) or a direct functional bug. After retesting and confirming the phenomenon is still present:.man/.aupThe 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.
- 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.
- 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 onePart of #Nevery sub-issue covered, and do not miss any references.
Run history (first two
--autofix-greenruns, 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:-
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 useclaude/issue-<N>-p<phase>. Two machines compute the same namebranch → the secondgit push/gh pr createnaturally collides and no longer double-opens. -
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-reviewacquire as soon as work startsagent: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. -
Cleanup is handled by
pr-sweepas 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 timeexecutebun <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 #Nso 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-verifyas a default — the pre-commit hook here issimple-git-hooks(not husky; this repo has no husky dependency), wired via the rootpostinstallscript. A hook that fails to spawn (biome: ENOENT/ the command not found at all) almost always means<package_manager> installhasn't run yet in this checkout — run it (or, if that's impractical,node node_modules/simple-git-hooks/cli.jsto just re-link the hook) and retry the real commit before ever bypassing it. Only fall back to--no-verifyif 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 grepconfirms 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 → updatepnpm-lock.yamlwithpnpm install --lockfile-only, confirm the diff is scoped, andgit addit alongsidepackage.json— staging the manifest without the lockfile breaks--frozen-lockfileeverywhere 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.ymlhas 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——
--commentlet the script upsert the report to the PR itself, the agent cannot run without posting, nor hand-edit the numbers:
(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_entry> --comment <PR#>verificationskill。
- before pushmustrun
- ★ 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 itusepre-pr.tsunit 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. Seee2e-verifyskill。 - ★ 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:- 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. - 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 useissue-{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 andorigin/<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). - 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.
- Self-check before submitting: after generating, use vision to inspect the images against the
- 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)。
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).ms=$(gh issue view <N> --json milestone --jq '.milestone.title // empty') [ -n "$ms" ] && gh pr edit <PR#> --milestone "$ms" - 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.
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.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"
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:linepattern: regularities induced across issuesprinciple: 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
- 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. - 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.
- Human vs AI is by content (
🤖 AI Agentmarker present near the top, not necessarily the literal first line — see Step 2), not author — both post under the same account here. issue-reviewdoes the per-issue work; this skill only decides what to feed it and enforces the resolve-action + discipline rules.- Safe-delete only; comment on what can't be cleanly/safely done — live deps, pending third-party confirm, security, A/B decisions stay human.
- 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-reviewacquire at start, release at end — moving collisions ahead of reading/verification/testing. (b) Deterministic branchclaude/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 bypr-sweepdeduplication close as fallback. - 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) useclaim.tsfencing mutex. The graph only enhances, never replaces, label scanning. --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 — thepre-mergeverification gate + a human still gate the merge (no CI on the PR path).- 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-phasesescalations) 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, currently1issue: corresponding issue number (numeric)gate: decision gate name, from the controlled vocabulary:disposition/skipval: decision value, from the disposition controlled vocabulary:pr/comment/close/skip/research/idea/feature/needs-human-confirmrun: UTC time,new Date().toISOString()formatrunner/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.