Commit graph

596 commits

Author SHA1 Message Date
Shaojin Wen
e93da9e387
feat(autofix): escalate stopped takeover PRs and age out unanswered pauses (#8960)
* feat(autofix): escalate stopped takeover PRs and age out unanswered pauses

Takeover PRs that hit the round cap (or a circuit breaker) went silent:
no label, no dashboard entry, no escalation — five PRs had been paused
for days. The fleet shepherd only tracked bot-authored PRs, so the whole
35-PR human takeover pool was invisible.

The autofix scan now applies an autofix/needs-human label whenever a PR
reaches its cap (the write rides every cap detection, so already-paused
PRs backfill on the regular scan rotation), and removes it wherever
management resumes or a human releases the PR. The fleet shepherd
enumerates the takeover pool onto its dashboard (state, stop reason,
pause age, plus an awaiting-human section for released PRs) and gains a
single bounded lever: a takeover whose pause went unanswered for
AUTO_RELEASE_DAYS days gets its takeover label removed with a bilingual
summary, keeping the needs-human label as the filterable TODO. Resume
evidence newer than the pause notice — bot markers, trusted re-arm
commands, fresh labeled events — vetoes the release; every read fails
closed and a per-tick cap bounds blast radius.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): harden the takeover auto-release against review round 1

Addresses the PR review's two Criticals and eleven Suggestions:

- Command-comment resume evidence now counts only while FRESH (2h grace)
  and UNSUPERSEDED by a refusal ack (fork-refused/base-refused/
  skip-blocked) — an ignored command expires instead of vetoing the
  release forever, and no permission logic is mirrored from the route.
- The release lever's population comes from the needs-human enumeration
  (needs-human ∩ takeover), never the display window; both enumerations
  cap at 100 with saturation warnings, and a failed enumeration degrades
  to an error row so the dashboard write (and its liveness watermark)
  always runs.
- The auto-release summary posts before the label DELETE, dedup'd by its
  own marker — neither half can strand the other on a transient failure.
- Awaiting-human rows use neutral wording (capped bot PRs land there too)
  and a shepherd-side heal clears stale needs-human labels left by manual
  UI releases on fork PRs (human unlabeled event, budgeted, skip-vetoed).
- Fail-closed deferrals now still render a dashboard row (the row append
  moved outside the evaluation arms); tick summary and dashboard header
  report the same counters; days_since() replaces pasted epoch math.
- Tests: command-evidence gate replays (fresh/refused/expired/acked),
  refusal-variant and command-string cross-file pins, DELETE-target and
  fallback-assignment pins, heal jq replays, unified-row-render pin.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 2 — cycle-scoped heal, retryable summary

- The stale-label heal now only counts a human unlabel NEWER than the
  latest label-apply, so an unlabel from an earlier takeover cycle can no
  longer heal the current cycle's needs-human after an auto-release (R2-1).
- The summary dedup marker is scoped to the current pause cycle (markers
  older than the latest cap notice are ignored), so a re-armed and
  re-capped PR still gets its second release summary (R2-4).
- The two DELETE levers no longer redirect act()'s stdout, keeping the
  DRY-RUN preview and failure warning visible (R2-5).
- AUTO_RELEASE_DAYS is base-10 normalized after the numeric guard, so a
  zero-padded repo variable can't silently kill the lever (R2-6).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 2 — cycle-scoped markers and mutation-tested pins

- Heal is cycle-correlated: only a human unlabel NEWER than the latest
  label-apply counts (an earlier cycle's unlabel can't heal this cycle).
- The release summary dedup marker is scoped to the current pause cycle,
  so a re-armed and re-capped PR still gets its second summary.
- act() stdout is no longer redirected on the two DELETE levers (DRY-RUN
  preview and failure warning stay visible).
- AUTO_RELEASE_DAYS is base-10 normalized so a zero-padded repo variable
  cannot silently kill the lever.
- Doc/workflow-header text corrected to the implemented order (summary
  first, marker-dedup'd) and to the idle-backoff backfill timing.
- Mutation-tested test pins for every gap the reviewer probed: days_since
  replay, NH_PREFIX interpolation + truth map, loop-1 deferral, full
  cross-file marker/refusal-set equality, label-constant cross-pin,
  EVENT_TS merge + promotion ordering, CLEANUPS increment, unclassified
  headline classification, filter byte-identity, exit-spelling ban,
  @uri encoding, sort/field-list attribution, paginate shapes, scope
  --arg bindings, LIVE_LABELS_JSON wiring, positional append pin,
  label-create idempotence + POST guard, and per-branch removal
  attribution in the toggle replay.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 3 — release-ack label gate and shared classifiers

- R3-1 (Critical): the every-scan cap-branch label POST is now suppressed
  when a release ack (takeover-ack released) is newer than the last re-arm,
  so a released bot PR is not re-labeled each scan (which would fight every
  release-side removal and ping-pong with the shepherd cleanup). A re-arm
  advances the window past the release ack, re-enabling the label.
- R3-7: the /retry re-arm's needs-human removal now honors autofix/skip,
  mirroring the takeover-command guard — a frozen PR keeps its only
  filterable escalation state.
- R3-2 (Critical): the takeover-enum error row no longer claims 'no release
  evaluation ran' — the lever is fed by the needs-human enumeration.
- R3-8: the conflict-dispatch lever refuses a paused (needs-human) PR
  instead of spending a dispatch slot the scan would refuse.
- R1-10: extracted pending_checks()/failed_test_url() helpers so both
  dashboard loops share one CI-status classifier (the round-1 reply was
  wrong that the restructure removed this duplication — it did not).
- Hardened the mutation-tested pins: exact terminal-headline count (5),
  full rearm DELETE line + single-API-write, AUTO_RELEASE_DAYS guard order,
  runRearm env/stub/assertion for the /retry DELETE + skip guard, and the
  scope-guard comparison operator.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 4 — label-lifecycle hardening

- R4-C1: the conflict-dispatch lever reads needs-human from the LIVE label
  payload (after live_skip), not the tick-start snapshot, so a label applied
  after enumeration is still honored.
- R4-C2: a re-armed PR that still carries needs-human (a resume-side removal
  failed) now gets a bounded, skip-vetoed cleanup retry instead of staying
  pinned in the paused population forever.
- R4-C3: the per-tick release budget is consumed before the first external
  write — a DELETE outage can no longer mutate many PRs while RELEASES=0.
- R4-C4: dashboard row routing follows post-action label state — a released
  PR moves to Awaiting human, a healed one drops off entirely.
- R4-C5: the AUTO_RELEASE_DAYS guard also rejects over-long digit strings
  before any arithmetic (Bash-int overflow would wrap negative and pass -ge).
- R4-32: takeover-command stop only removes needs-human when the takeover
  release actually landed (REMOVED_OK; 404 counts) — a failed release no
  longer strands the escalation label while latching RELEASE_ACKED.
- R4-2: the /retry skip guard fails closed — an unreadable label state keeps
  the label (mirrors takeover-ack's exit-1 convention).
- R4-3: the takeover-ack released arm and the stop branch both honor
  autofix/skip when removing needs-human.
- R4-S1: producer headlines must be explicitly classified terminal or
  transient — an unclassified headline now fails the cross-file test.
- Pins updated/added for every behavior above.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 4 — robust release detection and marker-true gates

- R4-1/R4-5: release detection now uses the takeover unlabeled EVENT
  (recorded on every removal path, unlike the tolerated-lost ack comment),
  and the suppression only applies to human-authored PRs — a bot PR
  released from takeover returns to standard management and keeps the cap
  notice + escalation label.
- R4-6: the conflict-dispatch lever requires marker truth (conflict_paused)
  — an armed PR with a stale needs-human label is dispatched normally.
- R4-C1: the pause check reads needs-human from the live label payload.
- R4-C2: re-armed PRs with a stale label get a bounded cleanup retry.
- R4-C3: the release budget is consumed before the first external write.
- R4-C4: dashboard rows route on post-action label state.
- R4-C5: AUTO_RELEASE_DAYS rejects over-long digit strings before arithmetic.
- R4-32/R4-2/R4-3: stop/ack/retry removal paths gate on REMOVED_OK and skip.
- R4-9/R4-10/R4-13: membership check, STATE escaping, HM_OK-branched error row.
- R4-14: command evidence requires a write/maintain/admin commenter.
- R4-11/R4-15/R4-24: behavioral replays for the classifiers, the release
  jq, and the gate nesting.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): keep the label-DELETE idiom byte-identical across workflows

R4-32's REMOVED_OK tracking reworked the takeover-command stop branch's
404-tolerance block, breaking the pr-self-report-label ↔ qwen-autofix
contract test that pins the two workflows' label-DELETE idiom
byte-identical. Keep the canonical idiom and derive REMOVED_OK from
REMOVE_ERR's content afterward (empty = landed, 404 = already off,
anything else = release did not land) — same behavior, contract intact.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(autofix): close review round 4b — mutation-tested harness hardening

- R4-4: re-bound the conflict-lever regex spans and anchor on
  conflict_paused so the pin can't resolve live_skip against the sync
  lever's call site.
- R4-16: runAck records gh calls and asserts per-branch needs-human DELETE
  counts (engaged/released=1; base-refused/skip=0).
- R4-17: pin the first-pickup scan DELETE inside the engage-ack success
  branch.
- R4-18/R4-19: ordering pins — takeover POST before needs-human DELETE
  (engage), marker comment before cleanup DELETE (/retry).
- R4-20: deleteFail stub branch replays non-404 (warns, status 0) and 404
  (silent) DELETE outcomes.
- R4-21: identity-failure paths assert no DELETE ran.
- R4-22: runRearm stub serves labels only when --json labels is requested.
- R4-23: full api-write census pinned (exactly api user + one DELETE).
- R4-25: skip fixture uses the production multi-label shape.
- R4-28: loop-2 fetch pins include the jq -s 'add // []' merge program.
- R4-29: cmdGate scenario where a refusal is OLDER than the fresh command.
- R4-30: takeoverEnum asserts its own sort:updated-asc qualifier.
- R4-31: multi-entry fixtures pin the max/last/length aggregation operators
  on CMD_TS, EVENT_TS, REASON, SUMMARY_POSTED, and the heal lever's
  LATEST_LABEL_TS/UNLABEL_ACTOR programs.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 5 — trust boundaries and evidence freshness

- R4-5 residual: PR_META now fetches author so IS_BOT_AUTHOR actually
  resolves (the exemption was dead on arrival), with a behavioral replay.
- R5-1: REMOVED_OK derives from the captured stream ('HTTP ' non-404 = not
  landed) instead of output emptiness — GitHub returns a body on success.
- R5-2: /retry only drops needs-human when management actually resumes
  (takeover label present or bot-authored) — an auto-released human PR
  keeps its escalation label.
- R5-3: conflict_paused requires a real cap notice AND a newer resume
  marker — label-present/notice-absent now fails closed toward paused.
- R5-4: a failed permission read defers the release (PERM_READ_FAILED),
  never counts as no-permission — at both evaluation points.
- R5-5: compute_resume_ts scans in-grace commands newest-first and
  permission-checks each (≤2 reads), so a stranger's echo can't shadow a
  maintainer's command.
- R5-6: the release branch re-fetches evidence and recomputes resume state
  immediately before the first write.
- R5-8: the heal re-checks the takeover label from the live payload before
  clearing needs-human.
- R5-9: same-second ties resolve toward resume/release suppression in both
  files (RESUME>=TERM; RELEASE_ACKED >= window).
- R5-10: the heal anchors to the current pause boundary (latest needs-human
  apply event); an absent anchor skips the cleanup, fail closed.
- Tests: whole-function compute_resume_ts replay (permission/shadow/tie/
  grace/refusal cases), heal anchor fixtures, toggle stub models the real
  DELETE body, runRearm orphan case.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 6 — contract-safe gates and subshell flag fix

- B5: the bot-fleet enumeration failure now degrades to a loud error row
  and falls through (FLEET_OK gate) instead of exiting before the
  independently-fed takeover/needs-human processing and the dashboard's
  liveness-watermark write.
- B12: the cap-branch LIVE_LABELS consent re-read fails closed on an
  unreadable gh pr view (a collapse to '' ignored a concurrently added
  skip for standard bot PRs).
- R6-1/R6-19: the takeover-release landed flag is keyed on the DELETE exit
  status (LBL_DEL_FAILED set inside the pinned idiom's failure branch) —
  never on output text, which lies in both directions. The
  pr-self-report-label idiom evolves identically to keep the cross-workflow
  contract green (and its own 'removed' log line no longer lies either).
- R5-4 residual: compute_resume_ts now returns via globals
  (RESUME_OUT/PERM_READ_FAILED) and both call sites invoke it directly —
  the previous  subshell silently dropped PERM_READ_FAILED, leaving
  the fail-closed defer branches dead.
- R6-3: command candidates are deduped by author before permission reads,
  so a stranger posting N commands can't burn the 2-read budget and shadow
  a maintainer's command.
- R6-4: an unreadable release history is reported as such, not as
  'released'.

* fix(autofix): close review round 7 — lever starvation, re-arm anchoring, permission shadows

- R5-7: the release lever gets its OWN enumeration of the paused population
  (takeover+needs-human, stale-first) instead of the long-lived needs-human
  display window — released-awaiting PRs aging back into that window could
  truncate exactly the fresh pauses that become release-eligible, starving
  the lever and making the zombie state permanent and self-feeding.
- R6-3: the 2-read permission budget now sets PERM_READ_FAILED on exhaustion
  (it was failing open), and the candidate walk sorts newest-first per author
  (group_by+max_by+sort) instead of unique_by's alphabetical order, so two
  read-only strangers can't shadow a maintainer's newer command.
- R7-1: the stale-label cleanup anchors on the current pause boundary (latest
  needs-human apply) and is marker-confirmed only — not keyed on TERM_TS, and
  never on command/label evidence — so a re-paused PR with a lost cycle-2
  notice isn't read as re-armed on stale cycle-1 evidence.
- R7-7: the /takeover stop success echo is gated on REMOVED_OK — a failed
  DELETE no longer logs 'removed'.
- R7-2: TAKEOVER_COMMAND/RETRY_COMMAND mirrored into the shepherd env and
  passed via --arg, so the resume matcher can't drift from the route.
- Tests: conflict_paused + re-arm guard behavioral replays, mirrored-command
  cross-file pin, engaged/released-with-skip ack matrix cells, LBL_DEL_FAILED
  branching, gnuDateShim hoisted to module scope, R4-24 nesting indices.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(autofix): complete the R4-16 ack-matrix DELETE-count coverage

Add fork-refused and skip-blocked ack cases to the takeover-ack harness
— management never resumed on either, so zero needs-human DELETEs, each
asserted by total DELETE count (not just toContain).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 8 — reachable re-arm cleanup, release race guards, pin census

* fix(autofix): close review round 9 — honest release-failed ack, cleanup attempt budgets, dashboard single-owner routing

- /takeover stop whose label DELETE failed no longer posts a
  'Takeover released' ack: a release-failed variant names the retry
  (R9-4), and the R7-7 echo pair gains symmetric log pins (R9-3)
- stale-label cleanups count ATTEMPTS like the release budget, so a
  DELETE outage trips the cap instead of leaving it inert (R9-5)
- dashboard renders each both-label PR exactly once: loop 1 defers by
  paused membership, loop 3 is the render of last resort (R9-1/R9-13)
- a 404 from the collaborators-permission endpoint classifies the
  author read-only instead of renewably deferring the release (R9-10)
- cap-branch release evidence reuses the per-iteration events fetch
  under a success flag (R9-18); release-clock comment corrected (R9-11)
- harness gates end-anchor the --json field list (R9-14/R9-15); the
  escalation POST and the ack-body census gain count pins (R9-16);
  the date shim answers only the +%s shape it emulates (R9-9)

* fix(autofix): close review round 10 Criticals — exact HTTP 404 release classification, isolated replay fixtures

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): close review round 11 Criticals — engaged stale-ack guard, exact HTTP 404 permission classification

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-15 17:32:23 +00:00
Shaojin Wen
90f754e73e
fix(ci): keep no-op review requests out of the PR review concurrency group (#9210)
* fix(ci): keep no-op review requests out of the PR review concurrency group

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(ci): pin precheck-pr bot login to the review constants

* test(ci): share one bot-login extraction across review-workflow suites

* fix(ci): route every review request to a per-run concurrency group

A bot-directed review_requested run joined the shared PR group on the
requested reviewer's identity, but whether it reviews anything is decided
later by authorize on the requester's write permission. A requester without
write produces a guaranteed all-skipped run that can still supersede a
lifecycle run sitting PENDING behind a still-terminating review — the same
lost-review race as #9091, through the bot-request door. Gate the shared
group on the action alone so no review_requested run can supersede a
pending lifecycle run; an authorized bot request still reviews immediately,
at the cost of an occasional duplicate review of the same head.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-15 16:42:57 +00:00
易良
9b39280078
fix(ci): skip non-bot review_requested siblings before jobs spend compute (#9204)
Opening a same-repo PR that touches CODEOWNERS-covered paths auto-requests every owner individually, so one PR open emits one review_requested run per owner (five within the same second on #8830/#9142). Only the bot-requested run can reach review-pr; the human-requested siblings used to spend a review-config runner plus an authorize job (CI_BOT_PAT permission API) each before no-op exiting. Mirror the requested_reviewer predicate precheck-pr already applies to fork PRs into authorize.if and review-config.if so the siblings complete as instant all-skipped runs.

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-15 08:38:57 +00:00
Shaojin Wen
e7a7ac1bfb
feat(autofix): deny-by-default footprint gate and positional window censuses (#9156)
* feat(autofix): deny-by-default footprint gate, positional window censuses, review-loop backlog

Follow-up to #8981/#8996, closing the structural causes behind their
review-round non-convergence:

- Deny-by-default footprint: every file a round touches maps to an AREA
  (declared workspace, else top-level directory, else the root file
  itself); areas outside the PR's own footprint are surfaced in a
  gate-authored advisory, or rejected retryably once the repo variable
  QWEN_AUTOFIX_FOOTPRINT_ENFORCE is staged to 'reject'. The enumerated
  class gate keeps rejecting regardless — this inverts the default for
  everything it cannot enumerate (a denylist is not a boundary).
- The three window censuses (PRIOR_TIMEOUTS, WIN_HEADS, PRIOR_HEADS)
  attribute comments positionally over their own scan-parsed eval
  markers instead of whole-body win= substrings: a neutralized marker
  quoted in a handoff excerpt, or any future marker embedding win=, can
  no longer double-attribute a comment (decoy fixture included; the
  census fixture's non-numeric round= placeholder is corrected).
- BITE_ENFORCE's reply arm inherits the thread root's CHANGES_REQUESTED
  membership, not just its body tag.
- Backlog tests: the bite restore-failure crash contract (verdict-less
  exit with the rejection document, driven by a ref-deleting runner),
  merge-base-anchored footprint compares under an advanced main
  (afterPr fixture hook), and the shrink+bite advisory append order.
- SKILL: cap each round's implemented batch (~8 findings, Critical
  first, defer the rest via comment-replies) — nine review rounds of
  evidence that oversized fix batches breed fix-of-fix defects — and
  document the footprint gate.

* fix(autofix): close the R1 footprint-gate findings

- Advisory lifecycle: one reset at gate start, every writer appends —
  the footprint advisory no longer dies to the shrink section's rm or
  its truncating write.
- Footprint membership is REF-ANCHORED: areas derive from the pre-round
  root manifest's workspaces globs (longest ancestor wins, nested
  workspaces correct), so a round cannot redefine its own boundary and
  the on-disk resolver is out of this path entirely; non-workspace
  paths under packages/ keep two segments so sibling projects stay
  distinct areas; emitted areas are newline-sanitized against phantom
  footprint grants.
- The enforcement knob rides step-level env at both verify gates —
  $GITHUB_ENV writes from earlier steps cannot downgrade 'reject'.
- TESTSIDE's critical() mirrors cr_attached (root and self), keeping
  enforcement and demotion on one comment set.
- Census ownership is LAST-WINS over scan-parsed markers (a stray
  quoted-or-appended marker cannot double-attribute), the replay decoy
  is now genuinely discriminating (old whole-body → 0, new → 1), and
  the growth-gate comment stops citing retired whole-body matchers.

Queued per the batch cap: per-line advisory bullets and the third sink
charset, discriminating fixtures at the two remaining census sites, the
reply-arm bite fixture, freight and merge-base footprint fixtures, and
digest-pinning the staged resolver for its remaining consumers.

* fix(autofix): close the R2 footprint-gate findings

- list_areas reads and translates the ref's workspaces globs ONCE per
  invocation and matches ancestors in-bash (was ~21ms git+jq+sed per
  file×ancestor call), emits printf %q keys — line-safe AND injective,
  so distinct areas can never collapse into one comparison key — and
  both render arms print one bullet per area from those keys.
- Producer failures are a STATE: a failed round- or PR-side diff (orphan
  history, transient git error) skips the footprint check loudly instead
  of shrinking one side into a verdict.
- The workflow-level FOOTPRINT_ENFORCE env is gone (the step-level pins
  are the only consumers and outrank it — dead config removed); the two
  step wirings are count-pinned.
- Fixtures: nested-workspace membership discriminates against the
  packages/ two-segment fallback (sibling nested workspaces stay
  distinct areas), and the advisory-lifecycle discriminator proves an
  earlier section's advisory survives the shrink section.

Queued: consolidating the six eval-marker regex variants behind one
grammar constant (touches six jq programs; its own change).
2026-08-14 17:28:16 +00:00
Shaojin Wen
4ee6a087e5
feat(autofix): judge review-feedback validity by content, not author (#8996)
* feat(autofix): judge review-feedback validity by content, not author

Wrong feedback drives wrong rounds regardless of who wrote it: maintainers
increasingly draft comments with models, so author identity carries no
correctness signal. The trust gate stays as the injection/authorization
boundary it always was, but the validity layer becomes source-blind and
execution-based, enforced by the verification gate rather than prose.

Three mechanisms:

- Bite check: a round changing both source and tests has its changed tests
  re-run against the pre-round tree (origin/<branch> sources with the
  round's test files overlaid). All green there means the claimed defect
  never reproduced — the shape of a plausible-but-false finding implemented
  as a fix — and the round is rejected, non-retryable, with the measurement
  in LAST_REJECTION so the next round can decline or escalate the finding.
  Fails open on every scope limit: single-workspace rounds only (gitignored
  dist carries the round's build across the detach, the same confound that
  A/B-exempts typecheck), runnable unit tests only, and any pre-round
  failure counts as biting.

- Sensitive-area footprint: a round may not expand into CI/verification
  machinery the PR itself never touched — .github/, .husky/, eslint/vitest/
  tsconfig configs, and the scripts section of existing root or first-level
  workspace manifests (the gate's own command surface). Judged by area
  class so takeover on an infra PR keeps full freedom; round-added
  workspace manifests are exempt. Rejected retryably (the repair pass can
  revert).

- Test-deletion advisory: shrinking coverage is surfaced by a gate-authored
  section in the round report (deleted files, net test lines), never by the
  agent's own prose, so a maintainer reads the agent's justification next
  to the machine measurement.

SKILL.md rewrites the address-review protocol to match: identical
verification for every author, probe evidence outranks any assertion,
refuted maintainer claims are escalated with the measurement instead of
silently obeyed or overridden, and severity tags alone no longer make an
item Required — the claim must be checkable and reproduced.

* fix(autofix): harden the validity gates per review round

- Scan round/PR diffs NUL-delimited with --no-renames: a rename out of a
  sensitive area now classifies the vacated source path (moving a
  workflow out of .github/ is a removal of verification machinery), and
  specially named files are no longer core.quotePath-mangled past the
  case patterns.
- Narrow the capability classes: .github/workflows|actions, .github/
  scripts, and passive .github metadata are separate areas (an
  issue-template PR no longer licenses workflow rewrites), and the
  transitive executable surface — repo scripts/ (minus scripts/tests/)
  and .npmrc/.nvmrc — joins the protected set.
- Gate the bite consequence on machine-read intent: rejection now
  requires the round to RESOLVE a Critical-tagged or CHANGES_REQUESTED
  finding (resolved-comments.txt matched against rc.json/rv.json);
  every other src+test round gets a gate-authored advisory on all-green
  instead — a behavior-preserving refactor pinning existing behavior is
  no longer rejected.
- Drop the blanket *.md exclusion from bite source detection: skill
  markdown is executable agent behavior, and the intent gating now keeps
  doc-only rounds safe from rejection.
- Sanitize deleted-test filenames in the gate advisory through a safe
  character set: a backtick in a legal git filename could close the code
  span and forge gate-authored markdown.
- Replace per-path basename spawns with parameter expansion.
- Tests: rename-evasion, metadata-vs-workflow class split, repo-scripts
  class with the scripts/tests carve-out, filename-forgery rendering,
  enforce-vs-advisory bite consequences (Critical tag and CR review),
  and tree-state-proving runners that flip on pre-round source with the
  round's test overlaid (plus the round-leak negative control).

One reviewed finding is declined with evidence in the thread: existential
batch semantics for mixed Critical rounds (per-behavior probe binding
needs test-result parsing; documented as a known limit at the check).

* fix(autofix): close the round-2 validity-gate findings

Sensitive-area scan: read NUL records directly (no tr re-mangling — a
newline filename cannot mint phantom footprint grants); resolve declared
workspace manifests and workspace-root configs through the trusted
resolver (nested workspaces protected, src-tree scaffolds exempt); split
root vs workspace manifest classes; guard the root workspaces array; give
the loop's own workflow and gate script their own class; classify .qwen/
(skills are executable agent behavior); anchor footprint content compares
at the merge base; sanitize violation paths in the rejection document.

Bite check: tolerate rc:-prefixed and CRLF resolved-comment ids (the
handle format SKILL prescribes — enforcement never fired without this);
count replies resolved in Critical-rooted threads as defect claims; skip
non-vitest workspaces (a vacuous --if-present pass must never reject),
self-package-name imports (dist confound), and rounds with paths outside
the resolved workspace; include renamed tests and changed snapshots in
the overlay; drop nested fences from the rejection document; surface
test-only defect claims as an advisory; document the already-fixed
re-raise limit and steer it to a no-code round.

Tests: classifier probe over every arm, footprint cases for the new
classes, enforce-vs-advisory negatives, reply-root enforcement, and the
rc:/CRLF handle round-trip.

* fix(autofix): close the round-3 Critical findings on the validity gates

- Gate-consumed helper scripts (resolve-owning-packages, settings-schema
  and contracts checks) join the autofix-loop class: an unrelated
  .github/scripts footprint no longer licenses rewriting machinery the
  gate executes.
- Skip round-scan files whose content equals current origin/main: a
  round that merges main (the flow SKILL prescribes on conflicts) made
  ROUND_RANGE degenerate and attributed all incoming main churn to the
  round, false-rejecting ordinary base updates.
- Round-added workspace-root configs are the round's own surface (same
  cat-file exemption manifests have); deleted workspace manifests are
  classified from pre-round existence instead of the on-disk resolver
  that can no longer see them.
- The bite vitest guard reads the PRE-ROUND manifest — the tree whose
  test script the detached runner actually executes.

* fix(autofix): close R4 validity-gate findings — gate-consumed surfaces join the taxonomy

- Supply-chain surfaces classify: lockfiles/shrinkwraps (root and nested)
  and patches/ (patch-package runs on every install) as supply-chain;
  .gitattributes (root and nested) as measurement-config — a -diff rule
  could blind numstat-based advisories.
- manifest_scripts_changed inspects resolution fields too: workspace
  manifests compare {scripts, exports, main, types}; the root manifest
  adds exports alongside workspaces.
- resolve-sandbox-image.mjs joins the autofix-loop class (it establishes
  the loop's isolation boundary).
- The noop path emits verified_head, making the prescribed no-code
  re-verification round mechanically able to resolve threads.
- The bite transcript is cleaned at gate start like its sibling logs;
  the advisory's test definition aligns with the growth brake's six
  globs (__tests__/, test-utils/ included).

R4-3 (post-round on-disk workspace resolution racing a same-round
workspaces negation) is declined in-thread: it requires the PR footprint
to already license manifest-scripts-root, which is the accountability
boundary working as designed; pre-round-tree resolution is queued with
the census follow-up. R4-5 (advisory in failure paths) queued likewise.

* fix(autofix): deflake the bite harness and align the test taxonomy

- Isolate fixture git from ambient global/system config (the sibling A/B
  fixture's GIT_CONFIG_GLOBAL=/dev/null pattern) and fail loudly on spawn
  errors with the exit status in the assertion message — the advisory
  sub-case intermittently died spawn-level under load with empty streams
  and no diagnostic (reproduced 1/6 locally, once on CI).
- BITE_SRC excludes __tests__/ like the gate's own TEST_PATHSPEC.
- SKILL's boundary enumeration names the supply-chain and
  measurement-config classes and the full protected manifest fields.

* fix(autofix): close R6 validity-gate findings

- Test-side defect claims take the advisory arm: when every resolved
  Critical thread sits on a test file (rc.json .path), the fixed test
  legitimately passes pre-round — enforcement grade 'advisory', never a
  rejection; the test-only advisory also no longer requires a matching
  *.test.* glob (snapshot-/helper-only resolutions surface too).
- Classifier arms: newline-bearing paths fail CLOSED as their own class;
  qwen-pr-safety-precheck.yml + pr-safety-precheck.mjs join autofix-loop;
  nested .npmrc/.nvmrc; eslint.legacy-filenames.mjs (imported by the lint
  leg's config); root manifest filter carries main/types.
- The self-import dist-confound guard matches the package name delimited
  (quote or subpath), so @qwen-code/qwen-code no longer swallows its
  -core sibling's imports.
- Test isolation extends to the footprint and advisory spawns (R5's
  rationale applied everywhere), spawn errors fail loudly there too, the
  classifier probe pins the supply-chain/measurement-config arms, the
  coverageOnly fixture asserts the advisory text, and the neutralization
  ledger header matches its count.
- The resolve-threads design doc records the widened no-op
  verified_head rule and its safety argument.

Deferred to the backlog per the convergence note: the bite-side restore
crash-contract test (shared-fixture work), origin/main-advanced footprint
fixtures, and advisory append-order pins.

* fix(autofix): close R7 validity-gate findings

- The resolve/reply pass is a shared function serving BOTH the pushed and
  no-op outcomes: the no-code re-verification escape can now actually
  resolve threads, and no-op declines finally post their in-thread
  replies (a pre-existing silence gap). The design doc states the shared
  path, its guards, and the named first-round residual.
- TESTSIDE demotion votes only over resolved CRITICAL threads (a source
  Suggestion resolved alongside no longer breaks it; a source Critical
  alongside keeps full enforcement) — three fixtures pin the matrix.
- The shrinkage advisory measures with --no-renames (a rename out of
  runner discovery is a shrink) and NUL-safe deleted names.
- Bite inputs pass through the merge-freight filter the class scan
  already applies, and BITE_SRC collects NUL-safe.
- Demoted rounds get their own advisory text (all-green is their
  expected shape, not a failed reproduction).
- The manifest block comment matches the resolver-backed code; the
  footprint and advisory test spawns get the isolation and loud
  spawn-error handling previously claimed — the R6 reply overstated
  that fix and this commit is the correction.

* fix(autofix): close the review-body re-checks on the validity gates

- Deleted-manifest classification honors the fixture exemption from the
  PRE-ROUND root manifest's workspaces globs (was_workspace_dir) — a
  deleted src-tree fixture manifest is no longer false-rejected, while a
  deleted declared workspace still classifies; the PR-footprint scan
  gets the same treatment anchored at the merge base, so a PR-deleted
  workspace keeps licensing later rounds.
- A config added into a PRE-EXISTING workspace is machinery (the gate's
  legs execute it); only a config born with its round-added workspace
  keeps the exemption.
- The shrinkage advisory applies the merge-freight skip per file (NUL
  numstat records), so a base-merging round is not charged main-side
  test churn in trusted-voice text.
- The bite rejection document renders filenames through the safe
  charset and collapses backtick runs in the runner tail below the
  outer fence length.
- AGENTS.md/CLAUDE.md classify as agent-policy; the root-manifest
  comparator covers lint-staged and config (sandboxImageUri) too.

Still standing by recorded design, acknowledged in the review body:
R1-9 (already-fixed re-raise), R1-27 (existential batch semantics),
R4-5 (post-round resolver vs same-round workspaces negation).

* fix(autofix): close the round-9 validity-gate re-checks

- TESTSIDE's critical() carries the CHANGES_REQUESTED review-state arm
  and receives rv.json, mirroring BITE_ENFORCE — a CR-enforced test-side
  claim demotes to the advisory arm, and a CR-enforced source claim can
  no longer collapse into it (R8-1, both directions).
- was_workspace_dir matches workspaces globs PATH-AWARE ('*' stops at
  '/', '**' spans, '?' single, '!' entries skipped conservatively): a
  nested src-tree fixture manifest deletion no longer false-rejects
  while a declared workspace deletion still classifies (R9-1); both
  pinned by fixtures.
- The PR-footprint manifest arm answers aliveness and membership from
  refs (origin/<branch> / merge base), never the round's on-disk tree —
  a PR-added workspace a round later deletes keeps its footprint class
  instead of walling the deletion (R9-3).

---------

Co-authored-by: verify <verify@local>
2026-08-14 09:54:51 +00:00
Shaojin Wen
22bacfe249
feat(autofix): escalate a non-converging diff to a maintainer handoff (#9104)
#8981's growth brake trims non-Critical feedback once a window's diff
grows past budget, but when the growth is Critical-driven (a complex
feature whose every fix opens the next fail-open gap the reviewer then
flags — e.g. PR #8777, 8 rounds, 13k additions) Critical-only cannot
help: the Criticals ARE the growth, so the diff keeps climbing and the
agent keeps patching.

Two additions on the autofix side:

- Feed the growth trajectory to the agent. feedback.md now opens with a
  "Diff growth this window" section (net src/test vs budget + how many
  prior rounds were over budget) whenever growth is measured, telling the
  agent to prefer minimal/subtractive fixes and to read a rising
  trajectory as a signal to escalate for a split, not add another guard.

- Detect divergence and hand off. A new per-round autofix-growth-now
  marker records each round's growth + over-budget flag; prepare reads
  the window's history and, once the brake has been over budget for
  >= GROWTH_DIVERGENCE_ROUNDS prior rounds (default 2, tunable) AND the
  diff has not shrunk from its worst, injects a "Needs a maintainer's
  decision — this PR is not converging" block. It is framed as a
  defer-to-human item, so the address run stops BLOCKED with a handoff
  (split / accept core + track the tail / redesign) instead of patching
  again. A diff that is over budget but shrinking, or a one-off
  overshoot, stays in ordinary Critical-only.

SKILL.md documents both blocks. Contract tests pin the knob, run the
extracted divergence detector against fixture history (climbing →
diverged, shrinking → not, sub-threshold → not, wrong-window → not), and
assert the growth-now marker is written on both report paths.
2026-08-14 09:04:49 +00:00
Shaojin Wen
b286875e72
fix(review): harden the pipeline against four live-run failures (#9086)
* fix(review): harden the pipeline against four live-run failures

Measured on three parallel PR reviews (qwen3.8-max, 2026-08-13, PRs
#9013/#9014/#9045) run via `qwen review run`:

- run.ts: pin the composed-verdict and report scans to the run's own
  target. The generic newest-composed scan captured a concurrent run's
  artifact — two of the three runs republished a neighbour PR's verdict
  (one reported REQUEST_CHANGES for a review whose own report said
  Comment). Also keep re-reading while the child runs: a coverage
  re-check legitimately recomposed a verdict 12 minutes after the first
  write, and the first-snapshot capture would republish the superseded
  one.

- budget.ts: drop placeholder gaps whose completion word carries a
  trailing budget adverbial. Three "none — all checks … completed
  within budget" non-answers reached two posted bodies because the
  completion idiom required the completion word to end the text.

- coverage.ts: label a non-chunk agent by the brief codename found
  anywhere in its launch prompt. Launchers prepend context lines, so the
  first-line label gave twelve finders one shared PR-summary sentence,
  and every budget-gap disclosure rendered as the same truncated PR
  quote instead of a name.

- copy_bundle_assets.js: emit dist/cli.js with a shebang and the execute
  bit. shellContextEnv blanks a QWEN_CODE_CLI a POSIX shell cannot exec,
  so every review subcommand issued from a session launched off the
  bundle silently fell back to the PATH's global install — all three
  runs executed the machine's auto-updated release instead of the tree
  they were launched from.

* test(review): follow the codename label into compose-review's fixtures

The backtick-collapse fixture's first line was itself the brief codename
shape, so the new codename extraction labels it `agent security` and the
first-line assertion no longer holds. Keep the sanitization intent on a
prose-only first line, and pin the codename behaviour — a prepended
context line must not reintroduce the shared-PR-quote label — as its own
case.

* fix(review): classify the run target with the child's own parser

Review feedback on the target pin: prNumberFromTarget re-derived PR
classification with a narrower regex than parse-args — /pull/<n>/files
URLs went unpinned, 0042 pinned pr-0042- while the child writes pr-42-,
and docs/pull/42 pinned a file target as a PR — so a completed (and
posted) review could be reported as one that produced no verdict.
Delegate to parseReviewArgs, whose verdict is what the child names its
artifacts from, and pin the divergent shapes as tests.

Also gate the bundle's shebang/exec-bit block with a package-assets
case (mode asserted off-win32, double-run must not stack shebangs), and
document the accepted same-target residual race on composedPatternFor.

* fix(review): pin run artifacts by exact target identity, share the identity-line parser

Round-2 review feedback, all six findings:

- run.ts: replace name-shape pins with the exact composed filename each
  target class produces (pr-<n> / file basename / the fixed 'local' token,
  per the skill's --out template). The (?!pr-\d+-) lookahead rejected a
  file run's own artifact whenever the reviewed file was named
  pr-<digits>-…, the PR branch's .* wildcard claimed that same artifact,
  and the pooled null class let concurrent file and no-target runs
  cross-capture each other's verdicts. Target classification now comes
  from classifyRunTarget (parse-args' verdict, basename for files).

- run.ts: newestArtifactSince returns {path, mtime}, so the capture poll
  reuses the scan's own stat instead of re-statting the path — the
  scan-vs-sweep window (and its untestable catch branch) is gone
  structurally.

- lib/agent-identity.ts: one parser for the identity line agent-prompt
  bakes into every launch, shared by cost-ledger's row labels and
  coverage's disclosure labels — the two hand-rolled copies could drift,
  and coverage's copy dropped the (round N) and owned-file suffixes,
  folding reverse-audit rounds into indistinguishable disclosure lines.
  cost-ledger still feeds it only the first line (quoted identity lines
  below must never be credited); coverage scans for the first
  line-anchored identity line (launchers prepend context lines).

- lib/budget.ts: one vocabulary for the budget-idiom family — 'below'
  joins the completion tail's position words, and the stayed idiom takes
  the same qualifiers ('stayed inside the tool-call budget').

- run.test.ts: handler-level assertion that the report scan is pinned
  (a strictly newer neighbour report must not become this run's
  reportPath), alongside the pattern-level cases for every shape the
  review probed.

* fix(review): round-3 review polish — CRLF identity lines, producer-side no-gap rule, edge-case pins

- agent-identity: tolerate a trailing CR (CRLF-recorded prompts fed
  through \n-splitting callers failed every parse and fell back to
  first-line prose); scan the launch prompt with one multiline match
  instead of materializing a line array per agent record; pin the
  round-over-file precedence with a both-suffixes test.

- agent-prompt: state the no-gap rule at the producer — write NO
  'Budget gap:' line when nothing was cut short — instead of leaving
  each agent to improvise a 'none' phrasing the consumer-side
  placeholder filter must chase forever.

- run.ts: strip trailing path separators before taking a file target's
  basename (a tab-completed 'src/' pinned 'qwen-review--composed.json',
  which no child artifact carries — fail-closed exit 1 on a completed
  review); pin file-run reports by their filename slot so a file named
  'pr-1234.md' claims its own report; document the two collision
  classes the basename-keyed pin defines (same-basename files,
  basenames spelling 'local'/'pr-<n>').

- budget.test: pin the parenthesis-form exception keep case beside the
  dash form.

* fix(review): round-4 polish — one budget vocabulary, named pin expectation, honest prompt claim

- budget.ts: spell the budget-position vocabulary once (BUDGET_QUALIFIED /
  COMPLETION_TAIL) and build PLACEHOLDER_GAP_RE from it — the literal
  carried three hand-copies that had already drifted twice in two review
  rounds; the space-separated 'tool call' form is pinned in both branches.

- agent-prompt.ts: the no-gap rule now states what actually happens to a
  'none' disclosure — at best filtered, and any unrecognized wording is
  published as a phantom coverage gap — instead of claiming the parser
  treats it as a gap, which was the negation of the filter shipped beside
  it.

- run.ts: derive the composed pin from composedNameFor and name the
  expected filename in both the no-verdict prose and the JSON result
  (expectedComposedName) — a naming drift between the pin and the skill's
  template was undiagnosable once Step 9 swept the near-miss.

- compose-review.ts: publicAgentSubject's provenance note now describes
  the codename labels coverage prefers, with first-line prose as the
  fallback.

* test(review): guard the no-verdict diagnostic and the pinned capture; align the chunk-role grammar

- run.test.ts: assert the no-verdict report names the artifact it waited
  for, in prose and as expectedComposedName — mutation-verified: dropping
  the suffix now fails.

- run.test.ts: force the neighbour's composed artifact strictly NEWER in
  the concurrent-run fixture. With it older, an unpinned newest-composed
  scan landed on the right file anyway and the regression passed;
  mutation-verified: reverting composedPatternFor to the generic scan now
  fails the handler test, not only the pattern units.

- agent-identity.ts: CHUNK_ROLE_RE takes coverage's CHUNK_RE shape
  (whitespace-tolerant, case-insensitive) so a hand-edited 'Chunk 3 of 7'
  cannot resolve as a chunk owner in the posted body and a role agent in
  the ledger row.

* fix(review): keep the bundle's write time across the shebang rewrite; hold the pins to the skill

Round-6 review feedback, all five findings:

- copy_bundle_assets.js: preserve dist/cli.js's atime/mtime across the
  shebang rewrite. stampReviewSourceDigest reads that mtime as the build
  time, so a bumped one certifies a bundle as newer than review sources
  edited before it and the staleness warning the skill's Step 0 stops on
  never fires. A full bundle stamps before reaching here, but a
  standalone run of this script — a flow the gate's own comment
  contemplates — was exposed.

- package-assets.test.js: pin both halves the block owes. The mtime is
  asserted against a fixture built 60s in the past, and the second run
  now arrives at mode 0644 so the exec bit must be re-set — demoting the
  chmod inside the shebang guard previously stayed green.

- run-skill-parity.test.ts: new. composedNameFor and reportPatternFor
  encode the bundled skill's Step 6 --out template and Step 8 report
  stems, and were pinned only against self-referential literals. This
  reads the templates out of SKILL.md and renders them per target class,
  so a skill-side edit fails next to the code that must follow it
  instead of silently in a later review.

- cost-ledger.test.ts: pin the first-line-only invariant — a launch
  whose prepended context sits above the identity line keeps the
  transcript's own id, never a label lifted from below. Consolidating
  both callers on labelFromLaunchPrompt now fails.

- agent-identity.test.ts: assert the two entry points genuinely differ
  on that prompt, so neither caller's policy can be collapsed into the
  other unnoticed.

* fix(review): stop the mtime assertion from pinning libuv's timespec truncation

The assertion compared the recorded mtime against the Date handed to
utimesSync, so it also pinned libuv's double-seconds → timespec
conversion: about half of all millisecond values read back 1 ns low
(X - 0.001), and builtAt is a fresh Date.now() - 60_000 every run — a
~50% coin flip that would have landed intermittent reds on unrelated
PRs through test:ci.

Capture what the filesystem actually recorded after the setup and
compare against that; the invariant under test is only whether the
shebang rewrite moves the stored time. 10/10 green through the CI entry
point, and it keeps its teeth: removing the production
fs.utimesSync(cliEntry, atime, mtime) restore fails it 3/3.
2026-08-14 04:38:49 +00:00
易良
6e21f72f57
fix(autofix): hold autofix rounds while review-pr is in flight (#8899)
* fix(autofix): hold rounds while review-pr is in flight (#8888)

* fix(ci): harden autofix review-in-flight gate

* fix(ci): ack deferred review fallback runs

* fix(ci): hold only cancelable automatic reviews in the gate (R2-1)

* fix(ci): bound review run fallback

* fix(ci): gate infra reruns behind review liveness
2026-08-14 02:13:02 +00:00
易良
fb6637f0d3
chore(ci): Add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and Scorecard (#9008)
* chore(ci): add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and scorecard workflows

* chore(ci): pin TruffleHog scanner version and drop invalid path input

* fix(ci): close security workflow review gaps

* fix(ci): fail package audit on install errors

* test(ci): pin security workflow guardrails

* fix(ci): pin security workflow test assertions for SHA refs, status edges, and push trigger

* test(ci): pin security workflow edge guards

* test(ci): pin security workflow contracts

* test(ci): pin secret-scan push guard

* fix(ci): quote secret-scan condition

* fix(ci): audit workspace package locks directly

* fix(ci): scope security checks concurrency

* docs(ci): explain mobile audit skip

* test(ci): link trufflehog version pin
2026-08-14 01:22:53 +00:00
Michael Yochpaz
60c338f144
fix(install): avoid Get-FileHash for Windows checksums (#9112)
* fix(install): avoid Get-FileHash for Windows checksums

* refactor(install): simplify Windows checksum verification

* test(install): require checksum resource disposal
2026-08-14 01:12:08 +00:00
易良
8e0033d64d
fix(ci): reduce ENOSPC and load-sensitive test flakes (#8982)
Some checks failed
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
npm cache producer / Save npm cache (push) Has been cancelled
* fix(ci): deflake the idle-watchdog env-parse script test

The test spawned three full agent subprocesses (~10s of wall time) to
pin a parse guard that is read once at module load. Under load spikes
on the shared self-hosted runner pool, one of those spawns failed or
stalled, failing CI at random (e.g. run 31556596385); the assertion
also discarded the script's failure.md, hiding the cause.

Collapse it to one run with the nastiest value (-1) — any armed
instant-true window kills the healthy stub agent at the first idle
tick — and assert on {status, failure} so a future flake names its
cause. Add retry: 2 to the script-test vitest config for the
remaining load-sensitive spawn tests; a real regression fails every
attempt.

* fix(ci): size test thread pools to the machine and retry load flakes

test:ci runs every workspace in parallel, and the cli/core suites each
pinned a fixed 8-16 thread pool regardless of the machine — on a 4-core
hosted runner that is an 8x oversubscription before any neighbor job
exists, and the shared self-hosted hosts run several registrations at
once. The contention is what blows the 15s ceilings those configs
already complain about: tests that pass locally in milliseconds time
out, and vitest workers lose their RPC under the pile.

Size the pools to os.cpus() (capped at the old fixed values so large
machines lose nothing) and add retry: 2 for the residual load spikes a
real regression still fails through every attempt of.

* fix(ci): restore the 0-boundary probe and right-size the deflake comments

Review feedback: the single -1 run could not catch a > 0 → >= 0
boundary edit (0 is the operator's documented disable sentinel and
would arm a zero-length window), and the comments overstated what the
run and the retry guarantee. Probe-verified: a >= 0 mutant now fails
the idleMs: 0 arm with the value named in the assertion diff. Two
short runs still halve the old three-run spawn volume. Reword the
retry comments to claim only deterministic regressions fail every
attempt.

* fix(ci): remove dead pool-resizing config and pin idle-timeout guard

The poolOptions.threads changes had no effect because vitest 3 defaults
to pool: 'forks', making minThreads/maxThreads inert. Revert to the
original fixed 8-16. The script-test deflake (idle-watchdog improvements,
retry: 2) is kept. Add a source-text pin for the Number.isFinite guard
in run-agent.mjs, covering the non-numeric class that subprocess-based
tests cannot exercise.

* fix(ci): capture the transient ENOSPC and shrink the manifest suite's inode hold

ENOSPC failed the Test step mid-suite on two different self-hosted
machines (actions-runner-test-22, actions-runner-test-11) in ~20s
bursts — 132 of 147 errors were mkdtemp failures — while the hosts
look healthy afterwards, so a post-mortem df finds nothing. Two
changes:

1. Sample /tmp space and inodes every 10s during the test step and
   dump the full df state when it fails, so the next occurrence
   records whether inodes or a tmpfs cap is what exhausts.
2. The manifest-repository-context suite held every 16k-file fixture
   tree until afterAll (~164k live inodes for the whole file); tear
   down per test instead so at most one tree is live at a time,
   removing the suite's own spike contribution either way.

* test(ci): pin the idle-timeout parse guard's source text for the NaN class

A healthy-agent run can never pin the non-numeric rejection class: a
NaN window never satisfies the >= kill comparison, so no run shape
fails on it. Pin the guard expression itself in the runner source
instead (this file's existing source-text pin style), and rename the
test to claim only the non-positive classes the runs actually pin.

* fix(ci): sample available memory alongside the ENOSPC diagnostics

The hosts' disks are verifiably not full, so byte exhaustion is out.
ENOSPC on a healthy disk points at a memory-backed limit instead: a
tmpfs /tmp or a job cgroup ceiling fails tmpfs writes with ENOSPC
while host memory is spiked by concurrent jobs, and clears within
seconds once they finish — matching the ~20s failure bursts. Sample
MemAvailable every 10s and dump /proc/meminfo on failure so the next
occurrence separates memory from inodes.

* fix(ci): limit retries to script tests

* fix(ci): route test temp files to the runner's disk-backed temp area

The ENOSPC bursts hit a host whose disk is verifiably not full, which
points at a memory-backed limit on /tmp (tmpfs mount or cgroup ceiling)
under concurrent-job memory spikes. Export TMPDIR=$RUNNER_TEMP for the
test step so mkdtemp traffic lands on the per-registration disk area
instead of the shared /tmp — curing the tmpfs case outright and also
stoping temp state from mixing across the several runner registrations
on one host. The sampler now reports the effective TMPDIR's filesystem.

* test(core): support long temporary workspace paths

* fix(ci): keep routed temp paths socket-safe

* fix(ci): use real short Linux temp paths

* test(ci): remove remaining teardown races

* test(ci): harden temp routing regression

* test(ci): stop leaked server reconcilers

* fix(ci): clean up test sampler reliably

* fix(review): keep skill context within manifest bounds

* test(web-shell): wait for image ingestion completion

* test(cli): avoid timed status line module import

* test(ci): remove remaining load-sensitive waits

* fix(ci): cap test forks on shared runners

* fix(ci): keep temp cleanup from failing tests

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-13 16:44:39 +00:00
Shaojin Wen
5a6452a2a5
fix(ci): make autofix verification gates hermetic to runner git config (#8961)
* fix(ci): make autofix verification gates hermetic to runner git config

A leaked global exec knob on the persistent pool (run 31516789251:
diff.external=global-driver in the runner user's ~/.gitconfig) failed
four per-hunk probe tests in packages/cli on #8613. The rejection was
charged to the round (package tests are A/B-exempt), which burned the
18-minute repair on a failure no repair can reach and ended the round
as a timeout — attempt 4 of the failure window, with nothing pushed.

Three layers against that class:

- Both verification gates (the review script and the issue-fix inline
  twin) now export a per-run throwaway GIT_CONFIG_GLOBAL (seeded with
  the workspace safe.directory) and GIT_CONFIG_SYSTEM=/dev/null before
  their first git command, so every check they spawn — vitest fixture
  repos included — is hermetic to the host, and a branch-authored
  `git config --global` dies with the run instead of poisoning the
  next one.
- The sanitize step (all three byte-identical copies) now also scrubs
  the runner USER's global config — denylist of the command-execution
  families only, so infra-owned routing/credential keys survive. This
  self-heals the live pollution on the affected runner on its next job
  and removes (not merely bypasses) a planted global core.hooksPath.
- test-efficacy.integration.test.ts gets the same GIT_CONFIG_GLOBAL /
  HOME isolation as git.integration.test.ts: the code under test pins
  --no-ext-diff, but the test scaffolding's plain `git diff` did not,
  so a hostile user git config could fail the suite anywhere.

Contract tests pin the gate redirects (before the first git command,
truncated per run) and functionally run the extracted scrub pipeline
against a poisoned fixture config, asserting the kept/removed sets.

* fix(ci): widen the config scrub and re-sanitize before PAT-bearing steps

Address the #8961 review findings (2 Critical, 8 Suggestions across two
reviews), all probe-verified by the reviewers:

- Denylist regex: subsection slots are .+ (git subsection names may
  contain dots — diff."a.b".command flattened past [^.]+), and the
  missing exec families are added: gpg.(*.)program, pager.*,
  interactive.diffFilter, difftool./mergetool., remote.*.uploadpack/
  receivepack. The functional fixture now covers every alternation plus
  dotted subsections, non-exec/corrupt/missing-config arms pin the two
  load-bearing '|| true' guards.
- The global scrub moved above the .git early-exit: host hygiene owes
  nothing to the workspace existing.
- New resanitize-git-config.sh (staged from the trusted base) re-runs
  the local allowlist sweep and the global denylist scrub at the top of
  both PAT-bearing git steps — the gates run branch test code on the
  host after the job-start sanitize, and the env redirect is not a
  filesystem boundary. Contract tests pin script/step lists equal, the
  staging in both jobs, the call-before-credential ordering, and run
  the script functionally against planted local+global keys.
- All three one-shot credential helpers lead with -c credential.helper=
  (empty resets the helper list; the first helper to answer wins, so a
  planted one must never run first). Count-pinned in the contract test.
- comment-status.integration.test.ts gets the same git-config isolation
  as its siblings; test-efficacy gains an isolation tripwire test that
  goes red if the redirect is removed, instead of only on hostile hosts.
- Comment fixes: the copies are cross-referenced as contract-test-pinned,
  and the system-config bypass is documented in both gates.

* fix(ci): close the XDG/env/transport bypasses around the config scrub

Address the #8961 round-2 review findings (5 Critical + 8 Suggestions,
probe-verified by the reviewers; the XDG listing gap independently
reproduced on git 2.55):

- The global scrub sweeps BOTH files of the global scope: with
  ~/.gitconfig and $XDG_CONFIG_HOME/git/config both present,
  `git config --global` lists/unsets only the former, so keys planted
  in the XDG file survived every copy. The scrub is now a loop that
  redirects GIT_CONFIG_GLOBAL at each file in turn.
- Denylist adds url.*.insteadOf/pushInsteadOf (transport rewrite of the
  PAT push/fetch; rest of url.* stays) and http.*.sslVerify/sslCAInfo
  (turns a kept http.proxy into a TLS-terminating interceptor); the
  three PAT helper chains lead with -c http.sslVerify=true.
- The staged resanitize script's provenance holds at cp time only —
  RUNNER_TEMP is writable by the branch code that runs in between — so
  the staging steps record its sha256 in GITHUB_OUTPUT and the PAT
  steps verify before executing.
- Both gates and both PAT steps export GIT_CONFIG_COUNT=0:
  GITHUB_ENV-injected GIT_CONFIG_KEY/VALUE entries apply at
  command-line precedence and outrank every file-level guard.
- Gates emit a ::notice when /etc/gitconfig exists (bypassed by the
  redirect — replicate needed settings via per-job env).
- Tests: the scrub's functional harness drives HOME/XDG fixtures and
  covers the new families; the resanitize run plants worktree-scoped
  config (deleting the rm -f line previously stayed green); the gate
  redirect block is executed against a hostile HOME and an env-planted
  GIT_CONFIG_* key; the isolation tripwire pins the NOSYSTEM leg and
  probes system-scope leakage.
- The process-env git isolation pattern is extracted into
  isolateHostGitConfig() in review/lib/test-utils.ts and adopted by all
  five suites that duplicated it; comment-status gains the same
  tripwire.

* fix(ci): take PAT git steps off host scopes and close the env channels

Address the #8961 round-3 review (5 Critical + 6 Suggestions,
probe-verified by the reviewer):

- Both PAT-bearing steps now run fully hermetic, same shape as the
  gates: a per-run throwaway GIT_CONFIG_GLOBAL + GIT_CONFIG_SYSTEM=
  /dev/null, so a concurrent job rewriting the shared ~/.gitconfig in
  the sweep->push window (max-parallel, one HOME across ~27 runner
  registrations) can no longer steer the push, and a URL-scoped
  sslVerify=false there can no longer override the -c pin. Both steps
  and both gates also strip the git ENV channels that outrank file
  config: GIT_CONFIG_PARAMETERS, GIT_SSL_NO_VERIFY/CAINFO,
  GIT_PROXY_COMMAND, GIT_EXEC_PATH, GIT_DIR/WORK_TREE, GIT_ASKPASS,
  GIT_SSH/_COMMAND, plus GIT_CONFIG_COUNT=0.
- The push-race salvage merge runs -c commit.gpgsign=false: a global
  commit.gpgsign=true with no key would exit 128 and be misread as a
  content conflict, discarding a verified round (R2-10).
- The maintainer-fork fetch, the one PAT-bearing network site the
  round-2 rollout skipped, leads with -c http.sslVerify=true
  -c credential.helper= (anonymous; public fork heads need no auth, so
  it fails closed on a 401 instead of feeding a planted helper the PAT).
- Denylist widens protocol.ext.allow to protocol.(ext.)?allow (the
  top-level fallback policy arms ext:: too) in all four copies.
- Tests: the two PAT hermetic blocks and the two gate blocks are pinned
  equal; the sha256 verify line is pinned verbatim and asserted to carry
  no bypass; the resanitize fixture plants a live XDG exec key (drops
  of the loop's XDG leg now fail); the gate redirect functional exec adds
  the env-channel unsets; diff-plan adopts isolateHostGitConfig (sixth
  suite) keeping its GIT_TERMINAL_PROMPT delta; comment-status tripwire
  gains the GIT_CONFIG_GLOBAL assertion.

* fix(ci): pin PATH, seal repo-redirect and env channels, harden all PAT sites

Address the #8961 round-4 review (6 Critical + suggestions,
probe-verified by the reviewer):

- PATH is pinned to a value the stage step records before any branch
  code runs, and LD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH are dropped, at the
  top of every PAT step and both gate steps — a $GITHUB_ENV-planted PATH
  or preload would otherwise swap the git/sha256sum/bash the digest gate
  itself runs on.
- The Prepare step (PAT-bearing, previously unhardened) now takes the
  same hermetic preamble as the push steps; all three PAT preambles are
  pinned identical by the contract test.
- The throwaway global config is created with mktemp, not a fixed
  literal path a same-user watcher could re-plant into after the seed.
- The env-strip list gains GIT_ALLOW_PROTOCOL (env twin of
  protocol.allow), GIT_COMMON_DIR / GIT_OBJECT_DIRECTORY /
  GIT_ALTERNATE_OBJECT_DIRECTORIES / GIT_SHALLOW_FILE (repo-redirect
  twins), across all PAT and gate copies; the salvage/fork fetches carry
  -c fetch.recurseSubmodules=false -c protocol.ext.allow=never so a
  planted submodule cannot execute an ext:: URL with the PAT.
- resanitize removes .git/commondir and .git/shallow (the file twins of
  GIT_COMMON_DIR/GIT_SHALLOW_FILE), and Push-and-report refuses to push
  a HEAD that is not the gate's recorded verified_head — closing the
  repo-redirect path that pushed attacker content.
- The gate runner (run-autofix-review-verification.sh) is now digest-
  verified before both gate passes, like resanitize already was: the
  branch runs its own build/test between the passes, so an unverified
  copy would let it define its own verdict.
- Contract tests pin every new surface: the three identical PAT
  preambles, the full unset var set, the mktemp path, the trusted-PATH
  wiring, the two gate-runner digest checks, the recurse/protocol pins,
  the HEAD==verified_head guard, and the commondir/shallow removal.

* fix(ci): pin gh env channels, harmonize allowlist subsection slots

Round-4 follow-ups:
- Pin GH_HOST=github.com and unset GH_TOKEN/GH_ENTERPRISE_TOKEN/
  GH_CONFIG_DIR before the first gh call in all three PAT steps, so a
  $GITHUB_ENV-planted GH_HOST cannot spoof the identity check and a
  planted GH_TOKEN cannot outrank the inline one.
- Harmonize the local allowlist's remote/submodule subsection slots to
  .+ (matching the denylist comment and preventing a dotted-name remote
  from silently losing its url/fetch on every resanitize).

* fix(ci): pin gh config dir and push the exact verified object

Round-5 closable findings before landing:
- R5-7: pin GH_CONFIG_DIR to a fresh mktemp -d instead of unsetting it,
  so PAT-bearing gh calls no longer fall back to the attacker-writable
  ~/.config/gh (whose config.yml can carry http_unix_socket and other
  transport reroutes) on the shared HOME.
- R5-8: push the exact verified commit object (PUSH_SHA:refs/heads/...),
  not symbolic HEAD which the push would re-resolve — closing the
  check-then-use race the verified-HEAD guard was added to close. PUSH_SHA
  is pinned to VERIFIED_HEAD under the guard and re-pinned to the merge
  result after each salvage merge.

The remaining round-5 Criticals (BASH_ENV/BASH_FUNC_* and LD_PRELOAD
executing at step-shell startup before any unset runs; GITHUB_OUTPUT
writable by gate-run branch code) are not closable from inside a Actions
step — they require runner-level isolation and are tracked as a
follow-up.
2026-08-13 11:39:04 +00:00
Shaojin Wen
9d55fab5f8
feat(autofix): brake review-round diff growth with per-window src/test budgets (#8981)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(autofix): brake review-round diff growth with per-window src/test budgets

Managed PRs bloat while still under the round threshold: #8853 grew from
315 to 1393 net lines in four bot rounds (86% of the growth was test
lines; one 'harden per review feedback' round alone added 609), and
#8276 grew ~2700 net lines under management. Every push regenerates
review suggestions, and every window re-arm reopens the five
suggestion-capable rounds, so the round-based Critical-only brake never
binds on the size dimension.

The prepare step now measures the branch's net diff vs the merge base,
split into test lines (*.test.* / *.spec.* files, __snapshots__/,
test-utils/, integration-tests/) and source lines, anchors a
per-counting-window baseline marker (autofix-growth-base, first-wins,
riding the window's first report comment like autofix-redcheck), and
engages Critical-only mode early once either dimension outgrows its
budget (vars.QWEN_AUTOFIX_GROWTH_BUDGET_{SRC,TEST}_LINES, default 400).
Two budgets rather than one because the measured bloat concentrates in
tests; a single budget cannot be tightened on tests without strangling
source fixes. The deferral preamble names the actual cause, and /retry
or re-engaging takeover re-anchors the baseline with the fresh window.
Critical findings, Request changes reviews, in-budget maintainer
feedback, failed checks, and conflict resolution flow exactly as before.

* feat(autofix): exclude mechanical churn from the growth measurement

Lockfiles (root and nested package-lock.json, npm-shrinkwrap.json) and the
regenerated settings schema arrive hundreds of lines at a time from a
single command and are skimmed rather than reviewed, so counting them
would burn the source budget on churn that carries no review burden. The
exclusion list names generated artifacts exactly — a broad glob would
silently exempt hand-written files from the budget. The fixture test now
proves a root lockfile ('**/' glob-magic at depth zero), a nested one,
and the exact schema path all stay out of the measured nets.

* fix(autofix): harden growth-brake measurement per review round

- Anchor the baseline marker under the window key prepare READ it with
  (LIVE_REARM_KEY), not the matrix WINDOW: supersede-exempt conflict
  rounds could write the live window's first marker under a dead key,
  letting the round's pushed growth escape the budget for the window.
- Apply GENERATED_EXCLUDES to the test-side measurement too: a lockfile
  under integration-tests/ would otherwise be excluded from NET_TOTAL but
  counted in NET_TEST, corrupting the NET_SRC subtraction.
- Count __tests__/ as test code, matching AGENTS.md's triage rule and
  repo-hygiene's PROD_EXCLUDE; suffix-less helpers there were charged to
  the source budget.
- Reject zero-padded budget values in the sanitize guard: [[ -gt ]]
  parses them as octal ('0400' brakes 144 lines early, '0900' silently
  disables the brake).
- Render signed growth values without a hardcoded '+' ('+-120' read like
  a misfire in the cause preamble, both languages).
- Fail open to zero when the three-dot diff has no merge base (orphan-
  history branches via fork takeover/adoption), mirroring the merge-tree
  conflict probe's fail-open.
- Retry the report post (3 attempts): that one comment carries the
  round's entire persisted state — watermark, round, redcheck head, and
  now the growth baseline — and the push has already landed by then.
- Behaviorally replay the sanitize fallback and the cause construction
  (three engagement shapes, both languages, sign rendering) instead of
  text-pinning them; extend the measurement fixture with a __tests__
  helper and a lockfile under a test directory.

* fix(autofix): close the round-2 review findings on the growth brake

- Spell the growth marker's window field key= instead of win=: the same
  report comment can legitimately carry a different window key than its
  autofix-eval marker (supersede-exempt conflict round after a re-arm),
  and three censuses attribute comments to windows by the whole-body
  substring win=<key> -->, which would double-attribute that comment to
  both windows (probe-flipped PRIOR_TIMEOUTS, WIN_HEADS, PRIOR_HEADS).
  A distinct token immunizes every such census without touching them.
- Make the deferred preamble's batch-budget sentence conditional: the
  OVER_BUDGET census only builds spans in round-brake territory, so a
  growth-only engagement below the threshold now states that maintainer
  feedback flows unaffected instead of promising accounting the census
  cannot produce.
- Special-case the report-post retry's final attempt: no trailing
  'retrying' + 10s sleep before giving up.
- Include __tests__/ in the env comment's test-line enumeration (the
  tunables doc must match the pathspec).
- Replay coverage for everything the mutation probes showed unpinned:
  the baseline wiring block (parseable/empty/malformed baselines), the
  no-merge-base fail-open (0/0/0 under -eo pipefail with the origin ref
  deleted), the report-post retry (single post on success; exactly three
  attempts, 'giving up', exit 1 on outage), the writer→scanner marker
  round-trip (negative src rendered from the real template and parsed
  back), and the budget-sentence branches in both languages.

* fix(autofix): close the round-3 growth-brake findings

- Invalidate growth anchors older than the latest stale-base auto-update:
  the update merges main into the branch and moves the merge base the
  nets are measured against, so an earlier anchor is no longer comparable
  — the next round re-anchors at the post-update size instead of
  misattributing overlap-resolution deltas to review growth.
- Pin the merge-base (three-dot) semantics: the measurement fixture now
  advances main past the divergence, so a two-dot regression changes the
  expected numbers instead of shipping green.
- Pin the sanitize guard's 7-digit cap (9999999 accepted, 10000000 falls
  back): past it bash integer literals wrap at 64 bits.

The census-side hazard (whole-body win= attribution vs multi-key
comments) is declined for this PR with the invariant documented at the
scanner: the growth marker's key= token cannot match any win= census,
and hardening the three censuses to positional attribution is queued as
its own change.

* fix(autofix): skip growth measurement when a managed fork head is named main

Prepare's fork path re-points refs/remotes/origin/main at the fork head
for a fork:main PR, so the three-dot measurement would compare the branch
against itself and report 0/0 every round — silently disabling the brake
while appearing to run. Unmeasurable is unmeasurable: skip and say so,
matching the no-merge-base fail-open.

* fix(autofix): treat an unmeasurable diff as a state, not zero nets

Zero-substitution anchored a bogus 0/0 baseline on the window's first
round (and manufactured phantom growth against an existing anchor).
NET_MEASURED now gates the whole brake: no anchor written, no growth
computed, no engagement — for both the no-merge-base and fork-head-
named-main cases, which are replayed with the skip line and flag
asserted.

* fix(autofix): close R6 — shadowed-base guard, loud unmeasured skip

- A local head branch literally named 'origin/main' shadows the remote
  ref in rev disambiguation, so the measurement would silently
  self-compare with NET_MEASURED still true — guard it alongside 'main'.
- The unmeasured state now announces itself instead of printing the same
  0/0 line as a genuinely empty PR.
- SKILL: the batch-budget sentence is scoped to round-threshold
  engagements, matching the workflow's cause-aware preamble.

R6-1 (.gitattributes steering numstat) is declined in-thread: the brake
is takeover-quality tooling on the accountability axis — a collaborator
with push access holds overt equivalents (removing the label), and a
.gitattributes flip is itself a visible diff.

---------

Co-authored-by: verify <verify@local>
2026-08-13 10:42:31 +00:00
易良
f159100c8e
chore(deps): bump sharp to ^0.35.0 to resolve GHSA-f88m-g3jw-g9cj (#8952)
* chore(deps): bump sharp to ^0.35.0 to resolve GHSA-f88m-g3jw-g9cj

* chore(vscode): regenerate NOTICES.txt for sharp 0.35 bump

* fix(scripts): read sharp pin from core package.json to prevent drift

The published CLI's sharp version was hardcoded in prepare-package.js,
which drifted from the workspace dependency on every bump. Read it from
packages/core/package.json so the publish pin always matches the declared
dependency. Add a test assertion to catch future drift in CI.

* fix(scripts): read sharp pin from package-lock.json instead of core package.json range

The previous approach read the sharp version from packages/core/package.json
(which has ^0.35.0) and stripped the caret, producing 0.35.0. This is the
range floor, not the lockfile-resolved version (0.35.3). Read from
package-lock.json so the published CLI ships the same version CI tests.

* fix(scripts): pin published sharp to core resolution

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(scripts): validate sharp pin against core manifest and add hoisted fallback test

- Align the lockfile reader with the sibling pattern in build-standalone-release.js:
  validate the resolved version against packages/core's declared sharp range
- Wrap the lockfile read in try/catch so a missing or malformed file
  surfaces a clear error instead of an opaque ENOENT/SyntaxError
- Add a test for the hoisted fallback path (node_modules/sharp) that
  actually executes in the current production release
- Add a comment explaining why sharp is exact-pinned like all other
  native optional deps in the published manifest

* fix(scripts): accept compatible sharp lock versions

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-13 06:56:10 +00:00
易良
ca44971815
fix(ci): cache downloaded linters on ECS runners (#9001)
* fix(ci): cache downloaded linters on ECS runners

* fix(ci): verify cached linter archives

* fix(ci): make linter cache writes optional

* test(ci): cover linter cache fail-closed paths

* fix(ci): harden linter cache setup
2026-08-13 05:13:23 +00:00
Shaojin Wen
187637449b
feat(review): cover modeled-system defect layers in the reverse audit (#8956)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(review): cover modeled-system defect layers in the reverse audit

A diff that models how an external system executes — a shell/git guard, a
sandbox, a permission interpreter — has a defect class the fixed dimensions do
not name: divergence between the model and the real system's STATE semantics
(what survives a function/eval/subshell/substitution boundary), not just its
syntax. That class is non-local and needs a differential oracle, so a static
single-model pass under-covers it, and the reverse audit's "two dry rounds"
stop rule is silent about any layer nobody walked.

Add a defect-layer lens across three sides, each independently revertible:

- Finder: the security pass gains a model-of-execution divergence hunt (run the
  real system as an oracle to discover it), and the invariant checklist gains a
  recursive-evaluator state-return contract for the cross-chunk half.
- Coverage: the reverse-audit brief asks each defect layer be walked and
  receipted on its own line; a taxonomy parses those receipts into per-layer
  coverage, and a standalone command reports it for A/B measurement.
- Cap: a deterministic gate emits one unreviewed-dimension entry per unwalked
  layer, capping a would-be Approve. It is opt-in via a repository-context
  domain, model out of the loop, and one-directional — it never ends the loop,
  blocks a Request changes, or changes convergence.

Extending the convergence rule itself so an unwalked layer keeps the loop
running is deferred behind an A/B on real modeled-system PRs.

* fix(review): fail-open the layer gate on a missing transcript dir

Address the review on this PR.

- Blocker: the gate's transcript read wrapped only the plan stat, not
  readTranscripts, which throws when the transcript directory is absent — so a
  manifest-marked diff in a transcript-less environment (a sandbox, a read-only
  HOME, a re-compose on a clean machine) crashed compose and posted nothing,
  contradicting the gate's own fail-open header. Wrap the whole read; add a test
  that exercises the real reader against a missing dir, which the injected-reader
  tests could not reach.

- Scope the automated cap to the shell/git model honestly: the brief and header
  promised a manifest-declared layer taxonomy for non-shell modeled systems that
  no channel supplies, so arming the sentinel on such a diff would owe the shell
  layers forever. Narrow the prose to what ships and name the manifest-taxonomy
  wiring as the follow-up that lifts the limit.

- Prefix the owed entry `reverse-audit layer coverage — ` so compose-review's
  caller-echo dedup cannot shadow the per-layer disclosures behind a `reverse
  audit` coverage subject; the verdict cap was already unaffected.

- Use the depended `glob` package in the measurement script instead of the
  experimental node:fs/promises glob.

* fix(review): corroborate and identity-anchor the layer-coverage reader

Address the second review round. The gate measured coverage from auditor prose
too readily, so a modeled-system diff with unwalked layers could release Approve
— the exact failure this feature prevents. Three probe-verified holes, all
closed, plus an end-to-end test that pins the cap through the real reader.

- Corroborate before a receipt counts: a transcript's receipts are read only
  when the harness's tool-call record shows it actually read the diff
  (diffToolCalls > 0, retirement's bar). A brief-only parrot holds every layer
  id from its own brief and can emit all six receipts without walking a layer;
  it has diffToolCalls === 0 and is dropped. successfulToolCalls > 0 would not
  drop it — the brief read is a successful call.

- Anchor the auditor selector on the launch IDENTITY line rather than a bare
  `reverse-audit` substring, which counted any transcript merely mentioning the
  role — a verifier inlining reverse-audit findings and quoting their receipt
  lines, a nested subagent — and pulled its finalText into the pool.

- Harden the receipt parser: a marker inside an inline code span or an indented
  code block is quoted, not used, and no longer parses as a live receipt. Allow
  a digit in a layer id so a custom taxonomy is not silently truncated.

- Refresh the now-stale module header (the cap ships, it is no longer "the next
  increment"), scope the arming docs to the shell/git layer set, and qualify the
  3B coverage claim (invariant-c runs only on heavy files; the cross-chunk
  contract backstops on the reverse-audit receipts).

The new compose-review cases exercise the real reader end to end: a partial-walk
auditor caps Approve to Comment, a full walk stays Approve, and neither a
diff-blind parrot nor a mis-identified verifier is counted.

* fix(review): track fences the CommonMark way in the receipt parser

Address the third review round.

- Critical: the receipt parser's symmetric fence toggle diverged from CommonMark
  three probe-verified ways, each releasing a QUOTED `Layer walked:` marker as a
  live receipt — a mismatched fence line (`~~~` inside a ``` block, or a shorter
  run) closed early, a list-item fence never opened, and a fence line with
  trailing content closed a block GitHub keeps open. Replace it with fence
  tracking that records the opening character and length, opens generously (0-3
  spaces, optional list prefix) and closes strictly (same char, >= length,
  whitespace only), biased toward skipping. The shared `usedLines` walk now backs
  both the parser and the `--infer` estimate, so neither credits a layer from
  quoted text.

- Correct the docs that overclaimed 3B coverage: Agent 2 does not run on a
  territory fan-out and the chunk agents do not inherit its brief, so the
  execution-model lens is 3A-only; on a huge diff the reverse-audit receipts and
  the cap carry the class, with invariant-c a heavy-file backstop.

- Fix the module header (coverage is the receipt, not a bare finding) and the
  test name that echoed it.

- Pin the gate's identity anchor against the real launch-prompt builder, so
  rewording the header cannot silently stop the gate selecting an auditor. Allow
  a digit in a layer id.

Deferred (non-blocking test gaps, noted in the thread): a test for the run-epoch
mtime fence, and extracting the measurement script's round-sort for a unit test.

* feat(review): carry the execution-model lens into 3B chunk agents

Two gaps a modeled-system review left open, both surfaced by the ongoing review
of the cross-worktree guard.

- The finder-side execution-model lens ran only on a 3A dimension fan-out; on a
  3B territory fan-out Agent 2 does not run and the chunk agents did not inherit
  its brief, so a huge guard/interpreter diff — the band this class lives in —
  got no finder coverage. Extract the lens into one exported constant and attach
  it to each chunk agent when the manifest declares the diff a modeled executable
  system, scoped to the chunk. Agent 2 still carries the same constant on 3A, so
  there is one source for both topologies. The cross-chunk contract still falls
  to the reverse-audit receipts and invariant-c.

- The state layers named only the ESTABLISH side of shell state. A model that
  grows an add-only map of function/alias definitions, export attributes, or
  options and never removes an entry diverges the moment the real shell removes
  one (`unset -f`, `unalias`, `export -n -f`, `set +a`). Name the removal side in
  the resolution-order and inheritance layer hints, in the lens's second bug
  shape, in the reverse-audit walk, and in invariant-a's collection check, so an
  auditor is led to check the removal path for every add path — the exact class a
  reviewer found when the guard's `definedBodies` map gained entries but modeled
  no removal.

* fix(review): close the layer gate's corroboration and fence leaks

Address the fourth review round — three release-direction gaps and cleanups.

- Critical: the receipt parser's fence tracker closed a list-item fence at any
  0-3 space indent, so a shallower closer released the quoted markers after it.
  Record the opener's indent (its content column) and close only at that column
  or up to three past it; a shallower or unrecognised closer keeps the fence
  open, biasing every remaining indent corner toward skipping.

- The corroboration bar was range-blind: `diffToolCalls > 0` passed an auditor
  that read a far chunk and then parroted its receipts. Add retirement's other
  half — the diff read must overlap the territory the launch prompt baked
  (`openedTheTerritory` + `bakedRanges`, now exported); a whole-diff auditor
  bakes none and still passes on the read floor.

- The empty-pool branch deferred to the reverse-audit-ran floor, but that floor
  has no diff-read requirement — so a run whose auditors all ran yet none read
  the diff went uncapped. Distinguish "could not measure" (fail-open) from
  "measured: auditors ran, none corroborated" (owe every layer).

- Cleanups: rehome the parseLayerReceipts JSDoc that had stranded above the
  fence helpers, drop the unused exported `coveredBy`, and pin the invariant-a
  removal clause with a test.

* fix(review): locate quoted regions with a real CommonMark parser

The receipt parser's hand-rolled fence/quote scanner diverged from CommonMark
round after round — each review pass probed another corner (mismatched fences,
list-item containers, trailing content, tab stops, HTML blocks, nested
blockquotes), and each gap released a quoted `Layer walked:` marker as a live
receipt. A second parser is a divergence hunt, and this skill's own rule is that
the oracle must come from the authority the code models, not a self-consistent
re-implementation.

Replace the scanner with `markdown-it` (already a workspace dependency, the
parser GitHub's own family uses): tokenize the return and treat every line inside
a fenced/indented code block, an HTML block, or a blockquote as quoted, reading
the block tokens' own line ranges. The receipt regex still guards inline
code spans (no leading backtick). This ends the divergence class outright —
HTML blocks, tab-indented code and nested blockquotes now quote their markers
with no new code, and the obsolete hand-rolled-closer test (which pinned a
divergent expectation) is replaced by cases verified against the parser.

Also (R4-5): stop attaching the modeled-system lens to an unreachable chunk,
whose one instruction is to return `Uncoverable:` and stop.
2026-08-12 18:15:11 +00:00
易良
464e8910e8
fix(desktop): harden release pipeline (#9009)
* fix(desktop): harden release pipeline

* fix(desktop): resolve release hardening review
2026-08-12 16:38:12 +00:00
易良
a32ec1ee4a
feat(desktop): add Aliyun OSS release mirror (#8976)
* feat(desktop): mirror releases to Aliyun OSS

* fix(desktop): harden OSS mirror workflow and tests

- Add ref guard to sync-desktop-to-oss.yml (dispatch only from main)
- Add diagnostic error messages for missing Windows/Linux installers
- Harden test: pin verify-index > 0 before ordering comparison
- Harden test: pin confirm-before-publish ordering and source comparison
- Add test: stable-only release validation in reusable sync job

* fix(desktop): harden OSS mirror permissions, stable-version guard, and non-latest repair

- Remove workflow-level actions:read; grant it only to the sync-oss caller job
- Reject suffixed versions for published stable releases in prepare
- Turn latest-feed comparison into a non-fatal check; condition publish/verify on match
- Assert both check_for_update call sites in release test
- Add jq stable-only guard assertion and endpoint default alignment test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-12 10:59:10 +00:00
Shaojin Wen
3a59332361
fix(ci): seed the dist-rebuild warning on every retryable A/B exit (#8958)
* fix(ci): seed the dist-rebuild warning on every retryable A/B exit

The baseline leg rebuilds dist/ from baseline sources, and every
retryable exit of the verify gate hands that tree to the repair
agent — but the "run npm run build first" steering note only
reached the green-baseline rejection. The comm -23 comparison
failure and different-signature exits sent the repair agent in
blind, free to trust or test against stale baseline artifacts
(the different-signature exit carried the note on #8765's branch;
the #8878 port kept it on the green exit only). #8765's post-close
round-3 review flagged the comm path as Critical.

Append the note on both missing exits and pin all three paths:
the DIFFERENT-reason test now asserts the note, a new test stubs
comm to fail and asserts the same, and the pre-existing test pins
the note OUT of its document — no repair runs for that verdict.

Mutation-tested, 3 of 3 caught: comm-path note dropped,
different-reason note dropped, note leaked into the pre-existing
document.

* fix(ci): single emit point for the dist note, name the comm-failure exit

Address the two review suggestions on #8958:

- The steering note existed as three byte-identical copies, and the
  "every retryable exit seeds the note" invariant depended on
  copy-paste — the exact drift this PR was patching (one exit seeded
  on #8765's branch, one lost in the #8878 port). Both reviewers
  flagged it. The string now lives in seed_dist_note(), called from
  all three exits.
- The comm-failure exit seeded the note but, unlike its sibling
  retryable exits, emitted no verdict-rationale line — an oncall
  could not distinguish "the comparison itself failed" from
  "baseline is green" without re-running the A/B. It now says so.

Mutation-tested: mutating the string inside the helper fails all
three path assertions at once; mutating the rationale line fails
the comm-exit test.

* test(ci): pin the no-identity baseline arm of the A/B gate (#8958)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-12 03:07:59 +00:00
顾盼
2cff1e7ad3
fix(ci): restore Live Host release mirroring (#8917) 2026-08-11 07:08:26 +00:00
Shaojin Wen
9946d13a35
ci: install tmux and zip tooling on the Linux test lane, and pin it (#8792)
* ci: install tmux and zip tooling on the Linux test lane, and pin it

Two suites go quiet on a runner without their tooling. capture-tui's
real-tmux block is `describe.skipIf(!hasTmux)`-gated, so on the hosted
ubuntu image — which ships no tmux — every real-tmux behavior skips inside
a green required check. The install-script packaging suite needs zip and
unzip the same way. This installs both where apt exists, bounded
(timeout-minutes) and advisory (continue-on-error absorbs the bound), so a
stalled mirror or dpkg lock neither hangs the job toward its 60-minute cap
nor reds the check.

A step that silently stops installing is the same outage as no step at all,
so it comes with a pin that reads the workflow the way bash does rather
than the way it looks. The parser is quote-aware (a `#` inside a message is
literal; a `;` inside one does not split statements), continuation-aware
(only an odd run of trailing backslashes continues a line, and a comment
never continues), and wrapper-aware (an apt-get behind sudo/env/nice/
timeout/time/nohup, with or without their options, or inside a subshell, is
still an apt-get).

On top of that it pins what actually keeps the step working: the
if-condition whole (a one-character `&&`→`||` took the already-installed
branch on a lane with zip but no tmux), the elif whole (a near-miss
falsifier like `false2 &&` killed the install branch), the else fallback
(deleting it removed the only signal on a lane with neither tmux nor
sudo), exact package tokens (`powerline-tmux` is not tmux), the flags that
make it work unattended (-y, sudo) and the ones that make it a no-op (-s,
--download-only and friends exit 0 having installed nothing), that nothing
may hard-fail the step in any errexit spelling or through a subshell exit,
that every apt-get REACHES a `|| echo` guard (an AND-list is transparent, a
pipeline or a `;` is not), and that every annotation reaches stdout
unredirected with `:⚠️:` at line start — a workflow command the
runner cannot see is just log noise.

Split out of #8388, whose capture-tui command is what made the tmux half
load-bearing. Each pin was mutation-verified against this workflow file.

* test(ci): close the pin's own escapes, and give its bash model tests

Thirteen findings on this PR, all against the pin rather than the step, and
two of them were the pin being WRONG rather than weak — those matter most,
because a pin that reds a legitimate edit is worse than one that misses.

Wrong: the redirect check matched `>` anywhere in an echo statement,
including inside the quoted message, so a semantics-preserving reword was
rejected — exactly what this file says it must not pin. And the sudo
requirement demanded literal `sudo apt-get` adjacency, which rejects
`sudo -n apt-get`, the sibling workflow's own convention. Both now ask the
question at the right level: quoted spans are blanked before any question
about shell syntax, and "runs through sudo" is asked of the raw statement.

Weak, each mutation-verified against ci.yml: `&>` was split as a separator,
so `echo ':⚠️:…' &> /dev/null` tore into a clean echo and an
unchecked redirect; apt's `-o APT::Get::Simulate=true` reached simulate
mode through a door the token blacklist never saw; the package pin matched
the whole logical line, so a package dropped from the install survived if
its token appeared in the guard's message; `apt-get update` was required by
nothing; `-y` and sudo were checked only on the FIRST install statement,
and nothing kept a foreign command off the install's chain to
short-circuit it; the step's `shell:` key — the one key that decides which
interpreter parses the entire block — was unpinned; the closing `fi` was
unpinned, so deleting it passed 5/5 while bash rejects the block and the
step runs nothing; and the else branch's annotation was a substring check
unanchored to any emitting command, so `: ':⚠️:…'` muted it.

The ~180 lines of bash-semantics helpers are the ORACLE every pin reasons
through, and they had no tests of their own. They do now — a table per
rule, each rule traceable to a mutation that escaped an earlier version.
Writing them corrected one of my own assumptions: `echo a \` + newline +
`b` joins to `echo a b`, not `echo ab`, because the space before the
backslash survives.

Also corrected the rationale in both files: they asserted in the present
tense that a real-tmux suite exists in this tree. It does not — it arrives
with #8388, and the tmux half of this step is deliberately pre-landed for
it. Saying so is the honest version.

* test(ci): close the pin's remaining escapes, and finish its bash-model fixtures

Four gaps survived the first pass, each probe-verified against the suite
before the fix:

- an always-failing `apt-get install` prefixed in the same && chain
  short-circuited the real install with every pin green, because every
  per-statement pin binds to a statement on the line and none sees the
  skip — now the step must carry exactly one install statement;
- `apt-get update` could be deleted or moved behind the install — now
  it must exist AND precede the install in statement order;
- `return` and `exec` were missing from the hard-fail blacklist, the
  same family as `exit`/`false` under the runner's `bash -e`: either
  leaves the required check green with nothing installed and not even
  the else-branch warning emitted (sandboxed verification's F1) — now
  blacklisted, with an end-to-end splice fixture pinning the axis;
- the guard walker's operator alphabet still split on the `&` of `&>`
  although statementsOf no longer does, reddening a guarded
  `apt-get … &> /dev/null || echo` — both now share the exemption.

Also finished the bash-model fixtures round one asked for: a quote
spanning a newline, and `timeout -k 1 5 sudo …` wrapper stripping.
Writing them exposed one oracle bug the round's own failure scenario
had measured: `set -- -e` assigns positional parameters, it does not
enable errexit — `--` now ends option parsing for the check.

Mutation battery against ci.yml: the five escape mutants (prefixed
install, return 0, exec true, update deleted, update reordered) all
flip red; the five tolerated forms (sudo -n sibling, dropped
--no-install-recommends, wrapper-wrapped update, &> redirect with the
guard intact, reworded warning) all stay green.

* test(ci): close the escapes a real edit would take, and stop there

Seven of sixteen findings, chosen by one question: would an honest edit hit
this? Each is mutation-verified against ci.yml.

- apt's long option spellings reached simulate mode past a `-o`-anchored
  regex (`--option APT::Get::Simulate=true`, `--simulate=yes`), as did
  `--version`/`--help`, which print and exit having installed nothing.
- The chain allowlist checked only a statement's leading word, so
  `&& sudo apt-get remove -y tmux` appended after the install passed while
  undoing it. It checks the subcommand now.
- `apt-get update` was pinned as load-bearing but never required to run
  through sudo, though that is the whole reason the install is.
- A quoted redirect target (`>'/dev/null'`) muted an annotation: blanking
  quoted spans leaves the operator, so the pin matches the operator itself.
- The already-installed branch's BODY was unpinned — emptying it to `:`
  left a broken-but-installed tmux undetected on the lane that takes it.
- The step was pinned before the test step but not AFTER the `ci_profile`
  step its `if:` reads; above it the condition is always false and the step
  silently never runs.
- The separator alphabet was encoded three times and the copies had already
  drifted. One definition now, used by all three consumers: a model of bash
  that depends on which function you ask is not a model.

The nine I did not take are recorded on their threads with reasons. Most
are adversarial rewrites — swapping branch bodies, wrapping the block in a
never-executing loop, nesting a conditional inside a pinned branch, hiding
the chain in a heredoc — and this file cannot win that game: anyone who can
restructure the step can also delete it. It guards accidental drift, which
is the failure that actually happens. One asks for a composite action
shared with qwen-autofix.yml; that is a repo-wide refactor and does not
belong in this PR.

* ci: make the install fail fast, bound its apt calls, and verify all three tools

Four review findings, all about the step rather than its pin, and all four
taken — they are the shapes a real runner produces.

`sudo` becomes `sudo -n`, the convention nine other steps in this file and
the sibling workflow already use: without it, a runner without passwordless
sudo gets a password PROMPT on stdin and the step hangs to its timeout
instead of falling through to the warning.

The `elif` tests sudo's USABILITY (`sudo -n true`) rather than its presence:
`command -v sudo` passes on a runner where sudo exists and cannot be used,
which takes the install branch straight into that hang.

The apt calls carry their own `timeout 280` inside the step's 5-minute
bound. When the STEP timeout fires there is no `|| echo` — the lane loses
its tooling and says nothing. A shorter command-level bound lets the guard
run and the annotation reach the check UI.

The already-installed branch verifies zip and unzip too, not just tmux: it
is taken when all three are present, so a broken-but-present zip failed the
packaging suite with no warning explaining why.

The pins moved with the step: "runs through sudo" now walks the wrapper
prefix (a bounded install is `timeout 280 sudo -n apt-get …`, and requiring
sudo FIRST would have redded it), the elif equality carries the new
condition, and all three tool probes are pinned to exist AND to be guarded.
That last pin needed a second pass: matching by substring reported the zip
probe present after deletion, because `unzip -v` contains `zip -v` — it is
word-anchored now, and each of the three deletions turns it red.

* fix(ci): install the tooling on root lanes too, where sudo does not exist

The Test check reds on a lane that ships no zip: install-script.test.js
throws at module load on a CI host missing zip/unzip — loudly, by design —
and this step could not prevent it on a root-container lane, because its
only install branch required `sudo -n true`, and root lanes have no sudo
(and need none). The step fell through to the advisory warning, nothing
installed, and the packaging suite threw inside the required check.

Add a root branch ahead of the sudo branch: uid 0 runs the same bounded,
guarded chain as-is. Lane probes under the runner's exact bash flags: a
root lane installs without sudo, a sudo lane installs through sudo -n, a
bare lane warns and exits 0, and an apt failure still reaches the guard.

The pins moved with the step: each branch must carry its own
update-then-install chain with exactly one install, and the branch decides
the sudo shape — the root chain must NOT reach for sudo (on a lane with no
sudo binary that exits 127, the guard fires, nothing installs), the sudo
chain must. Six mutants red, pristine green.

* fix(ci): fit the apt bounds inside the step cap, and pin what still escaped

* test(ci): close the pin's round-4 escapes, and stop reddening its rewordings

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(ci): close the pin's round-5 escapes with a branch-body allowlist

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-11 05:47:39 +00:00
qqqys
a86f1e5c0a
fix(ci): stream autofix agent progress (#8895)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* fix(ci): stream autofix agent progress

* fix(ci): preserve autofix API error retries

* fix(ci): trust structured autofix results

* fix(ci): bound autofix stream processing

* fix(ci): ignore oversized autofix stream lines

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-11 01:33:46 +00:00
Shaojin Wen
5dc98240c7
fix(ci): port the verify gate's remaining hardening from #8765 (#8878)
#8816's branch accidentally carried #8765's early commits, and the
takeover loop evolved the gate further there (subset identity via
comm -23, the retryable third arg, subset fixtures) — so #8765 closes
as subsumed, and this PR ports what main still lacks: the two
improvements its reviewers named for porting, plus the open round-6/7
findings that survive on main's gate.

- Pre-detach short-circuit: an empty head signature (vite/esbuild/
  crash — the KNOWN LIMIT class) fails closed regardless of the
  baseline, so decide it BEFORE paying the detach + full baseline
  re-run + restore.
- Build-dirt guard: the A/B'd build REWRITES a tracked file (the
  vscode companion settings schema), and the undiscarded rewrite makes
  either checkout refuse — degrading a real verdict into the crash
  path. `git restore -- .` before both checkouts; tracked-only, and
  the tree was asserted clean before the deterministic checks.
- Restore-failure semantics: a plain outcome=failed is an EVALUATED
  rejection — the watermark advances and a transient git failure
  strands the item as a permanent human handoff. The gate now leaves
  outcome unset (the gate-crashed path retries next scan) and still
  writes the detail document so the crash comment explains itself.
- The dist-rebuilt steering note seeds the repair feedback on both
  retryable A/B exits — the repair agent's only warning that dist/
  holds baseline-built artifacts.
- The stale-base retry handoff prefixes its embedded rejection with a
  the-base-has-moved note, so the retry agent is not steered toward
  no-action by framing written before the auto-update.
- The two A/B side logs joined the repair step's cleanup list.
- Tests: identity-less short-circuit, tracked-dirt survival,
  verdict-less restore crash, long-preamble render cap, PREEXISTING
  clause selection through the executable report harness, and the
  stale-framing note pin.

Mutation-tested, 5 of 5 caught: short-circuit dropped, restore guards
dropped, restore-failure reverted to the evaluated rejection, dist
note dropped, stale-framing note dropped.

Co-authored-by: verify <verify@local>
2026-08-10 13:58:27 +00:00
Shaojin Wen
7f61e03b82
perf(ci): make the triage budget operator-tunable and raise it (#8810)
* perf(ci): make the triage budget operator-tunable and raise it

The triage job's fixed 30-minute cap was killing every substantial PR's
triage at exactly the budget — five observed (8723, 8726, 8731, 8764,
8801), each a full agent run discarded — and the successful-run
distribution shows why: 22 jobs sampled ran median 5.8m / p90 22.3m /
max 29.5m. A max just under the cap is a distribution being clipped,
not one that fits.

Back the budget with QWEN_TRIAGE_TIMEOUT_MINUTES (fallback 60), the
same pattern as the autofix fleet caps in #8731 and probe-verified
there for expression-valued job fields. Triage is advisory: the worst
case a longer budget risks is one held runner, while a killed run
wastes the same runner and the work it was about to publish.

Pinned by a test asserting the variable-backed form with its fallback;
reverting to the fixed literal fails it.

* Address review: sanitize the knob, pin the active line

Both findings verified before fixing.

The knob was unvalidated, and the failure mode is nasty precisely
because the knob exists to be changed WITHOUT a PR: a malformed
repository variable — '60 minutes', '1h', a 6O typo, quotes, a decimal
— would make fromJSON throw at expression time, killing the triage job
before any step runs: no always() finalize, no status comment, a
silent stop whose run log blames fromJSON rather than the variable.
And '0' parses fine into an instantly-cancelled job. timeout-minutes
cannot be validated by the job that consumes it, so the authorize job
(which triage already needs, and which can run bash) sanitizes it:
bare-integer check, floor 10, ceiling 600, every rejection warning
with the VARIABLE's name. The triage job consumes the sanitized
output; || 60 covers the events where authorize is skipped.

The pin was an unanchored substring over raw YAML, satisfied by a
commented-out line while the job silently inherits GitHub's 360-minute
default — the review probe-verified that mutant surviving. The pin now
matches active lines only, and the sanitize step is replayed verbatim
against the review's whole enumeration: unset, sane, zero, runaway,
and five malformed shapes, each asserted to fall back and name the
knob.

Mutation-tested, 3 of 3 caught: the timeout line commented out, the
floor clamp dropped, the integer check bypassed.

* fix(ci): clamp over-long triage budgets before bash arithmetic (#8810)

A repository variable with more than 18 digits overflows bash's 64-bit
arithmetic, and the wrapped residue can land inside [10,600], silently
skipping both clamps with no warning — the one malformed shape where
the sanitizer's diagnostics go dark. Check the digit count before the
arithmetic and clamp such values to the ceiling with a warning.

Also pin the seams the replay test cannot see: which repository
variable feeds RAW, that the sanitize step is unconditional, the exact
clamp boundaries, the 10# decimal guard, and the overflow clamp.

* fix(ci): strip leading zeros from triage budget before the width guard (#8810)

* fix(ci): pin triage timeout seams on parsed YAML and runner shell flags (#8810)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 12:40:21 +00:00
顾盼
c0c0352e25
feat(cli): mirror Live Host releases through OSS (#8674)
* feat(cli): mirror Live Host releases through OSS

* fix(cli): harden Live Host OSS mirror fallback
2026-08-10 11:17:49 +00:00
Shaojin Wen
358091833b
fix(ci): watchdog silent sandbox hangs and reap the containers they leak (#8816)
* feat(ci): A/B deterministic gate rejections against the pre-round ref

A deterministic rejection in the autofix verification gate is only
chargeable to the round if the same check passes without the round's
commit. The gate charged every red to the fix unconditionally, and run
31276008548 measured what that costs when the premise is false: PR
8614's branch predated #8693's tsconfig guard while node_modules came
from the post-#8693 trusted base, so `npm run build` was equally red at
origin/<branch> — 63 minutes of accepted agent work discarded, an
18-minute repair burned on a failure the repair agent is forbidden to
touch (it may only amend the round's own fix), thirteen rounds in a
row, and the same again on the #8616 leg.

On rejection the gate now re-runs the failing check at origin/<branch>
(the branch as pushed, before the round) in the same environment:

- baseline green: today's path exactly — outcome=failed,
  retryable=true, the repair pass gets its chance.
- baseline red too: outcome=failed with preexisting=true and NO
  retryable. The repair step keys on retryable and is skipped — it
  cannot reach a failure outside the round's diff by construction —
  and gate-rejection.md says outright that the branch needs a base
  update (merge main), which flows into the failure comment as-is.

Fail-closed toward today's semantics: any A/B infrastructure problem
(missing ref, checkout failure) charges the fix as before, and a
restore failure after the baseline run rejects outright since the tree
can no longer be trusted. The round's work is still not pushed — this
changes the verdict's honesty and cost, not the push policy.

Tested by executing the real script in a real two-remote git repo with
an npm stub whose failures are keyed by commit SHA: round-caused red
(baseline green), pre-existing red (both red), and the untouched green
path. Mutation-tested, 3 of 3 caught: skipping the A/B, claiming
pre-existing without measuring, and dropping the tree restore.

* Address review: bound the A/B to checks it can honestly compare

All seven findings verified before fixing; the three Criticals were
each a way the A/B compared something other than the check that failed.

R1-1 — the contracts check feeds on stdin, which its first run drains;
the baseline leg re-ran against EOF and checked an empty file list.
R1-3 — the schema check's verdict rides on packages/core/dist, which
the core-rebuild guard built from ROUND sources and which, being
gitignored, survives the detach. Both checks are now A/B-exempt
(run_check_no_ab): their baseline verdicts prove nothing, and their
rejections stay where the repair agent can actually act on them.

R1-2 — a workspace the round ADDS does not exist at the baseline, and
npm exits 1 there with "No workspaces found" (measured; --if-present
forgives a missing script, not a missing workspace) — a round-caused
failure misread as pre-existing, skipping the one repair that can fix
the round's own package. The per-package loop now A/Bs only when the
workspace exists at origin/<branch>.

R1-4 — a chatty PASSING baseline used to flood the tail -c 3000
evidence window and push the actual failure text out of
gate-rejection.md, the sole carrier into the repair feedback, the PR
comment, and the next round's LAST_REJECTION. The baseline transcript
now goes to a side log and only a FAILING tail is merged back, where it
is the evidence.

R1-5 — the pre-existing paragraph pushed gate-rejection.md past the
report's head -c 3500 cap, truncating the closing fence for branch
names past 44 characters. Cap raised to 3900, invariant comment
updated with the new arithmetic.

R1-6 — preexisting=true had no read site. It now flows verify →
Finalize verification → the failure report, whose headline swaps the
generic gate clause for "PRE-EXISTING failure … needs a base update
(merge main)".

R1-7 — the no-round-commit guard was unpinned (deleting it kept all
tests green). Now exercised through the core-rebuild path, the one
A/B-eligible check that runs before the commit gate.

Four new behavioral scenarios (chatty baseline, no-commit round,
A/B-exempt checks, round-added workspace) plus workflow pins for the
forwarding, the clause, and the cap. Mutation-tested, 4 of 4 caught:
schema back to A/B (3 tests), guard dropped, side log reverted,
no-commit guard dropped.

* Address review round 2: A/B only what it can prove, prove what it claims

Ten findings across two rounds, each verified before fixing. The three
deepest share one lesson: the A/B is only sound for a check whose
inputs travel entirely with the git ref, and whose failure it can
IDENTIFY, not merely observe.

R2-1 — rc=1 at both legs does not make them the same failure: the
branch can fail for reason A while the round fails for reason B, and a
baseline infrastructure hiccup is a nonzero exit too. Pre-existing now
requires a MATCHING failure identity — tsc diagnostics normalized to
file + error code (positions shift with the round's edits), compared
via comm(1) on a per-check transcript. No diagnostics on either side
means identity cannot be established and the round stays charged.

R2-2 / R2-7 — gitignored dist survives the detach carrying the ROUND's
build, so any dist-consuming check A/Bs reverted sources against
round-built artifacts: package tests (channel-base resolved through
dist exports) and typecheck (sdk-typescript resolves core's d.ts —
probe-verified three-arm flip). Both are now A/B-exempt, as is lint,
leaving `npm run build` — the incident class, and the one check that
rebuilds its own inputs from the checked-out sources — as the sole A/B
candidate. The workspace-existence guard dissolves with it.

R2-3 — the fixture inherited the caller's global git config; a failing
global pre-commit hook broke all seven cases. The harness now isolates
GIT_CONFIG_GLOBAL/SYSTEM for every git child, and the suite is proven
green under a deliberately hostile hooksPath.

R2-4 — Finalize verification now selects preexisting from the same
attempt whose outcome it selects (repair verification included).

R2-5 / R2-8 — the "merge main" advice is now conditional at both
layers: the script paragraph states the measured fact and hedges the
remedy; the report headline uses the compare the step already ran —
behind/diverged gets the base-update clause, an up-to-date branch is
told its own pre-round code needs attention.

R2-6 — the rejection document now sizes its evidence tail against its
preamble (floor 500 bytes, total under the 3900-byte render cap), so
the closing fence can no longer be truncated off by a long branch name.

R2-9 — dissolved by R2-2: package tests no longer A/B, the guard and
its uncovered positive branch are gone.

R2-10 — the baseline-evidence merge is now pinned: the pre-existing
scenario asserts the baseline leg's own failure line (keyed by its SHA)
reaches gate-rejection.md.

Eight behavioral scenarios; mutation-tested 5 of 5: identity dropped,
typecheck re-enrolled, package tests re-enrolled, evidence merge
dropped, fixed tail restored.

* Address review round 4: sharpen identity, stage the git failures, sync prose

Nine findings, all refinements — the design held, the edges did not.

Identity now keeps the diagnostic MESSAGE (file + code collide: two
unrelated TS2339s in one file compared equal, skipping a repair that
could have shipped — probe-reproduced by the review), and the fixture
emits a SHIFTED position on the baseline leg so the position strip is
load-bearing instead of decorative (deleting the sed survived every
test before; it fails one now). vite/esbuild failures still yield an
empty signature by design — documented as the fail-closed limit rather
than half-widened.

The fail_signature assignments take `|| true`: grep exits 1 on the
normal no-match case and survives errexit today only because the caller
sits in an if-condition — a future unconditional call site would crash
the gate verdict-less.

The restore-failure branch is now stageable and staged: the baseline
leg recreates (untracked) a file the branch tracks, the checkout back
refuses, and the test pins retryable-not-preexisting with the
'could not restore' label. Relaxing the branch to `|| true` fails it.

Prose synced to the mechanisms that replaced it: the render-cap
invariant restates against the dynamic tail budget (the old 3000-based
arithmetic would misguide the next retune), the no-round-commit guard
comment names the core rebuild (schema/contracts left the A/B last
round), the describe wording counts both A/B-eligible builds, and the
pre-existing clauses no longer claim "the repair pass was skipped" —
with REPAIR_PREEXISTING forwarded, repair may have RUN; they now state
the invariant that is true either way: repair may only amend the
round's own fix, so it cannot reach this failure.

Mutation-tested, 3 of 3 caught: position strip dropped, message dropped
from the identity, restore rejection relaxed.

* fix(ci): watchdog silent sandbox hangs and reap the containers they leak

Four autofix rounds have died the same way (#8663 twice, #8761 r3,
#8763 r4): the agent's last output is the sandbox wrapper's
"ContainerName (regular): …" line at docker container entry, then
nothing — not one event — until the 2-hour absolute budget kills the
round. Four different runners, two image versions: systemic, not a bad
machine. Where exactly the container wedges is still unknown (that
needs docker state on the runner); what is certain from the logs is the
shape — a wedged sandbox produces NOTHING, and a legitimate run is
never silent for long (the fleet's longest tolerated quiet is the
review pipeline's 10-minute stream-idle window for thinking phases).

Two mitigations, each aimed at a measured half of the damage:

- run-agent.mjs gains an idle watchdog (QWEN_IDLE_TIMEOUT_MS, default
  20 minutes = 2x that longest legitimate silence): zero output for the
  window kills the agent with a distinct "idle-timeout … the sandbox
  likely hung at startup" detail, so the failure comment names the
  right knob and a hung round costs 20 minutes instead of 120. Polled,
  not reset-per-chunk — a busy stream should not spend its time
  re-arming timers.

- Both sandboxed jobs reap stale qwen-code-* containers at job start:
  a budget kill reaps the HOST-side docker client, not the container,
  so every killed sandbox keeps running on the persistent runner —
  observed directly when a later leg's container-name counter found
  qwen-code-0.21.8-0 already occupied and picked -1. One job per runner
  at a time makes any container alive at job start stale by definition.

Tested by executing the real run-agent.mjs end to end with stub agents:
the hang shape (one line, then silence) dies at the idle window naming
the idle limit, and a slow-but-talking agent that outputs every 400ms
across a 1500ms window survives to a clean exit — the test that
distinguishes a watchdog from a disguised absolute timer. Mutation-
tested, 3 of 3 caught: watchdog disabled, last-output tracking dropped
(the disguised-timer regression), cleanup dropped from a job.

* Address review round 5: the gate's verdict defects and the reaper's live kill

Budget-warning round — the five Criticals from both reviewers, no
suggestions (each deferred with a recorded reply).

fail_signature: `[^\n]*` in an ERE bracket expression does not mean
"rest of line" — in POSIX bracket expressions `\` is literal, so it
matched "neither backslash nor the letter n" and truncated every tsc
message at its first n. Nearly every real message has an early n
("Cannot find name", "is not assignable"), so distinct same-file
failures collapsed into identical signatures and a round-caused failure
could be labeled pre-existing, skipping the repair. grep is
line-oriented: `.*` is exactly the rest of the line. New fixture: two
messages differing only after their first n.

Pre-existing verdict: the intersection test mislabeled in both
directions. A round that ADDS a diagnostic sharing one normalized line
with the baseline was called pre-existing (repair skipped for a
round-caused, repairable failure); and `comm -12 | grep -q` under
`set -eo pipefail` SIGPIPEs comm (exit 141) once the shared output
outruns the pipe buffer, charging true pre-existing failures to the
round — the exact 18-minute repair waste the gate exists to kill.
Pre-existing now means the round's failing set is a SUBSET of the
baseline's, and the difference is captured before testing. New fixture:
a round adding a second diagnostic to a failing baseline.

Restore failure after the baseline leg: was retryable=true with HEAD
still detached at the baseline commit — the repair agent works in that
very checkout and does no git recovery, so its commit would land on the
baseline and be orphaned. Now rejected non-retryable (reject_fix grows
a third arg); the next round starts clean from the trusted checkout.
The restoreClash test pins the new semantics.

Stale-container reap: the premise "a runner runs one job at a time, so
any live qwen-code-* container is stale" holds per runner registration,
but the filter queries the docker daemon, which is per host — and this
pool runs several registrations on one OS. With per-issue/PR
serialization only, a concurrent job's sandbox is a substring match
away from `docker rm -f`. The reap now takes only provably-dead
containers (--filter status=exited/dead, both jobs) and the comment
says why a running one is left alone.

Preamble printf: the `\`` escapes sat inside a single-quoted format
where backslash is literal, so every pre-existing rejection rendered
raw backticks instead of code spans (shellcheck SC2016). Backticks
need no escaping there. Also syncs the side-log comment to the dynamic
tail_budget it actually renders.

Verified: scripts suite 140/140 (was 138; the two new fixtures and the
rewritten restoreClash test all fail against the pre-fix script),
npm run build / typecheck / lint pass, bash -n clean.

* Address review round 6: reap the kill's own orphan, tolerate the reaper

* Address review: hang-bound the reaper, unblock the kill path, pin the unpinned arms

- Wrap every docker call in the stale-container reap with timeout 30: an
  alive-but-wedged daemon blocks docker ps indefinitely, and the existing
  || guards only catch nonzero exits, not hangs (R3-1).
- Make the kill-path container removal async in run-agent.mjs: the
  spawnSync blocked the event loop between SIGTERM and the 10s SIGKILL
  backstop for up to its 30s timeout — in exactly the wedged-daemon
  scenario the watchdog exists for. The main flow awaits the removal so
  the leak warning stays deterministic (R3-6).
- Split the pre-existing gate clause for an empty CMP_R: a transient
  compare-API failure is "never measured", not "measured not-behind", and
  must not assert the branch's own code is at fault (R3-7).
- Swap the timeout breaker's closing remedy to the sandbox investigation
  when every counted timeout was idle, mirroring the round-level split
  (R3-11).
- Tests: pin the budget kill path separately from the idle kill path
  (R3-3), parameterize the idle-window parse guard over -1/0/NaN (R3-5),
  add a stderr-only liveness case (R3-12), pin the strict-subset A/B arm
  via a baseline-superset fixture knob (R3-15), and pin the breaker's
  current-round idle increment (R3-18).

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 11:00:11 +00:00
Shaojin Wen
e60d182241
perf(ci): tighten the automatic review kill switch for micro diffs (#8774)
* perf(ci): tighten the automatic review kill switch for micro diffs

Below the review skill's sweep floor (25 changed lines) an automatic
review keeps --effort high and its inline comments — a medium downgrade
would drop exactly the inline findings a source fix deserves, and with
the file-scoped suites and the one-round reverse audit the pipeline
itself is what shrinks on a micro diff. What a micro run must not keep
is a multi-hour kill switch: the small-PR budget halves with the same
90-minute floor the docs-only downgrade uses, so a hung run dies at the
scale of its work.

Reuses the PR_SIZE_LINES the size-aware budget already fetched — no new
API call; an unknown size never tightens, an explicit --timeout wins
(the size block is skipped), and a docs-only run is already halved,
never twice. Gate tests drive the extracted step source with seeded
sizes: the 24/25 boundary, the floor, the docs-only interaction, and
the unknown-size fallback.

* fix(ci): share one halve-with-floor implementation and pin the guard states

Review rework. The docs-only branch and the micro tightening now call
one halve_budget_floor() — a / 2 → / 3 mutant survived every test
because both micro inputs land on the floor under any divisor ≥ 2, and
two verbatim copies let a one-sided edit diverge the branches while the
comments claimed they matched; the floor cases now execute the shared
function and a structural pin asserts one definition, two calls, one
occurrence of the arithmetic. The threshold comment states the unit
honestly (total churn rides the skill's source-weighted sweep floor in
the direction that cannot over-tighten). Two surviving guard mutants
get pins: a manually requested review with a populated size is never
tightened (the caller owns its timeout), and a failed docs
classification still tightens a micro automatic run (the guard keys on
!= "true", not = "false").

* fix(ci): state the micro-tightening's true justification, not a false invariant

The comment claimed "total churn < 25 implies the skill's source-weighted
measure < 25", which is backwards: srcDiffLines counts raw unified-diff
lines (file/hunk headers, context), so it is LARGER than churn — a
scattered micro diff (churn 10 across 5 files) computes srcDiffLines ~65
and keeps sweep on, un-shrunk, while the gate still halves its budget.

The threshold is now stated as what it is: an independent "small PR"
churn bound, deliberately not SWEEP_FLOOR (the two measures differ, so a
micro diff may still run the sweep and the full reverse audit). The
tightening is justified by "churn < 25 bounds the reviewed territory and
90 minutes is ample for it even on the full pipeline" — measured, a
23-line PR runs high end to end in ~30 min — not by the pipeline
shrinking. With the SWEEP_FLOOR coupling claim removed, there is no
cross-file coupling left to drift, so no equality pin is owed.

* fix(ci): reword the micro test comment to the independent churn bound

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): reword the micro gate comment to the 180-minute budget it actually halves

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 04:01:36 +00:00
Shaojin Wen
3c3084e78a
fix(ci): route workflow label mutations through REST (#8761)
* fix(ci): route workflow label mutations through REST

`gh pr edit` cannot mutate anything on this repository: its GraphQL
lookup requests repository.pullRequest.projectCards, and with Projects
(classic) attached GitHub returns the deprecation as an error, so the
command exits 1 before applying the change. Reproduced from a live
clone against PR #8755 — the error names the field outright.

Three workflows carried label mutations through it:

- pr-self-report-label.yml: every add/remove arm failed — 43 straight
  run failures from 2026-08-04 on; the green runs were all the
  nothing-to-do arm. Self-reported PRs (like #8755, whose author also
  opened #8750) never got the label.
- qwen-autofix.yml: the `@qwen-code /takeover` and `/takeover stop`
  COMMAND paths never toggled the label — only the UI label events
  worked, so the command was dead weight wearing an ack.
- repo-hygiene.yml: the add was `|| echo`-guarded, so it never failed
  the run — it just never labeled anything, while the fallback message
  blamed a label that exists.

All five sites now use the REST issues/labels endpoints, which never
touch that query. Two traps handled on the way:

- Every label involved contains a slash, and in the DELETE the label is
  a PATH SEGMENT — unencoded it 404s. Encoded via jq @uri, and the
  tests assert the literal %2F because a real jq runs in the replay.
- The REST add auto-creates a missing label, which repo-hygiene
  explicitly promises never to do — that site gets an existence probe
  first, and its misdiagnosing fallback message is corrected.

Verified live on #8755 before editing anything: the exact gh pr edit
call fails with the projectCards error; REST POST applies the label
(backfilling the one it was owed), DELETE with %2F removes it.

Tests: the stub-driven replays for both the self-report step and the
takeover toggle now pin the full REST method + path (encoding
included), and a repo-wide guard bans `gh pr edit --add-label/
--remove-label` in every workflow so the class cannot return.
Mutation-tested, 6 of 6 caught: each of the five sites reverted to
gh pr edit, and the DELETE stripped of its encoding.

* fix(ci): harden REST label mutation steps per review (#8761)

* fix(ci): pin REST label failure policies per review (#8761)

Review round for the REST migration:

- The DELETE arms tolerated EVERY failure (`|| true`), masking
  403/5xx/network errors behind a green run and a false "removed"
  log. They now tolerate only the documented 404 race — any other
  failure emits a :⚠️: while keeping the step green
  (pr-self-report-label) and the release ack alive (qwen-autofix).
- Neither replay harness could make a `gh api` call fail, so both
  failure policies were unpinned. They gain failure knobs (knob
  value on stderr like a real gh HTTP error) and now pin: 404 race
  silent, other DELETE failures warned, POST loud. The toggle
  replay also moves to -eo pipefail like the runner's bash default,
  reproducing the step's real failure semantics.
- The jq stub enforced only the --arg shape; it now also enforces
  the `$l|@uri` program, so a filter mutation fails the suite
  instead of riding the stub's unconditional percent-encoding.
- The gh-pr-edit guard misfired on comments and miscounted lines
  after joining continuations: comments are stripped before
  matching, and offenders are reported at the physical line where
  the (possibly wrapped) command starts.

Mutation-tested with 8 probes, all caught: blanket || true on
either DELETE, || true on either POST, dropped |@uri, a comment
quoting the ban (stays green), an executable and a wrapped
violation (both red, correct line).

* Address review round 3: close the guard evasions, convert the release path

Four round-3 findings, each reproduced before fixing, plus the release
path the round-1 scope note deferred.

- The ban guard now scans what bash executes, not the YAML surface: the
  decoded run: values of every parsed workflow, whole-line comments
  stripped, continuations joined the way bash joins them (backslash-
  newline removed, nothing inserted), matched whitespace-tolerantly. All
  three reproduced evasions — a # inside a quoted string eating the
  trailing backslash, wraps inside the command prefix or a flag token,
  and folded scalars — are fixture-pinned. Offenders report as
  file » job » step; line numbers stopped meaning anything after joins.
- classify-release-notes.mjs mutates labels through REST now, and the
  guard grew an argv-form scan over .github/scripts/*.mjs that flags the
  old file (negative-controlled) — the release path was the last
  gh pr edit label site, failing silently behind continue-on-error.
- JQ_STUB enforces the full invocation: -rn (with -r alone real jq
  evaluates zero inputs and prints nothing), the binding name l (real jq
  exits 3 on $l undefined), and the program. Either reproduced mutation
  previously expanded the substitution empty, sent the DELETE to
  …/labels/ with no name segment, and the 404 tolerance swallowed it.
- The takeover engage POST gets the idempotent create its siblings
  carry, pinned to the label's real color (1D76DB): the REST add would
  re-create a deleted label silently with a random color.
- runToggle captures writes on throw, and the engage-failure assertion
  now pins the ORDER its comment claims: a failing apply must leave no
  "takeover-ack engaged" in the captured writes — the bare toThrow
  passed even with the ack moved above the POST (reproduced).
- The two REMOVE_ERR DELETE idioms are drift-pinned byte-identical
  modulo the label variable, the honest substitute for sharing shell
  across workflow files.

Mutation-tested, 6 of 6 caught: the evadable regex restored, -rn and
the binding name mutated in the workflow, the create dropped, the ack
posted before the POST, and the old .mjs flagged by the new scan.

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 15:05:15 +00:00
Shaojin Wen
afcc937ec5
fix(ci): render the queued-acknowledgement comment (#8726)
* fix(ci): render the queued-acknowledgement comment

The ack comment posted on every PR that requests a review was built as

  <!-- qwen-review-ack -->_Qwen Code review request accepted. …[workflow
  run](URL)._

with the prose glued straight onto the marker. A line opening with `<!--`
starts an HTML block, and that block runs to the line containing the
closing delimiter INCLUSIVE — the rest of that line is still inside it and
never parsed as Markdown. So the comment shipped as raw source: no
emphasis, and the link to the workflow run was dead text.

That link is the only pointer a PR author gets to their review run —
`issue_comment` runs are not attached to a commit, so they never appear in
the PR's checks list. Losing it leaves no way to reach the run from the PR.

Measured through GitHub's own renderer (POST /markdown, mode=gfm) on the
exact bodies:

  marker + text          -> 0 <a>, 0 <em>
  marker + \n   + text   -> 1 <a>, 1 <em>
  marker + \n\n + text   -> 1 <a>, 1 <em>

Use the blank-line form, matching how autofix-status already builds its
body. The marker text is unchanged, so the `contains(...)` upsert lookup
still finds prior acks and updates them in place.

Pinned by a test that scans every marker in the workflow and rejects one
with prose glued to it, skipping comment lines. It fails against main,
naming the offending line.

* fix(ci): harden the marker guard per review round 2

Pin the workflow-run URL weaving into the ack printf, bound the marker
scan to the marker's physical line, anchor the newline exemption to the
literal the marker opens, widen it to double-quoted printf formats, and
flag unquoted command-substitution concatenation. Declare the remaining
coverage gaps in the test instead of papering over them.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): widen marker-guard regex and declare known gaps per review

* fix(ci): pin ack link shape and dedupe workflow scan per review

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 06:04:26 +00:00
Shaojin Wen
9381827c88
fix(ci): enter Critical-only mode after five change rounds (#8751)
The suggestion cutoff sat at ten change-producing rounds, but the strict
round cap discards a plain PR at round ten before that threshold can
engage — so in practice it only ever bound takeover PRs, which spent ten
rounds growing their diff on lower-severity feedback before the brake
applied. Lower it to five so the loop stops implementing suggestions
while the diff is still reviewable.

The autofix skill already documented the five-round boundary; the
workflow now matches it.
2026-08-08 16:09:32 +00:00
Shaojin Wen
d91c66119b
fix(ci): match /review commands followed by a newline (#8723)
A comment of `@qwen-code /review` plus a newline and a body has never
triggered anything. The shape match tried to accept it with

  startsWith(body, format('@qwen-code /review{0}', '\n'))

but GitHub expression string literals are NOT escape-processed: that
'\n' is a literal backslash + n, so the branch matched nothing. The
command was silently ignored — no run, no feedback, in a path whose
whole job is to be the manual escape hatch.

Measured on a live runner rather than assumed:

  startsWith(<LF body>,   format(…, '\n'))              => false
  startsWith(<LF body>,   format(…, fromJSON('"\n"')))  => true
  startsWith(<CRLF body>, format(…, fromJSON('"\n"')))  => false
  startsWith(<CRLF body>, format(…, fromJSON('"\r"')))  => true

fromJSON parses JSON, which IS escape-processed, so it yields a real
newline. Both endings are needed: the REST API sends LF, the web UI
sends CRLF, and an LF pattern does not match a CRLF body. Applied to all
7 shape matches (6 /review, 1 /resolve).

The shell half had the matching gap: the command line is taken as
everything before the first LF, which on CRLF keeps a trailing CR. IFS
has no CR, so word splitting produced tokens like `--timeout=300<CR>`
that failed the numeric check with no visible cause. Strip it.

Three tests pin this: no shape match may use a non-escape-processed
literal, every shape match must carry both endings, and the CR strip
must follow the first-line split. All three fail against the workflow on
main. Mutation-tested: dropping the CR branches or the CR strip each
fails exactly its own test.

Found while re-triggering the PRs stranded by the #8648 outage — 19
multi-line trigger comments were accepted by `authorize`, then silently
dropped by this branch.

Co-authored-by: verify <verify@local>
2026-08-08 12:22:39 +00:00
Shaojin Wen
ee98f7420b
perf(ci): make the autofix fleet caps operator-tunable and raise them (#8731)
The three caps that bound the autofix review loop were compiled-in
literals sized for a much smaller takeover pool, so growing the pool
meant editing the workflow, opening a PR and waiting for review every
time. The takeover pool is not static — it grew to 37 PRs by
2026-08-08, and it keeps growing.

Back all three with repository variables, keeping the literals as
fallbacks, so the loop is resized in Settings → Variables with no code
change:

  QWEN_AUTOFIX_MAX_PARALLEL              (fallback 20, was 5)
  QWEN_AUTOFIX_MAX_TARGETS_PER_SCAN      (fallback 30, was 10)
  QWEN_AUTOFIX_MAX_CANDIDATE_INSPECTIONS (fallback 60, unchanged)

Verified on a live runner that `max-parallel` accepts the expression and
schedules by it — a 6-leg matrix resolving to 3 started exactly 3 legs
and began the 4th only after a slot freed. Not assumed: an invalid
expression here makes the whole file invalid, which this repository just
paid 12.9 hours of dead review automation for.

The raised fallbacks are sized against measurements, not guesses. At 5
slots the fleet served ~14% of the takeover pool at once, reproducing at
a larger scale the 81-minute tail measured back at 3. The ecs-qwen fleet
is 84 runners, so 20 concurrent legs take under a quarter of it, and the
legs sampled that day finished in 3-28 minutes. Worst case rises to 100
runner-hours across the fleet (20 slots x the 300-minute job cap), and
per-PR head-write concurrency groups are per-PR, so this adds no push
contention.

MAX_TARGETS_PER_SCAN has to stay above max-parallel or the scan cannot
emit enough legs to fill the matrix. That relation is pinned for the
fallbacks by an existing test and stated at both definitions for the
variables, where it becomes an operator invariant.

Mutation-tested, 4 of 4 caught: fallback equal to the budget, fallback
above it, and dropping either variable back to a literal.

Co-authored-by: verify <verify@local>
2026-08-08 08:53:07 +00:00
Shaojin Wen
6cae50c7ea
fix(ci): keep the review workflow under the expression-length limit (#8720)
The review workflow has been invalid since #8648 merged, so every event
it declares has been dead for ~12 hours:

  Invalid workflow file: .github/workflows/qwen-code-pr-review.yml#L1
  (Line: 751, Col: 14): Exceeded max expression length 21000

A `run:` body containing `${{ }}` is evaluated as ONE expression
template, and GitHub caps a single expression at 21000 characters.
"Run review" went 17705 -> 22282 chars in #8648 (17:00:32 on 2026-08-07);
the first startup failure is stamped 17:00:50. #8683 took it to 24042.

An over-limit expression does not fail a job — it invalidates the whole
file, so no run is created at all. Across the 400 runs since that merge
there is not one success, not one `pull_request_target` and not one
`issue_comment`: both automatic review and `@qwen-code /review` were
unreachable, while CI stayed green throughout because nothing covered it.

Pass the three context values the script reads through the step's env,
leaving the body free of `${{ }}`. The runner then never templates it and
its length stops mattering. No behaviour changes: each substitution is a
rename of the same value.

Pin it with a test that walks every workflow and fails any templated run
block over the limit, plus one that keeps this body untemplated — it is
past 21000 on its own, so a single `${{ }}` added back takes the whole
workflow down again. Both fail against the file currently on main,
naming it: `qwen-code-pr-review.yml > review-pr > Run review: 24042 chars`.

Mutation-tested, 4 of 4 caught: restoring a `${{ }}` in the body (3
tests), dropping either env binding, and hardcoding the value the env
used to carry.

Co-authored-by: verify <verify@local>
2026-08-08 05:53:35 +00:00
Shaojin Wen
ee2e5be666
fix(review): stop the agent transcript from executing workflow commands (#8683)
* fix(review): stop the agent transcript from executing workflow commands

The review agent streams its entire transcript to stdout, and the runner
scans every line for workflow commands. A tool result that quotes a file
containing one therefore gets EXECUTED.

Observed on run 31167034020 (PR #8681). That PR changes an
`actions/setup-node` input, so the agent read the action's own main.ts,
which legitimately contains:

    core.info(`##[add-matcher]${path.join(matchersPath, 'tsc.json')}`);

The runner took the rest of the JSON line as a matcher path:

    ##[error]Unable to process command '{"type":"user","uuid":...' successfully.
    ##[error]The path '...' is too long, or a component of the specified path is too long.

Three of those, and the step failed after 1h37m — a full review discarded
for quoting a file. Nothing about that PR is unusual: any review whose
transcript quotes `##[...]` or `::...::` breaks the same way, including
every review of this repository's own workflows.

Wrap the agent invocation in `::stop-commands::`, with a token randomised
per attempt so no output the agent produces can guess it and re-enable
parsing early. Parsing resumes immediately after PIPESTATUS is captured:
`echo` clobbers PIPESTATUS, so resuming any earlier would read the echo's
status instead of the agent's and report every timeout or crash as a
clean run. Resuming is on the errexit-disabled straight line, so it is
reached on success, crash and timeout alike — leaving it off would
silence the job's own ::error:: and the fallback comment's diagnostics
for the rest of the run.

Tested by driving the real extracted retry loop with a stub agent that
emits `##[add-matcher]`, asserting the bracket contains it, that the
token is random rather than fixed, and that parsing resumes on success,
hard exit and timeout. Mutation-checked: removing the guard, never
resuming, resuming before the status capture, and using a fixed token
each fail.

* fix(review): resume workflow commands on a line the runner can see

Round-2 review follow-ups on the stop-commands guard.

The resume was `echo`d, so a `--kill-after` SIGKILL that cut the agent
off mid-line appended it to that fragment. The runner matches `::cmd::`
at a line start only, so parsing stayed off for the rest of the job —
losing the retry `:⚠️:` and every later diagnostic on the one
path the guard exists to survive. Emit it with a leading newline.

The ordering assertions had no teeth: `indexOf` returns -1 when a line
is deleted or reworded, and -1 satisfies `toBeLessThan`. Deleting the
stop line left the suite green. Every anchor is now asserted present.

Cover the outcomes no scenario reached: an agent that streams and then
dies (the stub `timeout` exited before ever running it), a failing log
write (the only early return left unpinned), and a retry, which pins the
bracket as per-attempt with a token the previous attempt cannot reuse.

Mutation-tested, 7 of 7 caught: reverting the printf, moving the resume
past the tee check or before the PIPESTATUS capture, hoisting the
bracket out of the function, fixing the token, and deleting either end.

---------

Co-authored-by: verify <verify@local>
2026-08-08 04:13:27 +00:00
Shaojin Wen
7b7ff19bc3
fix(tests): avoid blocking integration test cleanup (#8688)
* fix(tests): avoid blocking integration test cleanup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tests): avoid failing fast on telemetry waits for live CLIs (#8688)

* fix(tests): drop dead telemetry-ready return and gate rig tests (#8688)

* fix(tests): pin the gated rig test in the no-AK guard (#8688)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-08 02:17:17 +00:00
Shaojin Wen
7942197666
fix(tests): apply integration worker limits to forks (#8689)
* fix(tests): apply integration worker limits to forks

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tests): keep no-AK integration gate at two fork workers (#8689)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <25325202+qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-08 00:53:33 +00:00
Shaojin Wen
f4802031d0
perf(ci): run docs-only automatic reviews at medium effort (#8648)
* perf(ci): run docs-only automatic reviews at medium effort

A 1-line docs PR costs the same 57-180 minute high-effort review as a code
change, and on a diff with zero source lines the passes medium drops - the
adversarial personas and the reverse audit - have no failure mode to hunt.
Counterfactual analysis over six dissected CI runs showed the one case
where those passes caught a real Critical was a source PR, which this gate
never touches: classification reuses the Test workflow's conservative
classify-profile.mjs (docs/**.md(x) + root prose only; markdown under any
src/ tree stays full, matching the review skill's own source rule), and
any fetch or classifier failure falls back to the full review.

Only the automatic pull_request_target review downgrades; every explicit
request (workflow_dispatch, @qwen-code /review) keeps full high effort.
Because an effective --comment forces high and medium never posts, the
downgrade drops --comment and a new step relays the review CLI's verbatim
"Review complete:" line - its machine-readable completion contract - as a
single PR comment, with a pointer for requesting the full review. The
docs-only budget is the size-aware timeout halved with a 90-minute floor.

* perf(ci): address review feedback on the docs-only medium gate

All nine review suggestions, each verified before fixing:

- review_requested is an explicit ask: the AUTO_REVIEW flag now excludes
  that action (authorize write-permission-checks its requester), so a
  maintainer requesting the bot's review gets the full high-effort run.
- The fetch-and-classify wrapper is extracted to
  .github/scripts/ci/classify-pr-profile.sh and consumed by both ci.yml
  and the review gate, so the classifier's input contract lives in one
  place; distinct exit codes preserve each caller's fallback messages.
- Neither completion-line fallback mints the reserved "Review complete: "
  prefix anymore, and the relayed line passes a strict not-posted
  disposition allowlist - on this never-posts path any posted-form
  disposition is false by definition (the measured phantom APPROVE
  posted), so it falls back to a neutral non-scrapable form.
- The relay upserts by its marker (mirroring the queued-acknowledgement
  step) instead of stacking a comment per push, retries the POST/PATCH
  three times, and never fails the job - a failed relay after a
  successful review must not trip the failure fallback into announcing
  a review failure that never happened.
- The Chinese relay copy no longer parses as "发行" and renders
  high-effort as 高强度 rather than 高档.
- The qwen-review docs-only-medium marker is registered in all six
  BOT_COMMENT_FILTER sites in qwen-autofix.yml, so clean docs-only
  relays cannot select PRs into autofix rounds as actionable feedback.
- The gate's behavioral invariants are pinned in
  scripts/tests/qwen-pr-review-workflow.test.js by executing the
  extracted bash: prompt-branch order (--effort medium instead of
  --comment), the halve-with-90-minute-floor arithmetic, the
  completion-line allowlist including the phantom shapes, AUTO_REVIEW
  exclusivity, the six-site marker registration, and the shared-wrapper
  routing in both workflows.

* perf(ci): harden the docs-only gate against round-2 review findings

Thirteen findings across two review passes; every fix is executed by a
test rather than asserted as text where the finding was behavioral.

- The relay marker exclusion in qwen-autofix.yml is author-scoped at all
  six filter sites: only the relay bot's own marker comment is filtered,
  so a human quoting the marker stays actionable feedback.
- classify-pr-profile.sh guards the 3,000-file listing cap (any mismatch
  against the PR's declared changed_files classifies full), uses
  mktemp+trap instead of a fixed path on the shared persistent pool, and
  ships its own node:test suite (renamed source→docs pins the projection
  contract; exit codes 2/3 pinned) registered in HELPER_TESTS.
- classify-profile.mjs restricts reserved root prose basenames to inert
  extensions - README.js / SECURITY.ts / LICENSE.sh classify full.
- The completion-line allowlist binds to pr-<number> and to the only
  verdict a medium run can produce (Comment, not posted) - a stale line
  for another PR or an Approve-shaped injection falls back to neutral.
- A dedicated review_completed output gates the relay: the state/head
  guards exit 0 without running the review, and outcome==success alone
  would have announced a review that never ran.
- The relay upsert filters by the authenticated bot login, re-resolves
  the comment id on every attempt, and falls back to POST when the PATCH
  target is gone - a participant posting the marker can no longer capture
  the upsert, a transient listing failure no longer mints duplicates.
- The gate and relay are now executed under stubbed executables in
  qwen-pr-review-workflow.test.js (docs_only/full/failure/explicit
  scenarios; POST/PATCH/never-fail branches), the AUTO_REVIEW pin covers
  both guard halves, and the marker contract is pinned producer-side and
  filter-side.

* perf(ci): fix the medium Request-changes swallow and the stale docs badge

Round-3 review findings (2 Critical, 8 test-gap Suggestions), each fix
executed by a test where the finding was behavioral:

- The completion-line allowlist accepts `Request changes, not posted` -
  compose-review caps only Approve at medium, so a docs-only run that
  verifies a Critical legitimately emits Request changes, and the old
  Comment-only allowlist swallowed exactly the blocker-finding outcome
  into the neutral fallback. Target binding to pr-<number> is unchanged
  and now pinned by a test, as is the last-line selection over a stale or
  injected earlier completion line.
- A stale docs-only badge can no longer outlive its revision: the full
  automatic review path now supersedes the bot-authored marker comment
  (strikethrough + superseded note) via a new --update-only mode that
  never mints a badge where none existed.
- The marker+author upsert protocol is extracted to
  .github/scripts/upsert-bot-comment.sh - one implementation shared by
  the relay and the supersede step (the per-step copies had already
  drifted), with its own node:test suite covering the author scope, the
  per-attempt re-resolution (deleted-mid-retry falls back to POST), and
  the --update-only no-op; registered in HELPER_TESTS.
- The classify-pr-profile gh stub now applies the wrapper's own --jq
  argument with real jq over API-shaped fixtures, so the projection
  contract is genuinely under test (negative control: dropping `status`
  turns the renamed-source scenario red).
- New pins: review_completed wiring end to end (run-step emit + both
  consumers' if clauses), the auto_review output->env wiring at both
  links, and both AUTO_REVIEW guard halves.

* perf(ci): never let a failed lookup mint or keep a stale docs badge

Round-4 review findings (1 Critical, 7 Suggestions):

- The upsert script no longer conflates failed lookups with empty
  results: the authenticated login, the listing, and the jq extraction
  are all resolved inside the retry loop as one prerequisite chain, an
  attempt whose prerequisites failed retries instead of falling through
  to POST (the shape that minted a permanent duplicate badge off one
  transient 5xx), and --update-only exits 1 on a failed lookup so the
  supersede warning fires instead of a false no-op success. New tests
  pin the failed-listing-then-PATCH path, the persistent identity
  failure, the update-only failure exit, and the update-only PATCH.
- Supersede now covers every path that owes the correction: a FAILED
  full review and an EXPLICIT requested review (the badge's own CTA)
  both retire the badge, gated only on docs_only_medium == 'false' -
  empty on runs that failed before classifying, so a badge is never
  superseded on ignorance. The body is cause-neutral: it asserts only
  that the badge described an earlier revision.
- The marker literal is defined once per step (MARKER variable, the
  qwen-triage convention) and shared between body and lookup argument;
  a pin requires the definition and --update-only on the supersede
  invocation.
- New behavioral pins: the Approve verdict stays rejected by the
  allowlist, github_ci_only never downgrades (CI helpers are
  executable), and review_completed's emit position is asserted AFTER
  the closed-PR and stale-head guards (the hoist mutant survived
  position-independent contains checks).

* perf(ci): make docs_only_medium three-valued and pin the untested guards

Round-5 review findings (1 Critical, 6 Suggestions):

- docs_only_medium no longer conflates "determined not docs-only" with
  "never determined": the output is three-valued ('' when the
  classification failed or never ran), so a transient classifier failure
  or a dispatch dry-run can no longer retire a still-accurate badge. The
  supersede condition names its two licensed paths explicitly - a
  POSITIVE not-docs-only determination (without requiring review
  success), or an explicit comment-mode review that completed (the
  badge's CTA; report-mode dry runs retire nothing).
- The count-mismatch fallback in classify-pr-profile.sh logs to stderr,
  so a systematic divergence is distinguishable from every PR genuinely
  classifying full.
- Six probed surviving mutants now each turn a test red: the supersede
  body is executed (existing-badge PATCH and the never-fail guard),
  ci.yml's rc-handling fragment is executed (exit 0/2/3 with the
  full fallback), the changed_files fetch failure exits 2, duplicate
  badges PATCH the last (newest) comment, and the relay's POSTed body
  must carry the marker that keys both the upsert and the supersede.

* perf(ci): bind the docs badge to the reviewed head and retire it on failure

MDX pages are executable (imported components, expressions), so the
classifier no longer treats them as inert docs-only changes. The relay
and supersede writes re-read the live PR state/head immediately before
the mutation and skip unless the PR is still open at the reviewed SHA,
the badge body names that SHA, a failed docs-only review now retires the
singleton badge instead of leaving the previous revision's outcome
visible, and the retired wording is cause-neutral (an explicit review
can complete on the very head the badge describes).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-07 17:00:32 +00:00
Shaojin Wen
20b9504276
feat(autofix): bridge fork-PR reviews into the credentialed review lane (#8676)
* feat(autofix): bridge fork-PR reviews into the credentialed review lane

A workflow run tied to a pull request whose head lives in a fork is
created with `Secret source: None`, so `secrets.CI_DEV_BOT_PAT` is empty
and the autofix review lane cannot authenticate. Those reviews therefore
wait for the scheduled scan, which GitHub throttles hard — the `*/10`
cron lands every 40-70 minutes on this repo.

Reading the PR with `github.token` instead would not help: review-address
still needs the PAT to push and comment, and it is empty for the whole
run. The credential has to come from a different run.

This is the standard `workflow_run` bridge for that.

- `qwen-autofix-fork-signal.yml` runs on `pull_request_review`, where the
  fork PR's event can reach it, and records only which PR was reviewed.
  It is deliberately powerless: `permissions: {}`, no checkout, no
  repository code, and the hosted pool rather than the persistent
  self-hosted one.
- `qwen-autofix-fork-bridge.yml` runs on `workflow_run`, which GitHub
  creates on the default branch with the repository's own token, and
  dispatches `qwen-autofix.yml` for that PR using `actions: write` — the
  same lever Fleet Shepherd already pulls. It holds no PAT.

The dispatched run re-derives admission from live API state, so the PR
number says when to look, never who may be touched. It is bound to
`workflow_run.head_sha` regardless: the signal run carries the reviewed
PR's head SHA, and only that PR has that head.

Measured, not assumed:

- `workflow_run.pull_requests` is empty for fork PRs, and
  `/commits/{sha}/pulls` does not resolve a fork head either (it does
  resolve an in-repo one), so the artifact is the only way to learn the
  number.
- `pull_request_review` runs the workflow file from the BASE branch: run
  31152873061's PR branch predates a main-only change to
  `qwen-autofix.yml` and it still executed the new file. A fork cannot
  edit what the signal writes.
- That run's `head_sha` equals PR #8436's `headRefOid` exactly, which is
  what makes the binding possible.

The bridge's validate-and-dispatch block is replayed under bash against a
stub API: forgery, an unreadable head, both heads empty, non-numeric and
path-shaped artifacts, a closed or retargeted PR, download and read
failures, and dispatch retry then exhaustion.

* fix(autofix): align fork bridge with route admission and coalesce bridged dispatches

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): harden the fork-review bridge per review feedback (#8676)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-07 16:11:48 +00:00
Shaojin Wen
188802719b
fix(ci): give review runs upstream-timeout headroom (#8673)
* fix(ci): give review runs upstream-timeout headroom

PR 8507's automatic review aborted twice on 2026-08-07 with "Request
timeout after 483s" - a classification message printing elapsed time, not
a configured threshold. Two distinct guards fired under a degraded
upstream: the 17-agent fan-out generation on a ~1.27M-token context
stalled chunk delivery past the 240s stream-idle default (the
pipeline-level retry doubled the loss to ~16 minutes), and on the fresh
retry an early small turn burned three ~120s connect/TTFB timeouts
through the SDK's internal retries.

Three knobs, applied to CI review runs only:
- model.generationConfig.timeout 600000 via the per-run QWEN_HOME
  settings.json (no env knob exists for the SDK request timeout);
- QWEN_STREAM_IDLE_TIMEOUT_MS 600000, tolerating a long thinking phase
  between chunks;
- QWEN_STREAM_MAX_LIFETIME_MS 1800000, keeping the drip-feed hard bound
  strictly above the idle window.

The outer GNU timeout and the review deadline still bound the job; the
knobs trade hang-detection latency for survival on slow-but-alive
upstreams. A test pins all three and the lifetime>idle ordering.

* fix(ci): deliver review timeout headroom via step env (#8673)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): pin stream-guard env values and bound stream-retry trade-off (#8673)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-07 16:04:37 +00:00
Shaojin Wen
6668c876a6
perf(autofix): stop restoring a 2.65 GB npm cache to protect a 29 s install (#8681)
* perf(autofix): stop restoring a 2.65 GB npm cache to protect a 29 s install

`actions/setup-node` is configured with `cache: 'npm'` in all three heavy
autofix jobs. Measured on one review-address leg, that step took 339s:
Node itself was free (`Found in cache @ .../_tool/node/22.23.2/x64`) and
2,654,052,865 bytes arrived at ~10 MB/s. The `npm ci` it protects ran in
29s in the very next step.

The bill is per job, not per run: build-cli pays it once (280s measured),
issue-autofix once, and EVERY review-address leg once — up to ten legs a
scan, five at a time. Observed leg costs ranged from 446s to 1207s.

The persistent pool keeps ~/.npm across jobs, so the download buys
nothing there. The hosted fallback is ephemeral and still wants it, so
the cache is chosen from `runner.environment` rather than from a copy of
the runs-on expression, which differs per job and would drift.

Also narrows an existing negative pin. `expect(workflow).not.toContain(
"runner.environment == 'self-hosted'")` was added to keep the reverted
dedicated-runner design out, whose artefact was a `command -v node` step
gated on exactly that expression (removed in #6261). That step is pinned
out by name on the following line, so the substring form only forbade the
`runner` context by accident — the same test requires
`RUNNER_ENVIRONMENT: '${{ runner.environment }}'` a few lines below. It
now matches the shape that was actually reverted: a step whose entire
`if:` is that expression. Mutation-checked: re-adding that step still
fails the test.

* fix(autofix): actually disable the npm cache restore on the persistent pool

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-07 15:58:24 +00:00
Shaojin Wen
18b925103a
fix(triage): finalize the status comment on cancellation too (#8436)
* fix(triage): finalize the status comment on cancellation too

The 'Finalize triage status comment' step ran under
success() || failure(), so a job-timeout (timeout-minutes: 30, reachable
on large PRs) or a manual cancel skipped it and the early '🔄 Qwen
Triage is running' lifecycle comment kept claiming the run was in
progress forever — the verify lane has publish-verify to catch its
cancelled case, but the triage lane has no other publisher.

Run the step under always() (still gated on a resolved number), branch
on job.status to say the run was cancelled rather than 'ended early',
and point the reader at re-running. The step body is already
best-effort throughout, so widening the condition cannot fail the job.

* test(triage): pin the cancelled finalize condition in the shape test

* fix(triage): finalize status comment only on this run's own comment

The cancelled finalize PATCHes whatever lifecycle comment the marker
lookup finds. When a run is cancelled before its own status claim
posts, the lookup lands on a previous run's comment — including the
terminal verdict wording written by qwen-triage-finalize.yml, which
shares the marker — and rewrites it to "was cancelled".

Only PATCH a comment this run owns: the claim embeds the run link, so
ownership is observable as the found body containing $RUN_URL. A found
but foreign comment is left untouched; a missing one is still POSTed.

Also pin what the shape test could not: the cancelled branch's Chinese
wording, and an executed-composer test that runs the step's script
against a stubbed gh to assert the posted body for each
(TRIAGE_OUTCOME, JOB_STATUS) combination plus the ownership cases —
swapping the success and cancelled bodies no longer survives the suite.

* test(triage): pin the last-comment selector in the finalize ownership lookup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(triage): harden finalize ownership pins and document lane race

Round-4 review feedback:
- Negatively assert the ZH terminal wordings too, so a composer body
  mixing two terminal states cannot pass the combos loop.
- Cross-pin the claim and finalize steps' RUN_URL env definitions, which
  the ownership contract couples; one-sided drift would otherwise make
  finalize classify its own claim as foreign.
- Document the accepted lane race with qwen-triage-finalize.yml's
  update_status writer at the ownership rule.

* fix(triage): bound finalize ownership match to the exact run link

* fix(triage): never strand a running claim in the finalize step

A foreign lifecycle comment made finalize skip every write, so a claim
whose POST had failed — or a legacy marker body that carries no run
URL — stayed at "running" forever. Post a fresh terminal comment for
unowned markers instead; the rare lane race costs one extra comment
but the thread always ends in a terminal state.

The marker lookup also picked the newest comment before asking who
owns it, letting a newer foreign comment mask this run's own claim.
Select the last comment this run owns so the classifier is
order-independent, and treat an unreadable comment list as its own
state (warn and skip) instead of conflating it with "no comment" and
posting a duplicate.

The success wording is now gated on the job not failing: 'Check triage
response' exits 1 on an empty summary while steps.triage.outcome stays
'success'. The behavioral test ran the step body without errexit even
though GitHub adds -eo pipefail, so such regressions stayed green;
align the harness with the real shell flags and add the missing
{success, failure} combo. Also reword the cancelled message, which
prescribed a re-run the run cannot know is necessary — cancellation is
usually cancel-in-progress supersession — and switch the two Chinese
strings to full-width punctuation per the file's convention.

* fix(triage): reclaim stranded running claims in the claim step (#8436)

The finalize foreign arm posts a second lifecycle marker, and once two
markers exist the claim step's newest-wins pick never touches an older
stranded "running" claim again — the base tree healed those by
overwriting its single slot. Select this run's own marker first (a
re-run keeps its run_id), then the oldest still-running marker, then
the newest.

Also reword the stale "no second post" comments to the ownership
semantics, soften the finalize invariant to its best-effort boundary,
and pin the previously untested behavior: one write-failure scenario
per finalize write arm, the empty-vs-foreign diagnostic, the legacy
marker path, and the last-own-marker rerun selection.

* fix(triage): keep the finalize status flip off live running claims (#8436)

qwen-triage-finalize.yml's update_status is the third writer of the
lifecycle marker and still picked the newest one unconditionally. When
it fires while a triage run is in flight, that newest marker is the
run's live "running" claim: PATCHing it erases the run URL the run's
finalize keys on, so the run classifies foreign and posts a second
comment, while the overwritten one — never the newest again and no
longer "running" for the claim step's reclaim — strands forever.
Prefer the newest marker that is not a live running claim, falling
back to the base newest-wins overwrite only when running markers are
all the thread has. Pin the selector with an executed test.

Also give the claim step's marker selection the one-line stderr
diagnostic the finalize step's arms already have, so a maintainer
investigating "the bot overwrote the wrong comment" can see which arm
fired and which id was chosen without refetching the comment list.

The two lifecycle harnesses each pasted their own gh-stub + runner and
had already diverged (only the finalize stub had the failure arms), so
a contract fix would have to land twice or silently degrade one test.
Extract one shared makeGhHarness, and use the arms it brings to pin
the claim step's previously untestable guards: one write-failure
scenario per `|| echo` arm, the `|| EXISTING_ID=''` list-failure
fallback, the legacy marker slot, the last-own-marker rerun selection,
and the claim body starting with the lifecycle marker every selector
is gated on. Also pin the finalize composer's `[view run]($RUN_URL)`
embedding, the finalize-side twin of the claim-side coupling already
pinned above it.

* fix(triage): key the status finalize on the claim's exported comment id (#8436)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(triage): pin claim --jq '.id' extraction and document finalize empty-id loss (#8436)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-07 08:10:54 +00:00
Shaojin Wen
63a99c2c11
fix(autofix): decline fork PR reviews instead of routing them to a scan that cannot authenticate (#8671)
GitHub hands no repository secrets to a workflow run tied to a pull
request whose head lives in a fork — the run header reads `Secret
source: None` — so `secrets.CI_DEV_BOT_PAT` arrives empty and every `gh`
call made with it is unauthenticated.

Route admitted those PRs anyway. The real-time fork branch spent two API
reads deciding it, then review-scan spent three more failing and exited 1
on `metadata_fetch_failed` — a reason whose blocked comment promises "a
later scheduled scan will retry", true for a 5xx and false for a
credential the run was never handed. Every review of a fork PR reddened
the workflow while changing nothing, and that noise buried the failures
that do need a human.

The branch's comment explained the admission by saying the event "runs
in BASE-repo context". That holds for the workflow FILE, which is read
from base, but not for the credentials.

So decline in route, exactly as the `pull_request` label branch already
does for the same reason. Nothing that functioned is lost: the admitted
path could never authenticate, and the scheduled scan admits fork
takeover PRs in repo context on its own. Real-time pickup for fork PRs
needs a credentialed lane, which a `workflow_run` bridge provides
separately.

Chosen over gating the PAT-backed jobs on a route output: that reached
the same outcome through a new output, two job conditions and a
concurrency expression that hand-mirrored them, while route already
declines this class of event one branch away and the two API reads still
got paid for.

Also add a hard guard on an empty PAT at the top of review-scan. No job
`if:` can read the `secrets` context, so a deleted or renamed secret —
or a lane nobody has modelled — is invisible until the step looks, and
it must stop there: unauthenticated reads answer as if the repository
held no PRs, which the scan would report as a healthy fleet of zero and
stay green while the loop is dead.

Tests: the fork-admission replay becomes a fork-decline replay over the
same shapes (including the two the removed branch existed to admit), and
pins that the decline costs no API call; the new guard is replayed under
bash with a present-PAT negative control.

Co-authored-by: verify <verify@local>
2026-08-07 07:58:26 +00:00
易良
5fdcdb28e2
fix(ci): avoid root-owned npm cache workspace files (#8669) 2026-08-07 07:08:34 +00:00
Shaojin Wen
7edc16ba11
feat(review): say so when the bundle is older than the review it runs (#8390)
* feat(review): say so when the bundle is older than the review it runs

Every `qwen review …` step runs the BUILT bundle, not the working tree. So
editing a review command, or switching to a branch that contains one, changes
nothing about the run until someone rebuilds -- and the failure is silent and
total: the run behaves like the last build, and every conclusion drawn from it
is a conclusion about that build.

Measured on 2026-08-02, dogfooding /review against #8368 from a checkout whose
bundle was fourteen hours old. Three things were invalidated at once and none
announced itself: `drive` and `mock-provider` had merged that morning and were
absent from the binary, so "the agent never reached for them" measured nothing;
and #8345's guard against scoring a mutant `survived` when its own collocated
test was red had merged too, so the run reproduced the bug it fixed and filed
three findings the current code holds as `inconclusive`. The round was
discarded and re-run after a rebuild.

`parse-args` is the first command of every review, which makes it the only
place a notice reaches a reader before they act on a result. It names the file
that is ahead, by how much, what actually runs from the bundle, and the command
to rebuild -- "rebuild" without evidence is advice nobody can check.

mtime, not git: the question is whether this bundle was built from this source,
and a git comparison answers a different one. A margin absorbs a checkout,
which writes everything at once in no guaranteed order. An installed package
has no sources beside it, finds nothing to compare, and stays silent -- a check
that cannot see the files must not accuse the build.

Also documents `findings --test-delta` for users: it can lower a severity, and
therefore change what the verdict is computed from, so it belongs beside
`--outcomes` rather than only in the skill.

* fix(review): watch the file every subcommand is registered in

`packages/cli/src/commands/review.ts` is where all 30-odd subcommands are
imported and registered, and it sits beside the directory rather than in it --
so a new command, or a changed dispatch, was exactly the change this check
could not see. A root may now be a single file, which is what that one is.

Confirmed end to end: with `review.ts` three hours ahead of a fresh bundle, the
warning names it.

Also two comments that did not match the code: symlinks of every kind are
skipped, not only directories (`isFile()` is false for a symlinked file too),
and the module now says what `QWEN_CODE_CLI` already covers -- talking to a
different program -- so it is clear this guards the other half, the right
program built before the change.

* fix(review): compare content, because a timestamp check cried wolf

The first version compared the bundle's mtime against the newest review
source, and it was wrong in the direction that matters most. `git checkout`
rewrites every file that differs between two commits, so returning to the
branch a bundle was built from re-stamps exactly those files and the check
calls a byte-for-byte correct bundle stale. Measured: with the sources
untouched and the bundle two minutes older, it warned. A line that fires when
nothing is wrong teaches its reader to skip the line, which would have made
this worse than absent.

The build now stamps a digest of the review sources it bundled into
`dist/review-sources.sha256`, and the check re-derives that digest from the
tree and compares. No margin to tune, no clock to trust, and no answer but the
true one. Verified end to end across all five cases: a clean tree is silent, a
source touched but unchanged is silent, and a real change under any of the
three roots -- the command directory, the `review.ts` that registers them, the
bundled skill -- warns.

The digest is now one rule stated twice, since the build script cannot import
the package it runs before building. `scripts/tests/review-source-digest.test.ts`
holds the two equal, on this repo and on a synthetic tree that exercises the
file-shaped root; a package test may not reach into `scripts/`, so it lives on
the side of the boundary that may.

Paths are folded relative to the repo root with separators normalised, and the
file list is sorted -- `readdir` order is a property of the filesystem, so
without it a bundle built in CI and a tree cloned locally would hash the same
source differently and every run would warn.

* fix(review): a diagnostic must not kill the run, and tests are not the bundle

Two Criticals and five suggestions from review, all verified before changing
anything.

`writeStderrLine` throws on EPIPE, so stderr piped to `head` would have killed
the review before it parsed a single argument -- a warning that destroys the
run it was warning about, and the opposite of this change's own invariant.
`writeStderrLineSafe` is the convention for diagnostics in this subsystem and
is what it calls now.

`reviewSourceRoots` builds paths with the platform `join`, and the test
asserted forward-slash literals, so all three elements would have failed on
the merge queue's Windows leg -- which the pull_request event never runs, so
the green CI here proved nothing about it.

Test files left the digest. esbuild follows imports from the CLI entry and no
test is reachable that way, so folding them in fired the warning for an edit
that cannot change a byte of the bundle -- the false positive this module
already rejected once. 112 files became 61, and a test-only edit is now
silent while a production one still warns.

The handler wiring is tested at last, against a real temp tree rather than a
mock of the reads under test: the derivation from `process.argv[1]`, the stamp
read, and the warning. All three mutations the review named -- dropping the
call, reading the stamp from the wrong directory, collapsing repoRoot to
distDir -- now redden it.

Also: the stamp's filename is pinned across the boundary it crosses (the build
wrote a literal while the check read `DIGEST_FILE`, so a one-sided rename
would have silenced the feature with every test green); the digest is computed
only when there is a stamp to compare it against, instead of hashing a hundred
files for a value the first guard discards; the `rebuildCommand` parameter no
caller ever set is gone; and the build script's comment no longer claims a
code-sharing relationship that does not exist.

* fix(review): fixtures are not in the bundle either

The same false positive, a third time and one directory over. Excluding tests
from the digest was right and incomplete: `review/__fixtures__` holds four
files — three responder modules and a captured comment — that a test loads at
runtime, from no import the bundler follows. Measured against `dist`: none of
the four appears in it, so editing one changed the digest while the bundle
stayed byte-identical and the warning claimed a review command had changed.

Both walks skip the directory now, and the parity test's synthetic tree grows
a fixture and a `.spec.tsx` so the two implementations are held equal on the
whole exclusion, not just the part the first case exercised. Reverting one
side reddens the local case AND both parity cases, which is what that guard is
for.

Verified the other direction too, since an exclusion can overshoot: every
review source that reaches `dist` is still covered. `DESIGN.md` and `SKILL.md`
both ship and both remain in the digest — checked, not assumed, after two
rounds of this exact mistake.

Six cases end to end after a rebuild: a clean tree, a test edit and a fixture
edit are silent; a production edit, a `review.ts` edit and a `DESIGN.md` edit
each warn.

* fix(review): allowlist the stamp, and stop guessing what the bundle holds

The Critical first: `create-standalone-package.js` fails on any top-level dist
entry outside its allowlist, and `review-sources.sha256` was on neither list.
The next release would have aborted the standalone archive on all five
targets, and no PR-time job runs the packager, which is why this suite is
green. Allowlisted -- shipping it is harmless, since a standalone install has
no `packages/` to compare against and the check stays silent there.

`lib/test-utils.ts` was in the digest: test support with a production-looking
name, imported by two test files and nothing else. That is the fourth patch to
one rule -- `.test.ts`, then `__fixtures__/`, then this, plus `.DS_Store` --
and each was found by a reviewer after it shipped. So the rule stops being a
list somebody remembers to extend: a new test asserts the property the list
approximates, that every file the digest folds in is reachable from production
code and nothing reachable is left out. Dropping `test-utils.ts` from the
exclusion reddens it, which is the fifth instance failing in CI instead of in
a review.

Three branches that no test reached, each with a mutant the review measured
surviving the whole suite: the walk's symlink skip (a directory cycle would
send the first command of every review into unbounded recursion), the
read-failure path (hashing the survivors of a concurrent checkout would accuse
a tree that is merely mid-change), and the build's stamp call site (removing
it left the scripts suite green while `npm run bundle` silently stopped
writing the stamp). All three now redden.

And `unmeasured` had no reader, so the one edge this check cannot measure but
can see -- sources present, stamp absent -- passed in silence. That is the
state of every existing checkout the moment this ships, and it is exactly the
silent failure the change was written to end. It now says so, while an
installed package, which has no sources either, still says nothing.

* fix(review): the guard was shallower than the property it claimed

The guard added last round asserts that every file in the digest is reachable
from production code. It did not: a file imported by nothing passed, because
the filter also required some test to import it; only `.ts` was inspected, so a
test-only `.tsx` or `.mts` helper walked through; and it read static imports
only, while this directory has nine `await import('./…')` edges. It asserts the
property now — every extension, orphans included, dynamic edges seen — and the
tree has no violators, so the strictness cost nothing today and is there for
the next file.

`__snapshots__` joins the exclusions. `vitest --update` regenerating a snapshot
would have moved the digest with the bundle byte-identical; none exists under
the review roots today only by chance, and 120 `toMatchSnapshot()` calls live
elsewhere in this package.

Three couplings that no test held:

- the allowlist entry that fixed the release-breaking R2-1 -- reverting those
  five lines left the whole scripts suite green, and the next failure would
  have been a release aborting on all five targets. `isAllowedDistEntry` is
  exported and the stamp's own name is asserted against it, so a one-sided
  rename fails here instead;
- the `.DS_Store` member of `NOT_BUNDLED_FILE`, absent from the repo and so
  from the parity tree -- one-sided removal stayed green while a macOS
  checkout would digest differently on the two sides forever;
- each `unmeasured` reason. Swapping the two arguments at the single call site
  kept all 76 tests green while telling a pre-stamp checkout its sources were
  missing.

And two comments that said the opposite of the code beneath them: the digest is
computed unconditionally on purpose (the pre-stamp notice needs it), and
`NOT_BUNDLED_FILE` helpers are deliberately not importers, since nothing
reaches the bundle through a file the bundle does not contain.

The two stderr diagnostics are documented for users, beside the sibling
paragraph this PR already added.

* fix(review): measure only the layout that can carry a stamp

`npm start` launches `node <root>/packages/cli`, and node sets `argv[1]` to
that directory -- so the derivation found sources under `<root>` with no stamp
beside them and printed "could not check" on every review, forever, with advice
that could never make it stop. That is the fires-when-nothing-is-wrong failure
this change argues against, on the path `start.js` sets `QWEN_CODE_CLI` to
precisely so reviews reach that build. Only a `<root>/dist/cli.js` layout is
measured now; anything else has no stamp to find and no way to grow one.

The build-side digest could kill `npm run bundle` where the check side degrades
gracefully: a file vanishing mid-walk threw out of the hash loop, and the stamp
is the copier's last step, so the build would fail with every asset already in
place. Caught and skipped -- a missing stamp is `unmeasured`, which the runtime
already treats as an acceptable answer.

The skill now says what to do with the warning, which is the half that makes it
reach a human: `parse-args` runs inside an agent's shell tool, the user reads
the agent's summary rather than raw stderr, and a line nobody repeats is a line
nobody sees -- which is how the 2026-08-02 round went wrong in the first place.
It also records that the instruction cannot help the run that needs it, since
the skill comes from the same bundle.

And the scope is stated where silence could be over-read: the digest covers the
review commands, the file that registers them, and the bundled skill -- not the
shared helpers those import. A quiet run means the review code matches the
bundle, not that the tree does.

* fix(review): refuse to certify a bundle the copier may not describe

The stamp described the tree as the COPIER saw it, and the copier runs after
esbuild -- so a source edited in between, or `copy_bundle_assets.js` run on its
own (it self-executes), wrote a digest certifying a `cli.js` built from
something else. Silence then means "verified fresh" when it is not, and that is
the only direction here where a quiet run is affirmatively wrong rather than
merely uninformative: every other gap degrades to `unmeasured`.

Timestamps are the wrong tool for judging staleness and the right one for
judging whether this stamp can be honest at all, so the build refuses when any
source is newer than the bundle it would attest to, and says why. Driven for
real: touching a review source and running the copier alone now prints
"skipped the source digest rather than certify a bundle it may not describe".

`it('counts the same files')` compared nothing -- it asserted `> 50` on the
build side while the check side exposes no count, so the title claimed a parity
the body never checked, and the margin over the real 56 made it a future false
alarm in `scripts/` for an unrelated change. Removed; the digest parity already
holds the file set.

"Root is a file" was inferred from `readdirSync` raising ENOTDIR, an assumption
about every platform's libuv on the one root that is a file -- `review.ts`,
where "a new subcommand was registered" lives. `statSync(root).isFile()` says
it instead.

And the check itself moves out of the handler into `bundleStalenessNotices`,
which is where the rest of it already lived. `parse-args` is about parsing
arguments again, the wording is testable without the yargs harness, and a
second caller -- an agent resuming a review never runs step 1 -- is one line.

* fix(review): align the twin walk, and stop a test from passing on nothing

The build side still inferred "this root is a file" from `readdirSync` raising
ENOTDIR, one commit after the check side stopped doing exactly that and said
why. A platform that maps the case differently would drop `commands/review.ts`
from one digest and not the other, and a byte-for-byte correct bundle would
warn on every review forever, on that platform alone, with rebuilding
reproducing the same one-sided walk. Both sides ask `statSync(...).isFile()`
now. Fixing one half of a pair and not the other is the mistake this file keeps
making.

The filename parity test had been passing on nothing since the previous commit:
it matched `writeFileSync(join(distDir, '…'))` against the script's source, the
literal moved into a `stampPath` variable, and the regex returned `undefined`
so the assertion compared against nothing. It runs the build against a fixture
now and reads the name off `dist/`, so it measures what the build does instead
of what its source looks like. Renaming the stamp on one side reddens it.

Also from review: the duplicated comment block in `parse-args`; an unreadable
source now says the check could not run rather than passing in the same silence
as an installed package, which is what the docstring already promised; the
"could not check" line no longer asserts that the checkout predates the
feature, since the build has three refusal paths and one of them means the
opposite; every refusal removes an existing stamp, because leaving an older
attestation beside a newer bundle is a weaker form of the certifying it
refuses; and `drive` calls the check, which the module comment argued for and
the diff had not done -- a resumed review never runs step 1, and that is where
the long work starts.

* fix(review): pin the regex group the parity tree missed, and say source, not command

* fix(review): allowlist what the bundle holds, and cover the drive notice (#8390)

* fix(review): treat unreadable review sources as unmeasured (#8390)

* test(review): pin the stamp guard mutations that survived the suite (#8390)

* fix(review): close staleness-check gaps and pin the round-4 survivors (#8390)

* fix(review): close round-5 staleness gaps for parity, refusals, and partial checkouts (#8390)

* fix(review): close round-6 gaps in the clause classifier, symlink layout, and pin honesty (#8390)

* fix(review): close round-7 gaps in the closure oracle, parity pin, and refusal pins (#8390)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(review): close round-8 gaps from the maintainer review (#8390)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(review): close round-9 nits from the maintainer review (#8390)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(review): pin the lease root in the synthetic digest parity case (#8390)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Autofix <autofix@qwen-code.dev>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-07 03:21:26 +00:00
qqqys
d5e47709a3
fix(ci): surface blocked autofix takeover admission (#8410)
Some checks are pending
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* fix(ci): surface blocked autofix takeover admission

* fix(ci): clarify blocked takeover retries

* fix(ci): retry blocked takeover reporting

* fix(ci): serialize forced takeover status writes

* fix(ci): report blocked managed PRs

* fix(autofix): treat terminal permission answers as a routine rejection

Two review blockers on the forced takeover admission gate.

R7-1: `read_live_permission` whitelisted only admin/maintain/write/triage/
read, so GitHub's definitive answers for "holds nothing here" never matched.
Bot-type logins (dependabot[bot], github-actions[bot], renovate[bot]) and org
logins return HTTP 200 with permission 'none'; nonexistent or empty logins
return 404. Both burned three API calls plus back-off and then returned
`permission_lookup_failed`, so the forced path exited 1 (a red run) instead of
the routine `author_permission_none` rejection, the blocked comment promised
"a later scheduled scan will retry" — a retry that can never succeed — and the
actionable "grant the fork author write access" guidance behind
author_permission_* was unreachable. `author_permission_${FPERM:-none}` could
never render `none`. The scheduled loop re-paid the same cost per candidate
per tick, permanently.

Accept 'none', answer HTTP 404 terminally, skip the call for an empty login,
and keep the retry budget for genuinely transient answers (5xx, network, auth)
so a legitimate write-holder is never silently rejected. gh's own stderr now
rides along in the warning instead of going to /dev/null — a rate limit, an
expired PAT and a 5xx were indistinguishable before.

R6-1: the new blocked-status comment lookup is the 14th `--paginate` code
site, but the deliberate site-count pin still asserted 13, failing
`Test (ubuntu-latest, Node 22.x)` deterministically. Bump it to 14 and record
why this site stays out of the `jq -s 'add // []'` normalizer: it consumes the
page stream inline via `--jq ... | .id` into `tail -1` and never lands in a
WORKDIR json file, so wrapping it in an array would break the tail-1 consumer.

R6-2: pin the forced-admission wiring (reader -> classifier -> live-permission
gate -> reporter) end to end, plus fixtures for none, 404, empty login, every
grant level, and a transient 5xx.

* fix(autofix): harden the blocked-takeover status write (#8410)

Addresses review round 2 on e49aadef.

- Status-comment lookup now uses the same jq filter as the sibling upsert in
  'Post autofix status comment': `// ""` guards so one comment with a null
  body cannot abort the program (verified: the old filter exits 5, so all
  three attempts fail and the run reds out without posting the very status
  it exists to post), and --arg for AUTOFIX_BOT so a repo-configured login
  containing " or \ is a mismatch rather than a jq parse error.
- The lookup no longer sends gh's stderr to /dev/null, matching the rule
  read_live_permission states 60 lines above.
- The blocked body resolves its run link from GITHUB_SERVER_URL like every
  other status writer, instead of hardcoding github.com.
- All four reporter warnings go to stderr like the two reader helpers, so
  the reporter stays safe to wrap in $( ).

Tests: the reporter and reader harnesses now run under production shell
options (`set -eo pipefail`) and call through `|| exit $?` so errexit is
suspended inside the helper exactly as the `if !` call sites do. New cases
cover the null-bodied comment page, the PAT-identity mismatch branch, the
in-repo pass-through (no collaborator call), and the permission_lookup_failed
red exit. Each kills a probe-verified mutant that previously survived:
dropping the `// ""` guard, dropping the `isCrossRepository == true`
conjunct, and flipping that `exit 1` to `exit 0`.

Verified: npm run test:scripts 946 passed / 9 skipped, 46 files;
prettier --check clean; eslint clean; git diff --check clean.

* fix(autofix): narrow the forced-scan lock to the review condition (#8410)

Review round 9 raised two Suggestion-level findings on the forced
admission path; both are addressed here.

R9-1 (.github/workflows/qwen-autofix.yml:1698) — the review-scan
concurrency group was broader than the job's own `if:`. GitHub
evaluates concurrency before the job condition, and `route` emits
`pr_number` unconditionally from the dispatch input, so a
`workflow_dispatch` with `phase: issue` + `pr_number: N` resolved the
group to `qwen-pr-head-write-N` even though `do_review` is false and
the job only skips. Because `issue-autofix` declares
`needs: ['route', 'review-scan']`, the skipped leg could queue behind
an in-flight address round for PR N (`timeout-minutes: 300`,
`cancel-in-progress: false`) and stall the issue phase the operator
actually dispatched, with only a "queued" badge to explain it. The
group now carries the same `do_review == 'true'` conjunct, so a run
that will skip falls to the per-run `github.run_id` group. The forced
path's serialization against review-address is unchanged.

R9-2 (scripts/tests/qwen-autofix-workflow.test.js:4034) — the pin
asserted both jobs' groups as independent literals and never compared
the two prefixes, so renaming one side while updating its literal in
the same block would ship green with the lost-update race on the
status comment silently reopened. The test now extracts both prefixes
and asserts equality, mirroring `groupOf` in
qwen-resolve-workflow.test.js, and a new case pins the group predicate
against the job's own `if:` block.

Verified: qwen-autofix / qwen-resolve / qwen-triage workflow suites
266/266 pass. Both new assertions were mutation-checked — reverting
the group to the broad predicate fails 2 tests, and renaming only
review-scan's prefix fails the equality assertion. Prettier and ESLint
clean (the group is double-quoted because the expression embeds
'true', which Prettier will not leave escaped).

* fix(autofix): carry pipefail on the blocked-status comment read (#8410)

Maintainer verification of the forced-admission path found the one `if`
in `report_forced_takeover_blocked` that tests a PIPELINE rather than a
single command:

    if status_ids="$(gh api .../comments --paginate 2> "${err}" | jq -rs ...)"

A gh failure carrying an HTTP status prints the error body to stdout, so
jq chokes on it and the bounded retry fires. A CONNECTION-level failure
(TCP reset, TLS abort, DNS blip) leaves stdout empty — `jq -rs` then
prints nothing and exits 0. Absent pipefail that reads as success on
nothing read: the loop breaks on attempt 1, `status_lookup_ok` goes true,
and the empty id routes the writer to its "no status comment yet" branch,
posting a DUPLICATE blocked comment beside the stale one, run green.

Set the option locally on that command substitution. `defaults.run.shell:
bash` expands to `bash --noprofile --norc -eo pipefail`, so the step is
already pipefail on a real runner and this is redundant today; it is the
only guard that survives that default changing or the helper moving to a
step that sets its own options.

Tests: replay the same connection-level failure with the ambient pipefail
dropped and pin 3 bounded reads, exit 1, and no comment written; add the
HTTP-status half so the two failure shapes stay distinguishable and the
local option cannot be "simplified" away as carried by jq; pin both the
`set -o pipefail` and `defaults.run.shell: 'bash'` textually, since the
ambient half is what every other gh|jq writer in this file relies on
without saying so.

Both mutations verified to fail the suite.

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-06 23:22:39 +00:00
rockybot2026
732f4d8a29
feat(voice): support trusted private ASR base URLs (#8350)
* feat(voice): support trusted private ASR base URLs

* fix(voice): address private endpoint review findings

* test(voice): cover private endpoint edge cases

* test(voice): pin remaining endpoint edge cases

* fix(voice): address private endpoint review feedback

* fix(voice): clarify allowlist URL and normalize IPv6

* fix(voice): harden NAT64 address validation

* fix(voice): address managed endpoint review findings

* refactor(voice): extract shared IPv6 transition unwrap ladder (#8350)

Deduplicate the IPv6-transition unwrapping sequence (mapped, compatible,
NAT64, dotted-quad) that was repeated verbatim between isPrivateNetworkIp
and isAlwaysBlockedVoiceAddress on both CLI and Desktop surfaces. A single
unwrapIpv6TransitionStep helper now yields the next canonical address (or
'blocked' for unrecognized ::ffff: forms), and each predicate recurses
through it, preserving the exact re-check semantics at every unwrap level.

* test(voice): cover allowInsecureBaseUrl wiring through desktop default transports (#8350)

* fix(voice): add allowlist hint to private-network rejection error (#8350)

* fix(voice): reject always-blocked base URLs before offering the allowlist hint (#8350)

* fix(voice): resolve exact desktop voice provider before OAuth (#8350)

* fix(voice): address review feedback for trusted private base URLs (#8350)

* fix(voice): align desktop voice resolution with CLI semantics (#8350)

* fix(voice): scope desktop fail-closed resolution to policy-bearing entries (#8350)

* fix(voice): address round-8 review findings for trusted private base URLs (#8350)

Run the invasive process-global `mock.module('ws')` suite as
voice-ws-handler.isolated.ts so the desktop package's single-process
`bun test` run no longer leaks the fake socket into unrelated ws
consumers; the existing isolated loop runs it in its own process.

Shape-guard the desktop provider scan: non-object modelProviders
elements are skipped (falling through to OAuth instead of throwing a
raw TypeError), and non-string baseUrl/envKey/settings.env values on a
voice-model entry now surface the PROVIDER_ENTRY_REMEDY remediation
error instead of crashing.

Compute the DashScope-compatible /v1 rewrite before any allowlist
match in fromExactModelProvider so the stage-1 check, the remediation
messages, and the top-level recheck all compare the same final URL and
a single allowlist entry converges for split-horizon deployments.

Extend the CLI allowlist remediation messages to state which settings
scopes honor the entry, since serve mode never shows the interactive
workspace-strip warning. Thread providerProtocol through the CLI voice
model seams (createVoiceModelSource and the daemon buildModelsConfig)
so protocol-mapped custom provider groups resolve like the rest of the
CLI model surface, and document the remaining protocol-agnostic desktop
scan in the design doc. Correct the getHomeEnvFallback comment: it
adopts the narrower getHomeEnvFallbackVars candidate set on purpose.

Add multi-record DNS answer tests on both CLI and desktop net guards so
the records.some classification is pinned against the array shape
defaultLookupHost always produces in production.

* fix(voice): address round-9 review findings for trusted private base URLs (#8350)

* fix(voice): address round-10 review findings for trusted private base URLs (#8350)

* fix(voice): classify desktop voice duplicates before ambiguity check (#8350)

* fix(scripts): compare voice guard mirrors as parse trees (#8350)

---------

Co-authored-by: rockybot2026 <265985139+rockybot2026@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-06 14:04:57 +00:00
Shaojin Wen
35108c0cf0
ci(autofix): run heavy autofix jobs on the ECS pool with hosted fallback (#8603)
* ci(autofix): run heavy autofix jobs on the ECS pool with hosted fallback

The heavy autofix jobs (issue-autofix, build-cli, review-address) were
pinned to GitHub-hosted runners, where they hold up to five 300-minute
slots per scan. Combined with CI, that saturates the org's ~20 hosted
concurrency slots and starves the PR-review workflow's hosted-only jobs
(delay timer, fork authorize, /resolve).

Route the three heavy jobs to the persistent ecs-qwen pool using the same
repository-guarded expression as the rest of the fleet, with an
ubuntu-latest fallback for forks of this repo and for
MAINTAINER_ECS_RUNNER_DISABLED.

This is safe for the fleet's two secret-bearing agent jobs:
- Every review-address target is live-gated to write+ (internal) authors
  at scan AND address time; in-repo PR heads already require push access.
- CI_DEV_BOT_PAT is a write-level branch-push token with no privilege
  escalation (cannot bypass branch protection).
- The ECS pool ships docker, and repo-hygiene already runs the sandbox
  image there in production; sandbox containers use --rm with no fixed
  name, so concurrent legs on one machine do not collide.

Relax the 'Check runner environment' gate to accept self-hosted, and drop
the github-hosted condition on 'Set up Node.js' so Node 22 is provisioned
on ECS. The contract test is updated to pin the new ECS routing expression
(including the repository guard and hosted fallback) instead of the old
hosted-only one.

* ci(autofix): harden ECS-routed heavy jobs for the shared persistent pool

- Restore workspace ownership before checkout in issue-autofix,
  build-cli, and review-address, matching the sibling pool workflows
  (root-owned leftovers from containerised jobs break checkout/npm ci).
- Sanitize the reused workspace's local git config (allowlisted
  unset-all + symlink-aware hook sweep, qwen-triage's pattern) before
  the PAT-bearing git steps in issue-autofix and review-address.
- Give issue-autofix a per-run WORKDIR (/tmp/autofix-<run_id>): pool
  registrations share one /tmp and issue-phase runs never serialize.
- Drop stale local autofix/issue-* branches after checkout so a retry
  on the same registration cannot hit "branch already exists" or push
  a failed attempt's stale commits.
- Extend the ECS routing guard for PR-family events (same-repo head or
  write+ author), mirroring ci.yml's classify_pr convention, and fix
  the runs-on comments that claimed fork PRs already fell back.
- Stop persisting CI_DEV_BOT_PAT into the origin remote URL: publish
  pushes authenticate transiently, and the redundant set-url in
  'Push and report' is removed (push/fetch already use a URL inline).
- Pin all of the above in the workflow contract tests, reusing hoisted
  job-block constants.

* ci(autofix): harden ECS pool hygiene with shared hardened sanitize action

Review round for the ECS routing of the heavy autofix jobs. The
sanitize step shipped in the previous commit was the trusted-lane
variant of the qwen-triage sweep, which two verified bypasses defeat
(worktree-scoped config carrying core.hooksPath, and a global
core.hooksPath steering the hook deletion). Port the hardened variant
into a composite action so all three heavy jobs share one copy, and
add it to build-cli, whose checkout runs in the same reused workspace
and feeds the bundle every PAT-bearing leg executes.

Also completes the persistent-pool hygiene the routing depends on:
per-run/per-target WORKDIRs now chmod 700 at creation and are removed
by an always() teardown step after the artifact upload (nothing else
ever deleted them); the stale-branch sweep detaches HEAD first and
globs via BRANCH_PREFIX; the artifact upload reads env.WORKDIR
instead of a duplicated literal.

The tautological runner-environment gate is repurposed as a
fail-fast docker preflight (a missing daemon otherwise surfaces only
at 'Resolve sandbox image', after npm ci/build), and Install tmux
switches to sudo -n so hosts without passwordless sudo fail with a
clear message instead of dying on a prompt.

The runs-on comments are rewritten to say exactly what they guard:
which events carry the same-repo/write+ clause and which rely on the
live write+ or label gates, why review-address is an author-permission
gate rather than a head-repository gate, and where the pool's docker
availability is proven in-repo (qwen-triage's container jobs on the
same labels).

Contract tests pin all of it: hygiene steps present and ordered
before checkout in every heavy job, the hardened sweep's ordering and
containment in the shared action, the docker preflight and tmux
hardening, the WORKDIR lifecycle, hosted-only short jobs, and generic
next-job slice boundaries so a job inserted after review-address can
no longer hide inside its test slice.

* ci(autofix): inline git-config sanitize step before checkout

The composite sanitize action was invoked as a local action
(`uses: './.github/actions/sanitize-workspace-git-config'`) BEFORE
`actions/checkout` in all three ECS-routed jobs. A local action resolves
from $GITHUB_WORKSPACE, so:

- on any clean workspace it hard-fails with "Can't find 'action.yml'" —
  every hosted-fallback run (forks, MAINTAINER_ECS_RUNNER_DISABLED=true,
  i.e. the documented rollback path) and any freshly registered ECS
  runner — and it cannot self-heal, dying before checkout.
- on a reused workspace it executes the leftover copy, which for
  review-address is the previous run's PR branch — pre-checkout execution
  of PR-controlled content, the exact vector the step exists to prevent.

Inline the hardened sanitize script as a plain `run:` step in each job
(the qwen-triage 'Clean stale agent state' pattern) and delete the
now-consumer-less composite action. Update the contract test, which had
pinned the broken ordering, to pin the inline run step instead, and add a
generic guard that no local action may precede a checkout.

* ci(autofix): harden PAT handling and pool isolation on the shared pool

Address the Medium findings from the ECS-routing review:

- PAT pushes (Publish PR, Push and report, and the fork dry-run
  preflight) now authenticate via a transient credential helper instead of
  a token-bearing remote URL. The token enters neither the reused
  workspace's .git/config nor the process argv, which is world-readable
  via /proc on this shared host; the helper config holds only a
  ${GITHUB_TOKEN} reference and is swept by the next sanitize pass.
- 'Check runner environment' now asserts the self-hosted runner is an
  ecs-qwen pool member by name, so a mis-labelled registration cannot
  silently claim a PAT-bearing 300-minute job.
- 'Reset autofix workspace' age-sweeps abandoned /tmp/autofix-* dirs (a
  hard runner kill skips the always() teardown and run_id never repeats)
  and prunes the reused workspace's .git.

The contract test is updated to pin the tokenless push/fetch URLs, the
credential-helper authentication, and the absence of a token-bearing URL.

* ci(autofix): create autofix workdirs with umask 077 and correct cleanup comments

* ci(autofix): scope PAT credential helper to a one-shot git -c, plus review fixes

Address the latest review round on the ECS-routed heavy jobs.

High — the transient credential helper was not transient: `git config
--local credential.helper` wrote into the reused workspace's .git/config
and the matching unset was skipped on every error path (bash -e abort,
the fork-preflight exit 0, the salvage-loop exit 1s), stranding a helper
that answers any host with the PAT where ci.yml / pr-review jobs could
read it. All three push paths now use a one-shot, host-scoped
`git -c credential."https://github.com".helper=...` (a git_auth wrapper
for the salvage loop), so nothing is ever written to .git/config.

Medium — narrow the sanitize allowlist's `remote.*` to
`remote.<n>.(url|fetch|pushurl)` (uploadpack/receivepack are command
strings the sweep previously kept), and cover all three inlined sanitize
copies in the hardening test with a byte-identical pin (previously one
of three), rewording the stale "composite action" references.

Low — drop the recursive `rm -rf` hooks fallback that could wipe the git
dir itself (warn instead), extend the /tmp sweep glob to the legacy fixed
dir (`autofix*`), and make `git gc` threshold-driven (`--auto --prune=now`).

The contract test pins the git -c / git_auth form, the absence of any
config-written credential helper, and the tokenless URLs throughout.

* ci(autofix): pin gc to the foreground and anchor push auth assertions

Round-2 review follow-ups:

- `git gc --auto --prune=now` detaches by default (gc.autoDetach=true),
  and --prune=now is unsafe when other processes write concurrently —
  exactly what follows in these 300-minute jobs. Force it synchronous
  with `-c gc.autoDetach=false` so the repack still only fires when the
  --auto thresholds say so, but never races the checkout, agent commits,
  or merge/retry loop that come after it.
- Tighten the two push assertions (dry-run preflight, Publish PR) to
  require the host-scoped `git -c credential."https://github.com".helper`
  prefix immediately before the push, instead of a bare
  `push --no-verify …` match that a `git push` regression would still
  satisfy.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-06 07:19:32 +00:00
Shaojin Wen
2c514b50b9
fix(autofix): serialize scan-and-pick issue runs in one concurrency group (#8435)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* fix(autofix): serialize scan-and-pick issue runs in one concurrency group

The issue-phase concurrency group fell back to github.run_id for every
run that route did not force a target — which is every scheduled run and
every label-triggered run. Run-unique groups exclude nothing, so two
overlapping scans (cron effectively fires every 40-70 minutes while the
job may run for up to 180) could both pass the live label recheck during
the minutes-long assess call, double-claim the same issue, and burn two
multi-hour agent runs on it; the loser then fails its push and posts a
withdraw comment.

Key forced dispatches per issue, label events on the payload issue, and
all scan-and-pick runs (cron or unforced dispatch) on one shared
'scheduled' group. cancel-in-progress stays false so a superseded tick
still runs when targets remain.

* fix(autofix): make concurrency group pin prettier-stable (#8435)

* fix(tests): sync qwen-resolve-workflow timeout pins with repository variables (#8435)

* fix(ci): pin issue-phase concurrency group equal to FORCED_ISSUE (#8435)

* test(ci): anchor right edge of issue-phase concurrency group pin (#8435)

* fix(autofix): keep never-runnable runs out of the issue-phase concurrency group (#8435)

* test(autofix): pin issue-autofix concurrency gate to the job if predicate (#8435)

* fix(autofix): exclude dry runs from the issue-phase concurrency groups (#8435)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(autofix): pin the Claim/Publish dry-run step gates (#8435)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: verify <verify@local>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-06 05:27:39 +00:00
顾盼
8fd0162c68
fix(release): normalize Live Host signing identity (#8579) 2026-08-06 02:29:38 +00:00
Shaojin Wen
89b3d5ea8e
fix(autofix): ship core dist in the review CLI bundle (#8612)
* fix(autofix): ship core dist in the review CLI bundle

The review fan-out (#8548) dropped the per-leg build: legs restore the
shared bundle's repo-root dist/ and rely on the verify gate's full
build for branch verification. But the gate's settings-schema and i18n
checks run BEFORE any build, on every path including no-action, and
their tsx-transpiled cli sources import '@qwen-code/qwen-code-core',
which resolves through the workspace symlink to
packages/core/dist/index.js. With no build on the leg, the generator
crashes with ERR_MODULE_NOT_FOUND and the gate misreports a
deterministic "settings schema is stale" rejection (run 31031063525 on
PR 8600), then burns an 18-minute repair agent round on an environment
problem no agent can fix.

Ship packages/core/dist (+~8.5MB gzipped) alongside the root dist/ in
the fan-out artifact and assert its entry point on restore. This
restores exactly the pre-fan-out state: legs used to build the trusted
base themselves before the branch checkout, so the gate always ran
against base-built core dist. The workflow contract tests pin the new
tar command and the restore-side assertion.

* fix(autofix): rebuild branch-touched core dist before the schema gate

Review feedback on the core-dist bundle fix:

- Rebuild packages/core from branch sources in the review verify gate
  when the branch diff touches core's sources, so the pre-build
  settings-schema check never compares the branch's committed schema
  against a base-built dist (changed runtime constants) or crashes the
  generator (changed exports). Lives in the shared gate script so both
  the initial and the repair gate are covered.
- Narrow the bundle/restore comments and their test mirror to the
  settings-schema generator: the i18n check resolves core to sources
  via the packages/cli tsconfig paths map and needs no dist (verified
  empirically).
- Anchor the tar contract pin at end-of-line so additive path drift
  fails the suite instead of passing on a substring match.
2026-08-06 01:22:02 +00:00