* feat(autofix): label-driven takeover and release; fix forced-dispatch green no-op
Takeover, exactly as designed: applying autofix/takeover (GitHub
triage+ only — the permission gate is GitHub's own) summons the loop
onto a PR, human-authored included; removing it releases the PR. The
autofix/skip label opts any PR out at every engagement path — the
autofix scan, the forced path, and the Fleet Shepherd walk — and wins
when both labels are present. Every toggle gets a bilingual ack from
the PAT-verified bot identity. The comment-command surface stays
CLOSED: the pinned no-comment-commands contract test is untouched.
Plumbing: pull_request labeled/unlabeled triggers; label events share
the per-PR route group (the event class is triage-gated); the group
expression also carries the #7163 payload trust prefilter so the two
open PRs converge on the same final text in either merge order; scan
candidates become bot PRs ∪ takeover PRs minus skip PRs (deduped);
fork label events are logged and dropped (no secrets to even post a
rejection).
The forced-validation replay immediately caught a latent bug live
since #6528: '(.isCrossRepository // true) | not' is false for EVERY
input (jq's // treats false as empty), so every workflow_dispatch
pr_number=N run — including the shepherd's conflict dispatches —
validated to targets=[] and exited GREEN having done nothing. Fixed to
'.isCrossRepository == false' (fail-closed on a missing field) with a
replay case pinning the trap.
Tests: autofix 55/55 (label engage/release pins, candidate-selection
replay with skip-wins and fork cases, forced-validation replay across
author/takeover/skip/closed/fork/missing-field); shepherd 12/12
(skip-filter replay).
* feat(autofix): '@qwen-code /takeover' comment sugar over the takeover label
Maintainer-mandated reopening of the comment surface, in the narrowest
possible form: '@qwen-code /takeover' applies TAKEOVER_LABEL and
'@qwen-code /takeover stop' removes it — nothing else. The label stays
the single source of truth: engagement and release happen exclusively
via the pull_request label events, so a manual label edit and the
command are one mechanism with two entry points, and the command's
whole blast radius is one label toggle.
Gates: an expression-level startsWith prefilter keeps ordinary
comments from ever starting a job; the body must match the constant
EXACTLY after trimming (no parsing, no arguments); allowed senders are
the PR author (who may lack label access — this is who the sugar is
FOR) or a write+ collaborator via the same permission API used by
review routing; closed PRs, non-PR comments, and the bot itself are
ignored. The toggle job is PAT-verified and presence-aware (no-op
toggles are explicit, since they fire no label event).
The 'does not expose comment-triggered autofix commands' contract test
is REWRITTEN into pinning this gated design, documenting the deliberate
reversal. New behavioral replay drives the extracted command branch
with a PATH-stubbed permission API across eight author/write/read/
exact-match/non-PR/closed/self cases. 56/56.
* feat(autofix): raise the round cap to 50 while a PR is under takeover
Large managed PRs routinely need dozens of feedback rounds — that is
the point of takeover — so the unattended MAX_ROUNDS=5 would strangle
exactly the PRs the label exists for. While TAKEOVER_LABEL is present
the effective cap is TAKEOVER_MAX_ROUNDS=50: the circuit breaker stays
(a bot/review-bot ping-pong is still bounded and every round still
requires new trusted feedback or a conflict), it is just sized for
explicitly delegated work. Removing the label restores the strict cap
on the next scan.
The scan computes the effective cap from the candidate's live labels
and stamps it into the matrix target; the address job shadows the
workflow-level MAX_ROUNDS with the matrix value, so every round
message, marker, and cap gate uses the same number consistently
(including #7163's address-time cap discard once both merge).
57/57 with a verbatim cap-selection replay (labeled → 50, plain → 5).
* feat(autofix): re-armable round windows, cap raised to 100, visible cap pause
The round counter is DERIVED state, stored nowhere but in the bot's
eval-marker comments on the PR — and counting is now windowed by the
latest '<!-- takeover-ack engaged -->' comment. Re-engaging (label
off→on, or repeating the takeover command on an already-managed PR,
which now posts a re-arm ack instead of a silent no-op) starts a fresh
window: a PR that exhausted its rounds continues under management with
one human action, auditable in the PR timeline. The WATERMARK stays
global across windows — feedback already addressed is never replayed —
and a PR never taken over has no ack, so strict lifetime counting is
unchanged. The prepare-side live round is windowed identically, so
pre-reset markers can neither trip the cap nor look like same-ts
round-advance duplicates (replay-proven).
TAKEOVER_MAX_ROUNDS rises to 100 per maintainer sizing, and pausing at
the cap is now VISIBLE on managed PRs: a bilingual notice with re-arm
guidance, once per counting window (marker-deduped past the latest
re-arm; a failed post retries next scan).
58/58: rearm windowing replay (no ack → lifetime; ack → round 0 with
watermark preserved; new rounds count from 1; latest ack wins), the
stale-gate re-arm interplay case, cap-selection at 100, and cap-notice
dedup pins.
* feat(autofix): collapsed-Chinese bilingual takeover comments; fix ESLint regex-spaces
Every takeover-flow comment — engage ack, release ack, re-arm ack, and
the cap-pause notice — now follows the project convention: English
body plus Chinese collapsed under <details><summary>中文说明</summary>
(pinned at exactly four sites). Bodies are built via printf so no
workflow indentation leaks into the markdown: the previous literal
multi-line strings embedded 10 leading spaces, which would have
rendered the trailing marker comment as a visible code block.
Also fixes the CI failure at 1497a7e6f: three extraction regexes used
literal space runs, tripping ESLint no-regex-spaces — now {n}
quantifiers. Lesson applied: prettier alone is not the lint gate.
58/58 + 12/12.
* fix(autofix): fork-safe takeover release, verified cap-notice write, doc drift
Review round (issue comment on head c2d8a89), all three findings:
- F1 (bug): 'unlabeled' on a fork PR emitted the release ack
unconditionally; fork pull_request runs carry no secrets, so
takeover-ack failed its PAT identity check — a red run, reachable
self-serve (the comment sugar accepts the fork PR's author, sticking
the label; any later unlabel fired the red run). The unlabeled
branch now mirrors the fork log-and-drop, and takeover-command —
which runs in issue_comment context WITH secrets — refuses fork PRs
up front with a bilingual explanation (adoption guidance), so the
label can no longer stick to forks via the command at all.
- F2 (convention): the scan's cap notice now verifies the PAT
identity before writing (memoized per run): a rotated PAT would
post under a foreign login that the AUTOFIX_BOT-scoped dedup can
never see, reposting every scan. Scan header updated — its single
write is identity-verified.
- F3 (doc drift): two leftover '50' references updated to speak of
TAKEOVER_MAX_ROUNDS.
- Note adopted: the leading-whitespace prefilter/trim nuance is now
documented at the prefilter.
58/58 + 12/12; collapsed-Chinese sites now pinned at five (the
fork-refusal joins the four acks).
* fix(autofix): window-keyed rounds, ordered commands, skip closure, marker scrub
Second review round on the takeover feature (dev-bot GPT-5 review at
c2d8a89) — all seven Criticals and three Suggestions:
- Round windows are now keyed, not timestamped: every eval marker
records the window key it was produced under (win=…, legacy counts
as 'none'), the current key is the latest engage ack's created_at,
and only current-key markers count toward the cap. An in-flight
address job selected before a re-arm can no longer re-cap the fresh
window with a late marker — and prepare discards any job whose key a
re-arm superseded while it sat queued (conflicts stay actionable).
- Takeover commands are serialized per PR (queued concurrency group):
an older /takeover cannot land after a newer /takeover stop read the
unlabeled state.
- Skip closure at every remaining gap: the command refuses engage and
re-arm on a skip-labeled PR (bilingual skip-blocked ack, no bogus
window anchor); the label-event ack does the same; the scan's fresh
per-PR fetch re-checks skip mid-scan; the shepherd re-checks the
LIVE label immediately before its two mutating levers (fail closed —
unreadable labels count as skipped).
- Honest release on bot-authored PRs: removing takeover now says
standard bot management continues (strict cap); only takeover mode
ends.
- Model files posted verbatim as PR comments (address-summary,
no-action) are scrubbed of HTML comments — the rule the handoff
DETAIL already applied — so induced output cannot forge control
markers the scanners would trust.
- Test hardening per the Suggestions: every bilingual body is asserted
individually (8 printf bodies), and the gh pr list producers must
request 'labels' in both workflows (fixture-driven consumers alone
went green without it).
58/58 + 12/12; the rearm replay now proves the race the key model
closes (an old-window round-50 marker landing after the ack counts 0).
* fix(autofix): sever PR hooks from PAT pushes; scope label routing; fork-author command gate
Third review round (yiliang114 P0-P3 + a second inline batch):
- P0: the address/publish push steps carry the PAT while the branch
carries PR-controlled .husky hooks (hooksPath was pointed there so
the agent's commits get checked) — a pre-push hook would execute
that code with the PAT in env. Both push sites now sever hooks
(core.hooksPath=/dev/null + git push --no-verify). The force-push
guard regex also false-positived on --no-verify ('-…f') and is now
precise (-f word / +refspec).
- P1: only the takeover label itself shares the per-PR route group;
an unrelated label changed in the same batch operation can no
longer cancel a queued takeover route.
- P2: candidate INSPECTION is bounded (MAX_CANDIDATE_INSPECTIONS=60),
not just emitted targets — idle candidates burn serial API calls
and takeover widens the pool. Excess rotates to the next scan.
- Fork-author command gate: author privilege is in-repo only — a
fork-PR author can no longer summon even PAT-authored refusal
comments onto their own PR (silent drop); write+ maintainers still
reach the explanatory fork refusal. Replay-proven both ways.
- Candidates jq now fails closed on the fork field (== false),
matching the forced path and the //-trap NOTE.
- The unlabeled path mirrors the labeled-path state guards: releasing
a closed/non-main/fork PR acks nothing (it was never engaged).
- Command-contract docs updated to the real side-effect set, and the
four-path toggle (plus skip/fork refusals) now has a full
gh-recording behavioral replay.
59/59 + 12/12.
* fix(autofix): live cap-notice dedup key, line-proof marker scrub, checkout hook severing
Round-3 review (issue comment at 427301c64), all four findings:
- R1 (regression): the window-key rename left the cap-notice dedup on
a dangling REARM_TS — empty rt made every historical notice count,
silently turning per-window dedup into per-lifetime, so a re-armed
PR hitting the cap again paused with no reminder (the feature's
headline case). Now NOTICE_RT=REARM_KEY with the 'none' corner
falling back to lifetime dedup (created_at > 'none' is never true
lexically, which would have flipped it into posting every scan).
The extracted-jq replay proves all three regimes.
- R2: the HTML-comment strip was line-oriented while jq scan()
matches across newlines — a marker split over two lines survived
the scrub and still parsed. All three publish sites now neutralize
the opening token itself (sed 's/<!--/<!\-\-/g' — line-independent,
and the backslashes render away in markdown). Proven end-to-end on
a split forged marker, with the sed extracted verbatim.
- R3 (pre-existing, folded in as the natural home): prepare's
checkout -B ran PR-controlled post-checkout hooks with the PAT in
env. Prepare now severs hooks like the push steps; the agent step —
no PAT, sandboxed tools — re-points .husky itself so its commits
still get checked.
- R4: candidates now drain NEWEST-first (unique_by sorts ascending;
sort_by(-.number) added), the comment states the real semantics
instead of a rotation that never existed, and the free busy skip no
longer consumes inspection budget.
59/59 + 12/12.
* fix(autofix): takeover-ack state read fails closed
Review suggestion, adopted verbatim: the ack's gh pr view fell back to
'{}' on failure, defaulting HAS_SKIP to false — a transient API
failure could post a wrong 'engaged' ack on a skip-labeled PR. It now
exits like the sibling takeover-command job (a red ack job posts
nothing; engagement is scan-driven and unaffected). Pinned both ways.
* fix(autofix): command-style comments are instructions, not review feedback
Motivating case: a maintainer posted '@qwen-code /triage' on a bot PR
and the loop burned a full agent cycle (checkout, install, build,
agent) to publish a no-action report explaining that the command is
not feedback. The takeover command introduced here would self-trigger
the same round on every engagement.
Comments matching '^\s*@qwen-code /' (any author) are now excluded at
all four feedback-decision sites — the scan's issue-comment count, the
NEWEST computation, the LIVE_NEW recount, and the prompt renderer —
alongside the existing bot-marker filter. Behaviorally proven: a
trusted takeover-command comment newer than the live watermark no
longer rescues a stale duplicate into an agent round, and the raw jq
counts real feedback while dropping the command. 59/59 + 12/12.
* fix(shepherd): fork check fails closed, matching the autofix convention
Review suggestion, adopted verbatim: the fleet filter kept
'.isCrossRepository != true' (fail-open on a missing field) while the
autofix candidates migrated to '== false' in the same PR. Aligned;
the filter replay now includes a missing-field row and proves it is
rejected. 12/12.
* fix(autofix): trusted runner staging, live author privilege, rotation, sentinel watermark
Auto-review of the merged head (16 findings; 11 adopted, 3 rebutted in
replies, 1 rename, 1 already-covered):
- The address agent step invoked run-agent.mjs from the CHECKED-OUT PR
branch with the model key in env — branch-controlled code on the
host (takeover targets human branches). The runner is now staged
from the trusted base into RUNNER_TEMP (same pattern as the schema
gate) and invoked from there.
- Author command privilege is LIVE, not durable: authors must hold
triage+ today, so an ex-member's authorship no longer summons
secret-bearing runs (the sugar's audience — members below write —
still qualifies).
- The terminal-handoff sentinel ts is excluded from watermark
computation (scan and prepare): it is a flag, not an evaluation
time, and it previously made a re-arm after a terminal handoff dead
on arrival by filtering all future feedback forever. Terminal
skipping stays round-based and thus window-scoped.
- Candidate inspection gains a rotating start offset (fixed
newest-first plus the budget starved the oldest tail FOREVER once
the pool exceeded the budget).
- issue_comment events get their own per-PR command group: bursts
coalesce away from review routes, and pending-slot replacement is
exactly latest-intent.
- The queued toggle re-verifies OPEN + base=main; the cap notice
honors dry-run and re-verifies live consent before posting; the
release ack on a skip-labeled bot PR now says skip governs.
- Shepherd: live_skip is reason-aware (an API outage is reported as
fail-closed, never as consent withdrawn) and both levers check their
budgets BEFORE the PAT-backed live read.
- Tests: the supersede fixture is now discriminating (old-window
marker lands AFTER the ack — timestamp-windowing would have counted
it); the force-push guard catches combined short options (-uf); the
sentinel-watermark rule and rotation are replayed; the labels test
is renamed to stop claiming the comment surface is closed.
60/60 + 12/12.
* test(autofix): restore recheck-before-checkout ordering pin; pin all win-marker sites
Two review suggestions, both fallout from the merge-resolution test
unification: the eligibility recheck's BEFORE-checkout ordering
assertion (lost with the replaced pin test) is back inside the replay
test, and all three eval-marker producers now carry per-site win=
format pins (the global count-3 catches removal but not a
lose-one-gain-a-duplicate swap). 60/60.
* fix(autofix): trusted-commenter cmd groups; hooks severed at every host checkout; honest model-key framing
- The issue_comment command group now requires a trusted-looking
payload association (same prefilter pattern as reviews): an
arbitrary commenter's rejected command can no longer cancel a
maintainer's queued command out of the shared per-PR group.
- Both verification checkouts (review gate and issue publish) sever
hooks like every other host checkout — no secret sits in those
steps, but branch post-checkout code on the host broke the
convention (five severing sites now pinned).
- The agent-step comment and the PR risk section now state the model
key's real exposure explicitly: the CLI forwards OPENAI_API_KEY
into the sandbox and the agent's job is to build/test the branch,
so a taken-over branch's scripts can read AUTOFIX_OPENAI_API_KEY —
an accepted, documented consequence of takeover (PAT remains fully
severed via trusted staging + hook severing); the key should be
low-privilege and rotatable. 60/60 + 12/12.
* fix(autofix): label events get their own route group; non-takeover labels never start jobs
Two review suggestions, adopted verbatim: pull_request label events now
group as route-label-{N} (distinct from the review group — a
simultaneous review and label toggle on the same PR can no longer
cancel each other), and the route job gate filters pull_request events
to the takeover label, so a triage labeling session across dozens of
PRs burns zero runner slots. 60/60.
---------
Co-authored-by: wenshao <wenshao@example.com>