Files
qwen-code/.github
Shaojin Wen 7bc00a3a08 feat(review): re-anchor on drift, run actionlint, account blockers, refuse retries (#16001)
* feat(review): re-anchor on drift, run actionlint, account blockers, refuse retries

Four fixes from the 2026-10-31 audit of seven review run ledgers
(#15057, #15377, #15385, #15578, #15853, #15955, #15974):

- Head drift posts the completed review at the new head (the re-anchor
  protocol) instead of abandoning the round — the maintainer's ruling
  on #15578, never landed in the skill until now. The once-per-review
  bound and the COMMENT cap survive; the marker withholds its anchor.
- script-lint runs actionlint's workflow-level checks (syntax, context
  availability, action refs) and gates them as [lint] Criticals; only
  the embedded run: shell stays deferred. #15974's unloadable workflow
  (runner context in jobs.env) is the regression test, measured against
  actionlint 1.7.12.
- pr-context writes a blocker inventory; compose-review cross-checks it
  against the round's blockerRulings and caps at COMMENT when the walk
  skipped a blocker (#15974's missed review-level blocker).
- submit documents --dry-run as the only sanctioned probe and refuses a
  re-post at the commit already landed (already-posted-this-commit) —
  the retry that replied this round's own roots back to themselves
  (#15853). The receipt records lastCommitId.

Each fix carries its rule text, pin tests, and a DESIGN.md incident.

* fix(review): isolate actionlint config and refresh lint on re-anchor

Both Criticals from the #16001 review, reproduced against the exact PR
source and closed structurally:

- The newly active actionlint invocation auto-loaded the reviewed repo's
  .github/actionlint.yaml, so a PR-added ignore-all rule suppressed every
  workflow diagnostic — including the unloadable-workflow expression this
  linter was activated to catch. It now runs against a private mkdtemp
  neutral config via -config-file (measured to override the discovery),
  fails closed when no private config can be created, and carries exactly
  the two trusted compatibility exceptions the repo's own lint script
  runs (self-hosted runner labels, environment.deployment schema lag).
  The regression plants the ignore-all config in a real temporary git
  repository and proves the expression finding survives.

- The re-anchor protocol dropped fixed authored findings but left the
  lint gate regenerating its findings from the old-head report, so a
  lint blocker the delta fixed was republished against the new commit.
  The protocol gains a refresh step (scratch worktree at the new head,
  script-lint --diff rewriting the report bound to the new diff) and a
  lintDiffPath compose field that makes the gate — body and ledger
  marker alike — hash the new diff as its freshness basis. Executable
  regressions pin both directions: fixed blocker gone (COMMENT, absent
  from body and marker), still-standing blocker keeps gating
  (REQUEST_CHANGES). A malformed lintDiffPath fails closed.

* fix(review): base-tree actionlint config, empty-diff parse, lane provisioning

Three findings from the round-3 review of #16001, each reproduced
against the exact source and closed:

- The private {} config blocked PR-planted suppressions but also dropped
  the repo's maintainer-merged compatibility exceptions (ECS runner
  labels, concurrency.queue, the $/ self-repository action reference),
  turning correct release-workflow changes into bogus [lint] Criticals.
  The trust boundary is now the MERGE-BASE tree's .github/actionlint.yaml
  (hex-shape-guarded git show, copied into the private file, {} only when
  the base has none); the head copy is never read, and the two trusted
  -ignore constants ride on top exactly as scripts/lint.js runs them.
  Pinned with a two-commit repo and a real-binary run showing the base
  exception honored while the planted ignore-all stays blocked.

- An empty refresh diff (the new head reverted the PR's last change) was
  treated as a parse failure: the range selection fell back to the old
  plan's context-inclusive hunks and promoted a pre-existing finding into
  a blocker the gate accepted as fresh (the empty diff has a valid hash).
  diffParsed now separates parse success from range count; empty and
  deletions-only diffs classify everything pre-existing. The reviewer's
  exact scenario is an executable regression.

- The lanes that run the gate provisioned no linters, so the new
  uninstalled-capping fired on CI and the real-binary regressions were
  skipIf'd out of every run. The review job and the unit-test lane now
  install the pinned, SHA-256-verified actionlint/shellcheck through the
  repo's own installer and promote the job-scoped dirs onto GITHUB_PATH
  (unconditional, advisory, Windows excluded). The 335 workflow meta-test
  pins pass unchanged.

* fix(review): lint in the worktree cwd, guard lint.js entry, pin the sha fence

Three gaps from the scripted deep verification of #16001 (50/55
assertions, 17 mutants), two of them in the round-3 fixes themselves:

- F1: actionlint resolves the config's paths: entries against its cwd,
  and the runner spawned it in the session checkout, so every
  path-scoped base exception was inert while the global keys kept
  working — the split that hid behind the labels-only test. The runner
  now receives the worktree root and spawns inside it with PWD aligned.
  The real-binary suite gains the path-scoped fixture it was missing: a
  benign concurrency.queue edit that must lint clean, and an attack cell
  where the base exception suppresses the schema-lag finding while the
  planted head ignore-all still fails to hide the expression.

- F2: the provisioning steps imported scripts/lint.js, whose
  unconditional top-level main() ran a full lint pass on import and
  never reached the install loop, silently under continue-on-error.
  lint.js now runs its CLI only when it is the entry point
  (import.meta.url guard, argv[1]-safe for node -e imports), and both
  steps assert each installer produced its binary before promoting the
  dir, so a future no-op install fails visibly instead of degrading to
  skipped tests.

- M15: the option-injection fence was unobservable — git's own
  rejection of a malformed option matches the guard's refusal, so the
  guard's mutation survived. A spawn spy now pins the fence directly:
  non-hex merge base never spawns git; a hex-shaped nonexistent one
  does (positive control with the exact argv).

Also acknowledges the verification's correction: the bot review's
$/prepare-release-runner example does not exist at this head (the base
config entry is precautionary); the queue exception at line 485 is the
one reachable path-scoped case, and F1 is pinned on exactly that shape.
2026-10-31 13:43:34 +00:00
..