Commit graph

626 commits

Author SHA1 Message Date
Shaojin Wen
c59910ba3f
fix(ci): make autofix finding replies idempotent (#9463)
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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(ci): make autofix finding replies idempotent

A crash-and-rerun of an address round, a same-run repair that regenerates the dispositions, or a later round re-declining the same finding all reproduce the same comment-replies.json entry — and the reply step posted it again, landing identical bot replies on one thread (observed 2026-08-16: one identical reply posted three times, #9296).

The thread fetch now also reads each comment's author and body, and the reply step skips posting when the thread already carries a comment by the autofix bot whose body equals the neutralised body about to be posted. A changed body — new information from a later round — still posts; a threads view without author/body, or a stale/empty one, degrades to the old post-always behavior. The replies API itself is already the no-review-event path, so this PR only adds the missing idempotence (the P1 replies item of #9296).

Refs #9296

* fix(ci): restore inner pageInfo in autofix threads query and pin reply-gate contracts (#9463)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-20 14:10:26 +00:00
Shaojin Wen
4807d06b31
fix(autofix): mutation-probe new guards before a round commits (#9578)
* fix(autofix): mutation-probe new guards before a round commits

Provenance analysis of six multi-round takeover PRs found roughly a
third of post-initial review findings were introduced by the
immediately preceding fix round, and the dominant shape was guards
and branches added with no test of their own: the deterministic gate
re-runs only the tests that exist, so an unwitnessed guard passes
every gate and its hole resurfaces as a new finding in a later
round. Require the agent to prove each newly added guard or branch
kills a test (mutation probe) before committing, and record the
probe in the round summary.

* fix(autofix): pin the mutation-probe mandate, not just its intro

The original pin stopped at the preposition "before", so a future
edit that kept the intro phrase but gutted the remove/confirm-FAIL/
restore procedure would leave the contract test green. Pin both
halves of the mandate. Mutation-verified: gutting the mandate in
SKILL.md now fails the skill contract test.

* fix(autofix): also pin the mutation-probe remediation clause

The procedure pins cover what the probe does, but not what to do
when it exposes an unwitnessed guard: dropping the remediation
sentence ("write a test that pins it (or drop the guard)") left the
whole suite green while the rule told the agent to probe without a
verdict path. Pin the remediation clause too; mutation-verified.
2026-08-20 14:03:00 +00:00
Shaojin Wen
099a71c936
fix(ci): heal a symlinked workspace instead of wedging the runner on it (#9498)
* fix(ci): heal a symlinked workspace instead of wedging the runner on it

The hardened wipe guard refuses any workspace that canonicalizes outside
the runner workspace. That refusal is correct, and it created a permanent
failure: when a previous job leaves the workspace replaced by a symlink
pointing outside — or by any non-directory — the guard resolves it to the
target, refuses, and exits 1 having removed nothing. Nothing else clears
that state, so every later job on the runner dies at the same line,
forever. The pre-guard code wiped through the link and self-healed by
accident. Reproduced against main's own step text before this change.

Heal it: the link itself lives inside the runner workspace and is safe to
unlink, and only once it is gone can a legitimate wipe proceed.

The layer has to sit before canonicalization — afterwards the path has
already resolved to the target and the allowlist refuses before any repair
can happen — which means it judges a raw path, and that is where the first
attempt at this (closed with #9369) went wrong. A raw `"$RWS"/*` match
accepts `$RWS/link/sub` as a string while the kernel resolves it through
an intermediate symlink to a file outside the runner workspace, so the
unlink and the mkdir landed outside and only then did the allowlist refuse
the wipe. Here the containment is judged on the canonicalized PARENT —
never on $WS, which would resolve through the very link being removed —
and the unlink then acts on the raw path, so it takes the link and never
follows it.

Four more constraints the same review surfaced: the raw trailing-slash
strip moves ahead of the predicates (both `[ -L "$WS/" ]` and
`[ ! -d "$WS/" ]` resolve through a link and report its target, so one
slash hides the corruption); the allowlist root is prepared before the
heal, since it bounds it, and an empty $RUNNER_WORKSPACE would degenerate
the containment pattern to the match-all `/*`; both the unlink and the
mkdir fail closed, because under `-e` a failure that is not the last
command of an && list is swallowed and would leave the wipe running on a
corrupt path; and the heal logs what it found and where the link pointed,
since this incident otherwise leaves no trace at all.

All three copies get it — the two triage wipes and the A/B wipe — with
per-suite fixtures: the wedge healed (link gone, directory recreated,
target's contents intact), the intermediate-symlink attack refused with
the outside file unmutated and zero rm calls, the non-directory half, the
trailing-slash spelling, the fail-closed unlink, and the ordinary
workspace where the heal must not fire at all. Mutation-checked layer by
layer; each has a fixture that fails when it is removed.

One pre-existing test changes meaning: the canonicalization pin used a
symlinked workspace and asserted refusal, which is now the healed path. It
moves to a vector the heal does not touch — an intermediate symlink whose
far end is a directory — and keeps its mutation strength: with the
canonicalization deleted, find resolves the link and hands the outside
directory's entries to the rm recorder.

Closes #9480

* fix(ci): keep the heal's log out of the workflow-command channel

Three findings from the first review round on this layer.

The heal logged the symlink's target inside a `:⚠️:` line. The
target is bytes a PREVIOUS job chose — on the verify lane that job may
have run a contributor's code — and the runner parses `::` at the start
of any stdout line as a workflow command, so a target of
$'…\n::error::forged' let the step reporting the corruption forge an
annotation. The annotation now carries no untrusted bytes: the target is
stripped of line breaks, capped, and printed on its own prefixed line,
where a leading `::` cannot begin a command. Verified against the real
step text — the forged line lands as data, and no output line starts with
`::error::`.

The mkdir leg's refusal had no executed fixture while its `rm -f` sibling
had one. It does not need a permission trick: `rm -f` returns 0 for a
path whose parent is not a directory (it reads as "already absent"), and
the mkdir that follows cannot succeed — so the branch is reachable, and a
swallowed failure there would run the wipe against a path that does not
exist. Fixtures in both suites, and it runs as root too.

And the post-run triage copy's header still said this copy "predates the
checkout-heal hardening and never received it" while carrying the whole
guard plus the heal directly underneath. That header is the in-code
inventory the eventual convergence of these copies will read; understating
it is how a sync strips layers in the wrong direction.

* test(ci): drive both wipe copies in the remaining single-step heal fixtures

* fix(ci): keep the Serve A/B job from timing out on slow runners

---------

Co-authored-by: Qwen Autofix <autofix@qwen-code.dev>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-20 13:54:36 +00:00
Shaojin Wen
6fe9ce4886
fix(ci): stop counting wedged queued runs as in-flight in the shepherd (#9518)
* fix(ci): stop counting wedged queued runs as in-flight in the shepherd

When GitHub refuses to start a workflow run it still CREATES it: the run sits
`queued` forever with zero jobs and cannot be cancelled or deleted through the
API. On 2026-08-19 an oversized qwen-autofix.yml produced a run like that from
the shepherd's own liveness dispatch, and the watchdog counted it as in-flight
for the next 18 hours:

  last scan signal: 2026-08-19T05:01:14Z (1107m ago), in-flight: 1

The age gate said "dispatch a scan", the in-flight gate said "one is already
running", and nothing ever completed the run that would clear it. The loop
stayed dark until a human looked.

Treat a run still `queued` past ZOMBIE_QUEUED_MINUTES (30, overridable via the
QWEN_SHEPHERD_ZOMBIE_QUEUED_MINUTES repository variable) as wedged rather than
live. One `wedged` predicate is defined once and reused by the in-flight count,
the conflict lever's busy-set, and a new census, so the three readers cannot
disagree. Only `queued` runs wedge — a review-address run legitimately runs for
hours — and a missing createdAt reads as brand new, so unknown age never
licenses a duplicate dispatch.

The wedge is now visible instead of silent: a :⚠️: names the count and
the oldest one, the tick heartbeat carries `wedged-queued:`, and the dashboard
carries a banner. Invisibility is what made this expensive — PR-event runs kept
reporting success while every scheduled scan was dead.

Verified against the real run list from the incident: the old predicate returns
in-flight=1 (starved), the new one returns 0 with a census of 2. Behavioral
tests replay both jq programs and the busy-set walk verbatim from the workflow.

* fix(ci): keep shepherd busy-set job-verified and bound wedge re-dispatch (#9518)

* fix(ci): reject degenerate zombie threshold and name the paused liveness gate (#9518)

* fix(ci): name the recorded liveness run in the shepherd wedge remedy (#9518)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-20 13:37:22 +00:00
Shaojin Wen
313f191150
fix(autofix): make the brake's BLOCKED handoff a first-class round outcome (#9297)
* fix(autofix): make the brake's BLOCKED handoff a first-class round outcome

When the growth brake fires, feedback.md tells the address agent to stop BLOCKED with a handoff — but the output contract only accepted address-summary.md or no-action.md, so a round that followed the instruction died as 'finished without required output file(s)', the brake's decision text was buried under a generic failure.md, the report said 'could not produce a passing fix', and the job left a red review-address check that the next scan counts as new feedback. Observed on #9222 rounds 6/7.

The handoff becomes a first-class verdict end to end: run-agent.mjs honors an agent-written handoff.md (with no fix verdict) as a graceful exit the way it already honors failure.md, and shields it from the API-error retry reclassification; the verification gate reports outcome=handoff for a no-commit round with a handoff and no failure.md; finalize lets handoff pass without failing the job; the report step runs for this outcome, posts the handoff note with the eval marker (watermark advances — the feedback is consumed as evaluated), and names the stop honestly instead of reporting it as a failed fix. The skill now tells the agent exactly which file to write when the brake fires. A coexisting spec output still outranks the handoff, and failure.md coexistence keeps the failed classification, so crash paths are unchanged.

* fix(autofix): align the handoff outcome's consumers and pins with its contract (#9297)

Review found the new handoff outcome breaking two pinned helper tests
(stale breaker-headline wording, unclassified headline in the fleet-shepherd
contract test), misreporting handoff rounds in the status-comment finalize
step, and leaving the whole handoff chain unpinned against mutation.

- Update the breaker headline pin to the PR's reworded headline.
- Classify the handoff headline as transient in the shepherd contract test
  and drop its "AutoFix stopped" prefix so the shepherd's terminal-only
  REASON regex cannot capture a transient stop (the shepherd workflow itself
  stays outside this round's footprint).
- Include handoff in the Finalize-status published-report branch.
- Give deliberate stops their own takeover-digest census bucket instead of
  the residual crash/infra bucket (EN + ZH).
- Neutralize :: workflow commands at the two new handoff echo sites.
- Use the runner's non-empty missing() convention for handoff.md so an
  empty file cannot read as a verdict in one layer and not the other.
- Correct the run-agent.mjs precedence comment: when a handoff coexists
  with a spec output, the gate (handoff branch first) decides the round,
  matching the documented "handoff + no-action -> handoff" contract.
- Pin the handoff chain where its siblings are pinned: finalize replay,
  POST_HANDOFF replay, mark/headline replays, the gate's no-commit decision
  table, the stub-runner handoff/empty/API-error cases, the report-step
  if-clause, and the census needle-to-emit cross-pins.

* fix(autofix): classify a no-commit handoff before the gate's structural checks (#9297)

Review proved the new handoff classification unreachable exactly where
the brake fires: the structural pre-checks (core rebuild, settings
schema, contracts) judge the PR's own diff and reject before the
no-commit fork, and the growth brake fires on precisely the red PRs
whose diff trips them. A compliant handoff (no commit, only handoff.md)
then classified as a retryable failure, so the repair pass deleted
handoff.md and could commit against the brake's explicit stop — the
self-feeding loop the handoff exists to prevent. Reproduced with the
real gate script: schema-check-fail + no-commit handoff exited 1 with
no outcome=handoff.

Move the no-commit handoff classification above the structural checks
(right after the failure.md exits, which keep their precedence). A
handoff claims nothing — acted=false, deferred to a human — so the
checks' false-no-action rationale does not apply, and the retryable/
repair machinery must never engage on a round the brake told to stop.
The no-op fork reverts to no-action-only classification.

- Add a gate test: stale schema + no commit + handoff.md classifies
  outcome=handoff, exit 0, no retryable (fails on the pre-fix gate).
- Pin the handoff-note :: workflow-command neutralization in both
  layers (the gate's sed and the runner's replaceAll), which review
  showed were surviving mutations.

* fix(autofix): reject a no-commit handoff written over a dirty workspace (#9297)

* fix(autofix): report a dirty-handoff rejection honestly, not as a failed fix (#9297)

* fix(autofix): reject a handoff written beside a round commit, non-retryably (#9297)

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

* fix(ci): classify the committed handoff shape as its own non-retryable outcome

R7-1 on this PR: a round that HAS a commit beside handoff.md skipped both brake-violation guards (clean tree misses the dirty guard; committed ref misses the no-commit branch) and fell through to the structural checks, where reject_fix defaults to retryable and the repair pass deletes handoff.md and may commit again against the brake's stop.

Classify it before the structural checks under its own outcome committed_handoff, sibling of dirty_handoff: non-retryable, its own honest report headline (reusing dirty_handoff's wording would claim nothing was committed when a commit exists), listed among the report-publishing outcomes in the status classifier, and never routed through finalize's pass list. Pins updated in the same pass: the committedWithHandoff gate case now expects committed_handoff with no retryable, the shepherd contract test classifies the new headline as transient (loop stays engaged), the status-classifier pin names all five outcomes, and the handoff-contract gate test gets an explicit subprocess budget (eight fixture arms outgrew the 5s default).

* fix(ci): count committed-handoff rounds in the milestone census rejected bucket (#9297)

* fix(autofix): publish brake violations green and preserve handoffs across crashes (#9297)

Two Critical review findings on the handoff output contract.

Brake-violation rounds (dirty_handoff / committed_handoff) ended with a
red review-address check: the eval marker stamps ts=NEWEST, strictly
before the check completes, and the scan counts failed checks completed
after the watermark — including this workflow's own review-address
checks — as new feedback. The next scan re-selected the PR and burned a
full agent round on the item the posted headline promised not to retry,
once per violation. Admit both outcomes to the green finalize arm the
way the clean handoff already is (the diff's own comment names this
self-feeding loop as the reason handoff went green), and key the report
step's routing and POST_HANDOFF trigger on the outcomes themselves so
the green rounds still publish their honest headline, handoff note, and
eval marker instead of going silent.

A crash, budget kill, or loop guard after the agent wrote handoff.md
synthesized a failure.md that shadowed the note: the gate reads
failure.md first (outcome=failed), the report preferred it, and the
timeout sentinel re-handed the item the brake stopped. Preserve the
agent-written handoff in the crash branch (exit 0, mirroring the
agent-written-failure.md arm), and never let writeHandoff overwrite a
non-empty agent verdict.

Both findings reproduced against this commit's verbatim code before
fixing: the case/jq replay showed the violation check red and counted
as new feedback, and a stub run showed the synthesized failure.md
shadowing the handoff. New behavioral tests fail pre-fix and pass
post-fix.

* test(ci): give four subprocess-heavy replays explicit budgets

The milestone digest, stale-duplicate revalidation, deny-by-default footprint, and recoverable-API-render tests spawn multiple bash replays of the real workflow/gate scripts each; the files those replays parse grew with this PR's handoff chain, and all four outgrew the 5s default (each verified to pass with an explicit 30s budget, matching the suite's convention for subprocess-heavy tests).

* fix(ci): mirror the handoff outcome consumers into the recovery clone (#9297)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-20 08:32:43 +00:00
易良
0baaec2b32
chore(ci): Drop NPM_TOKEN in favor of npm Trusted Publishing (#9552)
* chore(ci): Drop NPM_TOKEN in favor of npm Trusted Publishing

* chore(ci): Pin npm 11 for Trusted Publishing in release jobs

* test(ci): Cover Trusted Publishing requirements
2026-08-20 08:31:41 +00:00
易良
48b30647d0
refactor: centralize cross-package contracts (#9497)
* refactor: centralize cross-package contracts

* fix(build): harden cross-package contract checks

* docs(core): clarify sub-session prompt limit scope
2026-08-20 06:24:41 +00:00
易良
b219e3a716
chore(ci): Add --provenance to npm publish and id-token permission (#9532)
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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* chore(ci): Add --provenance to npm publish and id-token permission

* test(scripts): expect --provenance in npm publish step assertion

PR #9532 adds --provenance to every npm publish in the release pipeline.
Update the workflow-pinning test to match the new command so the helper
test suite stays green.
2026-08-20 05:21:02 +00:00
Shaojin Wen
3b3818db87
fix(ci): keep qwen-autofix.yml under GitHub's 500 KB start-runs limit (#9517)
GitHub does not start runs for a workflow file larger than 500 KB (512,000
bytes) and reports nothing when it stops. qwen-autofix.yml crossed that line
on 2026-08-19 at 512,782 bytes: schedule ticks stopped firing, every
workflow_dispatch sat "queued" forever with zero jobs and could not be
cancelled, and issues/issue_comment went quiet — while pull_request_review
runs kept succeeding, because a PR event resolves the workflow from the PR's
own branch and those carry older, smaller copies of this file. The loop
therefore looked half-alive and stayed dark for a day.

Move 75 long comment blocks (1,326 lines) verbatim into a sibling design
record, .github/workflows/qwen-autofix.md, leaving each block's opening lines
plus a `qwen-autofix.md#af-NNN` pointer where it sat: 518,055 -> 426,437
bytes. No executable line changes — the YAML parses to an identical document
outside `run:`, every `run:` script still passes `bash -n`, and the only lines
removed anywhere are comments. Steps that are duplicated verbatim across jobs
share one pointer so they stay byte-identical.

Add .github/scripts/check-workflow-size.sh (gate at 470,000 bytes), wired into
CI on every profile: a .github-only PR classifies as `github_ci_only` and
skips the `full`-only checks, which is exactly the PR that can trip this.
Tests pin the gate, every workflow's size, and pointer/section symmetry.

Delete qwen-autofix-recovery.yml. It was cloned during the incident on the
theory that the workflow ENTITY was wedged, but it carried the same oversized
file, so its dispatches queued identically and its schedule never fired.
2026-08-20 01:56:45 +00:00
Shaojin Wen
133cf8bfcf
refactor(cli): consolidate shared helpers ahead of the legacy audit skill (#9345)
* refactor(cli): consolidate shared helpers ahead of the legacy audit skill

Move the pieces the upcoming /audit skill needs out of command-group
ownership so no skill imports across command groups:

- the findings schema moves from commands/review/ to cli/src/utils/ as-is;
  every review consumer imports it from the new home, and the stale-bundle
  digest, bundle-asset list, and artifact comment track the move
- safeTarget (traversal-safe slug) and tokenizeArgs (quoted argument
  splitting) lift to cli/src/utils/paths.ts and shell-args.ts, with
  review's copies re-exporting/redirecting
- the two private git check-ignore copies (review test-plan, team memory)
  consolidate into one fresh-by-default helper in core utils; the memo
  stays caller-side so a remedy re-check observes the flip

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

* fix(cli): restore review ledger machinery dropped by the consolidation

The shared-helper consolidation silently reverted three behaviors PR #9175
added to compose-review, against the PR's stated "no behavior change"
intent: the unreviewed-dimension anchor exemption (scopeUnproven /
dimensionGapsAreDepthOnly / isNonDiffDimensionGap), the LEDGER_MAX_ROUND
stamp clamp, and the bilingual budget-stop phrase splice. Restore them with
the tests that pin them; SKILL.md, ledger.ts, and deadline.ts still
document all three.

Also harden the new helper tests:
- safeTarget: the deep-path fixtures now share a flattened prefix longer
  than the kept window, so a truncation-only slug (no digest) collides
  instead of shipping green.
- isGitIgnored: each GIT_* scrub arm now carries a discriminating fixture
  (three arms previously passed with their scrub line deleted), and every
  foreign git init scrubs ambient repo-placement selectors.

* fix(core): scrub git config-injection channels from the ignore probe

Ambient GIT_CONFIG_COUNT (inline KEY/VALUE injection) and
GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM (config-file redirects) can aim
core.excludesFile at a foreign rules file and flip the probe's verdict
for the -C worktree — the same leak class the existing selector scrubs
close. Measured on the pristine probe: both channels turn a
not-ignored path into an ignored one.

Also pin the guards the review found unpinned: the two config
channels, the `--` separator for dash-leading paths, the timeoutMs
wiring, safeTarget's hash-of-original-target property, and
tree-existence of the lifted review helpers.

* fix(core): scrub pathspec-magic env channels from the ignore probe

* fix(core): drop the whole GIT_* env family from the ignore probe

Two more leak channels surfaced on the probe's per-variable scrub list:
GIT_ICASE_PATHSPECS (the fourth pathspec modifier — ambient, makes
check-ignore reject every pathspec with exit 128, which the catch reads
as not-ignored) and GIT_CONFIG_PARAMETERS (the inline -c channel git
itself uses to propagate config to children — ambient, can aim
core.excludesFile at a foreign rules file). Measured through the real
function: the first flips a genuinely ignored path to not-ignored, the
second flips a not-ignored path to ignored.

Since the channel list grew by one leak per review round, drop the
whole GIT_* family instead of enumerating, and close the system config
tier explicitly (GIT_CONFIG_NOSYSTEM=1) so host policy in
/etc/gitconfig can no longer answer for the -C worktree — that ambient
dependency also made the config-redirect arm red on any host whose
system config matches the probe path. Pin the icase member, the
PARAMETERS channel, the default 5 s deadline (previously unpinned), and
add a lower timing bound to the caller-deadline arm so it cannot pass
vacuously when the shim is not executable.

* fix(cli): make the safeTarget slug space prefix-free at the dash boundary

Review's cleanup sweeps .qwen/tmp/ by qwen-review-<slug>- prefix. A slug
that itself carried '-' — natively (pr-6771 vs pr) or via the truncation
join — could extend a shorter slug, letting one target's cleanup delete a
DISTINCT target's artifacts (R8-7). The truncation branch this PR carries
newly lands deep targets inside the cap instead of dying ENAMETOOLONG,
which turned the latent collision live.

Drop '-' from the slug alphabet entirely (dashes flatten like separators)
and join the truncation digest with '_': with '-' out of every slug, the
qwen-review-<slug>- boundary is unambiguous by construction — no slug can
start with another slug plus '-'. prev-ledger side files keep their
hardcoded dashed name on both writer and reader, untouched by the slug.

Tests pin the prefix-free property (short-vs-short, short-vs-truncated)
and the fixture names follow the new slugs.

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

* revert(cli): restore safeTarget byte-identical to the pre-lift behavior

Reverts the slug alphabet change (539495226d) and drops the deep-target
truncation branch, per review round findings R12-1 and R8-7:

- R12-1: flattening dashes renamed only one side of review's two-sided
  artifact-naming contract — bundled-skill templates, composed names,
  prev-ledger and brief/report producers hardcode the dash spelling, so
  the bypass-audit tripwire would silently skip and the cleanup sweep
  would leak ~15 dash-form artifacts per review.
- R8-7: the prefix-sweep hazard only exists because of the truncation
  branch this lift carried; main's safeTarget has no truncation, so a
  behavior-preserving lift must not add it. Deep-target support belongs
  in a follow-up paired with the sweep-side structural fix it needs.

safeTarget is now byte-identical to the pre-lift implementation (moved,
not modified); tests pin the dash spelling and the leading-strip rule as
they behave on main. Full review suite green (5022).

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 <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-19 14:53:44 +00:00
易良
b5577b7d11
fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel (#9202)
* fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel

Normalizer-classified unrecognized_event / unrecognized_session_update debug events no longer enter transcript blocks[]: they are mirrored onto a capped unrecognizedDiagnostics sidechannel instead. This stops them from finalizing a streaming assistant/thought block (which dropped a following assistant.usage frame) and from consuming the maxBlocks budget (which let repeated noise evict real conversation content). malformed_payload diagnostics and client-dispatched debug events keep their existing block semantics.

* fix(sdk): align browser bundle budget

* fix(sdk): close the sidechannel review round (#8823)

- export the sidechannel API through the daemon barrel
  (selectUnrecognizedDiagnostics, UNRECOGNIZED_DIAGNOSTICS_LIMIT,
  DAEMON_UI_UNRECOGNIZED_DIAGNOSTIC_REASONS + types) and pin the
  reachability in daemon-public-surface.test.ts
- restore the MAX_TEXT_BLOCK_LENGTH cap on sidechannel text, mirroring
  truncateText exactly (suffix fits within the cap)
- ship the unrecognized reason subset as a runtime const array and route
  by membership, so a new reason cannot fall through to appendStatusBlock
- copy the correlation fields createBase stamps (promptId, sourceRecordIds,
  branchRecordId, originatorClientId) onto sidechannel entries; drop the
  dead source/data switches
- un-fuse the budget-history comment chain in scripts/build.js
- update docs/developers/daemon-ui for the split routing
- tests: full entry shape, text cap, block-path debugReason counterpart,
  and a webui malformed_payload interleave sibling so the #7012
  flush-before-guard keeps a discriminating stimulus

* fix(sdk): address round-2 sidechannel review for #8823

- build.js: bump daemon browser bundle budget 191KB -> 192KB
  (195,591 bytes measured > 195,584 cap; build failed at head)
- webui: narrow the observer-mode debug guard so unrecognized_*
  diagnostics reach the reducer sidechannel; only block-path debug
  events are dropped
- webui: merge history-store unrecognizedDiagnostics in
  applyTranscriptHistory so paged-back sessions keep diagnostics
- transcript: extract truncateTextAtLimit shared by the block and
  sidechannel truncation paths
- transcript: reset unrecognizedDiagnostics on rewind alongside the
  sibling per-turn state resets
- types: rename DaemonUnrecognizedDiagnostic.receivedAt to
  clientReceivedAt (matches the sibling block projection)
- tests: reason-prefix conformance pin, rewind reset, narrowed guard,
  history pagination merge

* fix(webui): avoid flushing sidechannel diagnostics

* fix(sdk): preserve diagnostics across rewind

* fix(webui): dedupe sidechannel history records

* fix(webui): align the paging sidechannel test with the normalizer keys

The paging test added in e6b40e5c failed deterministically (webui
suite red, CI Test job red) for two reasons:

1. The fixtures stamped only _meta['qwen.session.recordId'], but the
   SDK normalizer's extractSourceRecordIds reads
   _meta.qwenTranscript.sourceRecordIds — no sidechannel entry ever
   carried sourceRecordIds, so the dedupe assertion could not pass and
   the new displayedRecordIds loop was never exercised by a passing
   test. Stamp BOTH keys, matching production replay frames
   (acp-bridge buildUpdateMeta) and the sibling dedupe test.
2. Cap arithmetic: LIMIT-1 live entries + 2 fresh history entries =
   LIMIT+1, so the newest-wins slice evicted record-old-1 which the
   test asserted present. Emit LIMIT-2 live events so the post-merge
   total lands exactly on the cap.

Also correct the post-merge index assertions: history entries come
first (old-1, old-2), then the deduped-once live overlap, then the
first live mystery event. Suite 506/506, eslint + prettier clean.

* fix(sdk): raise diagnostic sidechannel bundle budget

* fix(sdk): raise the daemon browser bundle budget to 198KB and pin the diagnostics selector

- The sidechannel routing + selector cost ~1037 B over the 197KB cap
  (bundle measured 201893 B), failing the browser-bundle size gate; bump
  MAX_DAEMON_BROWSER_BUNDLE_BYTES to 198 * 1024.
- Fold the rebase-residue 190→191→192 KB ledger entries into the accurate
  190→195→196→197→198 lineage so the next bump has one canonical history.
- Add a behavioral pin for selectUnrecognizedDiagnostics: it must return
  the routed sidechannel itself (toBe), discriminating a `return []` or
  shallow-copy regression that the typeof-only surface test cannot see;
  flip-verified.

* fix(sdk): reset the user pointer on sidechanneled diagnostics, share the routing predicate

appendUnrecognizedDiagnostic left activeUserBlockId untouched while the
replaced appendStatusBlock path reset it for every non-user block; a
later mergeable user.text.delta with no promptId stamp (e.g. a peer
client's $ <cmd> echo) then appended onto the earlier user block
across the diagnostic, collapsing two user turns into one and skewing
rewindTranscriptToUserTurn's kind==='user' turn indexing. Keep the
reset (assistant/thought pointers stay untouched, the point of the
sidechannel); witness test flip-verified red without the one-line reset.

Also export isUnrecognizedDiagnosticReason from types.ts next to
DAEMON_UI_UNRECOGNIZED_DIAGNOSTIC_REASONS and call it at all three
routing-guard sites (reducer, provider flush condition, provider drop
filter) so the #7012/#8823 guard pair classifies every debug event
against one source instead of three hand-written copies.

* fix(ci): prevent bite harness SIGPIPE

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-19 14:38:00 +00:00
Heyang Wang
5003ab3c7f
feat(web-shell): add transcript contract prevalidation (#9388)
* test(web-shell): add transcript contract prevalidation

Freeze reproducible evidence for current transcript paths before any
VS Code or HTML export production migration.

- Add versioned fixtures, closed export schema, and capability gates
- Probe direct-daemon and ACP identity under partial history prepend
- Preserve raw adapter semantics and full write_file Turn Output diffs
- Document the two-MR architecture, security constraints, and blockers

* fix(web-shell): harden transcript prevalidation gates

Make the evidence-only contract suite enforce the review assumptions it
documents while preserving the existing runtime transcript behavior.

- Run the contract suite in the required no-AK integration job
- Fail closed on ambiguous identity probes and deduplicate gate kinds
- Enforce manifest, hash, export safety, and renderer version boundaries
- Cover visible transcript text and stable Desktop packaging semantics
- Record the complete PR comment evaluation and verification outcome

* fix(web-shell): close transcript prevalidation gaps

* fix(web-shell): remove brittle Desktop wiring probe

Keep transcript contract prevalidation at the evidence level it can
actually prove. The previous source-text assertion could both reject
equivalent formatting and pass unreachable packaging code.

- Remove the Desktop script parser and its false behavioral claim
- Mark installed-artifact verification as deferred to Desktop smoke tests
- Clarify MR1 matrix, CI wiring, and provenance evidence boundaries
- Refresh the hash-locked capability matrix fixture

Note: This does not change Web Shell or Desktop production behavior.

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
2026-08-19 14:13:12 +00:00
Shaojin Wen
1eb8a0c7f8
feat(review): wire --resume through /review and the review run subcommand (#9153)
Surface the local resume feature (PR #9092) on the paths a user reaches
it from:

- `parse-args.ts`: `/review <pr> --resume` parses to
  `resume: { requested, effective }`, gated on PR targets (a local
  review's diff comes from a live working tree with no stable interrupted
  state). A `--resume` on a non-PR target warns and is inert.
- `run.ts`: the `qwen review run` headless wrapper takes `--resume` and
  passes it through to the `/review` prompt.
- `SKILL.md` Step 1 gains a "Resuming an interrupted run" branch: on
  `resume.effective`, append `--resume` to `fetch-pr`, branch on its
  `resumed` JSON, run `recover-findings`, re-enter the audit loop at
  `latestReverseAuditRound + 1`, and read the restart bound back from
  `restartsSpent`.
- `DESIGN.md` / `docs`: document resume as a LOCAL convenience.

The CI review workflow runs FRESH — it does not pass `--resume`. A CI
attempt runs no-sandbox on the reviewed PR's own code and its worktree is
deleted the moment it exits, so there is no interrupted state on disk for
a retry to continue; a resume would refuse `worktree-gone` and start over
anyway. The retry loop and its test assert the fresh-only wiring.
2026-08-19 05:13:09 +00:00
qqqys
b6e93d27ad
fix(autofix): paginate review threads instead of reaching the oldest 100 (#9390)
* fix(autofix): paginate review threads instead of reaching the oldest 100

`resolve_and_reply_threads` fetched `reviewThreads(first:100)` with no
pagination. GitHub returns review threads in ASCENDING creation order, so a
single page is the OLDEST hundred — on a long-running PR, precisely not the
threads the current round is answering.

Both blocks downstream map an inline-comment id to its thread. A thread past
the page is absent from `THREADS_JSON`, so an implemented Critical is never
resolved and reads as still open, and a declined finding's reply is answered
by silence. Those are the two outcomes the function exists to prevent.

Live: 8 of the 22 open takeover PRs exceed the cap. #8403 carries 1256
threads, so one page reached 8% of them — and all 1256 are unresolved.

The code already detected this: it requested `pageInfo{hasNextPage}` and
emitted a `:⚠️:` when true. It just never fetched the next page.

Use `gh api graphql --paginate`, which is built for exactly this shape, and
slurp its node stream into the flat array both blocks already expect. On
#8403 that is 13 requests in ~10s.

A partial fetch is USED rather than discarded: losing twelve good pages to a
rate limit on the thirteenth would resolve nothing at all, so the failure is
announced and the threads in hand still map.

One residual stays open and is now announced rather than implied: a thread
carrying more than 100 comments still truncates, so a comment past that page
is unmapped and each block falls back to the id as given. No thread in the
live pool comes close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(autofix): keep the pagination failure's reason, and pin both warnings' absence

Round 1's three Suggestions, all on the partial-fetch path this PR adds.

R1-1: `2> /dev/null` on the paginated fetch discarded gh's stderr — the only
text saying WHY pagination stopped. The warning announced THAT it stopped, so
the oncall could not separate a transient rate limit (back off) from an
expired PAT (rotate) or a network failure without re-running the ~13-request
query by hand. Captured to `${WORKDIR}/threads-fetch.err` with the pattern
already used elsewhere in this workflow, and its tail folded into the warning.

R1-2: the outer thread pagination silently depends on the inner `comments`
pageInfo NOT asking for `endCursor` — gh's paginator adopts the first pageInfo
carrying both fields. The `Residual:` note actively invited a maintainer to
close that residual by adding it, which would hijack the thread-page cursor
and stop after page one at exit 0 with no warning, silently restoring the
oldest-hundred bug. Documented as load-bearing, in the comment block above the
fetch rather than inside the query literal — a `#` line there is transmitted.

R1-3: both new warnings were asserted only in the positive, so a mutation
making either unconditional shipped green. Added the clean-run absence
assertions this file's own convention calls for (321 `not.toContain` uses),
and the gh stub now writes a reason to stderr on failure so the folded-in text
is assertable.

Verified: qwen-autofix-workflow 178 passed. Mutation-checked — restoring
`2> /dev/null` and making the pagination warning unconditional each fail a
test. The one remaining failure (`behaviorally replays the stale-duplicate
revalidation`, 5s timeout) is identical with these changes stashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(autofix): keep gh's error body out of the slurped review threads

Round 2 of the review on #9390 found the paginated review-thread fetch
poisons its own output on a partial page, and asked for two clarifications
around it.

R2-C (Critical) — on a failing page gh skips `--jq` and appends that page's
raw response body (a rate-limit message, or a GraphQL error envelope) to
stdout after the good nodes. The unfiltered `jq -s '.'` slurped it as an
extra element, and both consumers below iterate `.comments.nodes[]` over
every element, so the first one exited 5. This step runs under errexit, so
that aborted 'Push and report' AFTER a good push had landed — the report and
the markers were skipped and the job failed. That contradicts the two
invariants the block documents: a resolve failure must never fail a good
push, and a partial fetch is used rather than discarded. The slurp now keeps
only thread-shaped documents.

R2-1 — the comment block warned against adding `endCursor` to the inner
`comments` pageInfo, but the outer pageInfo's field ORDER is load-bearing for
the same reason: gh's cursor scanner carries its flags across pageInfo
objects and breaks at the first one yielding both fields, so alphabetizing to
`pageInfo{endCursor hasNextPage}` stops after page one just as silently. Said
so at the query, and at the test pin that goes red on a reorder, so the pin
is understood rather than bumped.

R2-2 — the stderr fold dropped the `tr '\r\n' '  '` that its ten sibling
sites apply. Actions parses workflow commands line by line and gh's
secondary-rate-limit stderr spans two lines, so the annotation kept only the
first — cutting off the words that separate a back-off from a credential
rotation.

Verification: `scripts/tests/qwen-autofix-workflow.test.js` 179/179; yaml
parses; eslint and prettier clean. Mutation-checked all three: reverting the
slurp filter fails the resolve arm with exit 5 (expected 5 to be 0),
reordering the outer pageInfo fails the field-order pin, and dropping the
`tr` fails the folded-reason arm on the second stderr line.

* docs(autofix): correct the field-order comment's mechanism (#9390 R3-1)

The comment explaining why `pageInfo{hasNextPage endCursor}` order is
load-bearing described the silent stop as happening with the carried
`hasNextPage` "already true" from the last inner page. That cannot
produce the symptom: gh's `findEndCursor` returns a cursor only `if
hasNextPage`, so a carried true would keep the walk going.

The real mechanism is the opposite one. The scanner carries its flags
across `pageInfo` objects and breaks at the first point both have been
seen; under `pageInfo{endCursor hasNextPage}` that break lands on the
outer `endCursor` while `hasNextPage` still holds the last INNER page's
value — almost always false, since thread comment pages rarely truncate
— and the outer page's own `hasNextPage` is never read. gh returns no
cursor and the walk stops after page one, exit 0 and silent.

Reworded in both places the clause was copied to: the workflow comment
and the field-order pin's comment in the test. No assertion, no shell,
and no query text changes; `pageInfo{hasNextPage endCursor}` and the
test that pins it are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(autofix): keep gh's thread-fetch stderr off a predictable WORKDIR path

R4-1 (Critical, #9390): the review-thread pagination wrote gh's stderr to
`${WORKDIR}/threads-fetch.err` and read it back with `tail -c 300`, both
without a file-type guard. WORKDIR (`/tmp/autofix-review-<pr>`) is
bind-mounted read-write into the agent docker sandbox, and the round that
just finished ran branch code inside that sandbox, so the name is
attacker-chosen by the time this step runs.

A planted FIFO makes bash block on the O_WRONLY open before gh even execs,
and the only reader is the `tail` that runs strictly after gh returns — so
the step hangs to the job timeout with the push already landed, losing the
report comment and the round markers. That breaks the invariant this block
states for itself: a resolve failure must never fail a good push. A planted
symlink instead turns the redirect into a truncate/write against the link
target and the tail into a 300-byte arbitrary-file read folded into a public
`:⚠️:`.

Route the stderr through a fresh `mktemp` regular file instead, matching the
`gh api user` checks elsewhere in this workflow, and remove it afterwards.
The diagnostic is unchanged: the warning still carries gh's own reason, which
is the only text separating a transient rate limit from an expired PAT.

Test: plant a symlink at the old path, run the block through a failing fetch,
and assert the target's bytes are neither overwritten nor folded into the
annotation; plus assert the named path is not created at all. Mutation-
verified — restoring the `${WORKDIR}` redirect turns the canary assertion red
(`expected 'threads-fetch stub failure' to be 'CANARY-MUST-SURVIVE'`). The
FIFO half cannot be written as a plain assertion because the pre-fix code
hangs rather than fails; the same "named path is never opened" property
defuses it.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 01:32:46 +00:00
Shaojin Wen
bc2d205d29
refactor(ci): simplify the review checkout self-heal back to wipe-and-retry (#9327)
* refactor(ci): simplify the review checkout self-heal back to wipe-and-retry

#9220 fixed a real incident (a corrupt persisted workspace made seven
review jobs fail checkout on the same missing SHAs), but eight review
rounds grew the heal step from ~15 lines into ~60 lines of path-guard
layers (realpath canonicalization, two trailing-slash strip loops, a
denylist case, a RUNNER_WORKSPACE allowlist) plus ~450 lines of tests
pinning their mutation resistance.

Every removed layer defended against a mangled GITHUB_WORKSPACE. That
variable is set by actions/runner; anything that could mangle it — a
compromised runner, a step writing GITHUB_ENV — already executes
arbitrary code on the machine and needs no wipe to do damage, so the
guard cannot defend against the only actor able to trigger it. The
realistic contract is the :? guard: fail loud on a dropped variable.

Kept and still pinned by tests: the pool wipe idiom, the sudo fallback
leg (exact argv), the never-fail exit contract with named survivors,
the identical retry checkout, and the continue-on-error invariants.
Also dropped with the guards: the GNU-only realpath flag and its
host-probe test machinery.

* test(ci): pin the runner-owned GITHUB_WORKSPACE premise before the workspace wipe

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

* test(ci): seal the live override channels into the review wipe step

* fix(ci): refuse a redirected workspace and pin the clean-wipe silence

Addresses the two doudouOUC findings on the simplified heal:

- The wipe now validates the filesystem OBJECT at $WS, not just the
  string: find -P does not descend a symlinked start, so a redirected
  workspace logged 'wiped for a clean retry' while deleting nothing,
  and the secret-bearing review step would then run through the
  redirection. Refuse loud on a symlink or non-directory — POSIX-only,
  no false-positive surface (a legitimate workspace is always a
  runner-created plain directory), and it pins the only sudo-escalated
  wipe in the pool to a validated target.
- The clean-wipe silence branch was unpinned: the reviewer's minimal
  mutant (dropping the if/fi pair) shipped an empty-list survivor
  warning on every heal with the suite green. The clean-wipe test now
  asserts the success annotation and the absence of the survivor
  warning; both mutants verified red.

* test(ci): seal the wipe step's surviving override channels

* test(ci): seal the wipe's surviving override channels, pin its signals

Addresses the open review findings on the simplified heal:

- The seal's premise covered declarative env, $GITHUB_ENV/$GITHUB_PATH
  run writes, and the pre-wipe action set, but three channels passed it
  unchecked: a wipe-step `shell:` or workflow/job `defaults:` wrapper
  re-targets the environment at exec time; SHELLOPTS rides the same
  bash-startup family as BASH_ENV/ENV yet sat outside the dangerous
  name class; and ACTIONS_ALLOW_UNSECURE_COMMANDS re-enables the legacy
  ::set-env:: / ::add-path:: spellings the run-text scan did not match.
  Each channel was reproduced green against the old seal (mutant probe)
  and now turns it red.
- The both-legs-fail test now also pins the else-branch "could not
  wipe" warning, and a dedicated test pins the `[ ! -d ]` refusal for a
  nonexistent workspace — the plain-file test alone still passes a
  guard mutated to `[ -f ]`.
- The non-sudo wipe leg keeps its stderr: the 2>/dev/null discarded
  exactly the diagnostics oncall needs when the wipe fails, and the
  sudo leg already ran unsuppressed.

* fix(ci): refuse workspace wipe through symlinked path components (#9327)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-19 00:56:21 +00:00
Shaojin Wen
846fc05461
feat(ci): post autofix failure-path handoff comments bilingually (#9386)
* docs(autofix): design bilingual failure-path handoff comments

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

* feat(ci): post autofix failure-path handoff comments bilingually

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

* fix(ci): pin bilingual handoff sanitization by content, escape withdraw excerpt

Address review R1-1/R1-2/R1-3:

- Escape + iconv the issue-lane withdraw failure.md excerpt, the one
  publish site without `<!--` escaping: a failure.md quoting an HTML
  comment whose closer sits past the 1500-byte cut opened an
  unterminated comment that swallowed the new 中文说明 block (R1-3).
- Widen the escape-site count test to the multi-`-e` sed form
  (9 -> 12 sites) and pin the full zh sanitization pipeline per-site
  on both lanes; dropping the `<!--` expression from either zh site
  or a tag substitution from the withdraw site now fails (R1-1).
- Pin EN/ZH correspondence for every non-empty assignment site of
  HEADLINE/CAUSE/LAST_FIX/GATE_CLAUSE/IDLE_CLAUSE/REMEDY: count-only
  pins let a swapped adjacent HEADLINE_ZH pair pass all tests (R1-2).

All four mutation witnesses from the review now fail the suite
(verified locally: probe each mutation, expect red, restore).

* fix(ci): close the bilingual handoff review gaps (R2/R3)

Workflow fixes:
- Neutralize :: in the issue-lane run-log dump loop (agent-written
  files on step stdout parse as workflow commands; the PR-lane twin
  already did this) — R2-1.
- Extend the wrapper-defense substitutions (<details, </details,
  <summary) to the three excerpt sites that only escaped <!-- and
  now sit above the new 中文说明 wrapper: API_ERROR_DETAIL (flows into
  HEADLINE_ZH inside the wrapper) — R2-3; the PR-lane DETAIL_FILE
  excerpt (address-summary/no-action files are mandated to END with
  their own <details> tail, so a cut-straddling tail leaves a live
  severed opener) — R2-4; the withdraw failure.md excerpt — R3-1.
- The withdraw comment's 中文说明 block now renders unconditionally
  with a translated REASON (REASON_ZH per branch), mirroring the
  PR-lane headline floor: crash shapes where run-agent.mjs writes
  failure.md itself no longer degrade to zero Chinese — R3-2.

Accepted and documented (design doc §5): fence-token severance across
the byte cut — render-only, markers parse raw, and a balancing
heuristic stays wrong when the cut lands mid-closer — R2-2.

Test pins (each mutation-verified locally): branch-selected zh labels
— R2-5; zh gate-note text + condition + position — R2-6; failure.zh.md
membership in all four dump loops plus the issue-lane :: sed — R2-8;
the ZH_DETAIL guard — R2-9; full-line rm -f pins on the three pre-agent
cleanup sites — R2-10; the BODY append shape — R3-3; wrapper internal
ordering — R3-4. Design doc §2 reconciled with §5 on the no-detail
fallback sentence — R2-7.

* fix(ci): close the R4 review gaps (case-insensitive tag defense, pin gaps)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-19 00:17:35 +00:00
Shaojin Wen
f0dcdfc157
feat(triage): add a deterministic flakiness gate to sandboxed verification (#9130)
* feat(triage): add a deterministic flakiness gate to sandboxed verification

Closes #9125. PR #9086's ~50% mtime-assertion flake passed every automated
layer because each executed the changed tests exactly once — a coin flip a
single green run cannot distinguish from health. The gate re-runs the PR's
added/modified unit-test files N times (default 5, vars.QWEN_VERIFY_FLAKE_ROUNDS
to override, clamped to 2..10) through the same entry points CI uses and
compares outcomes per group across rounds.

Design constraints, each pinned by a workflow test:

- One-way authority: 'flaky' demotes the published headline (even a trusted
  agent merge-ready); no gate value can raise or soften one. The gate runs
  the PR's own test code, so it can always be neutered — but a gate that can
  only demote is not worth forging.
- Divergence-only signal: a group failing identically every round is
  deterministic (CI owns it) and an environment-sensitive suite must not
  false-positive here; both report informationally, never demote.
- Fail open: the gate is not under -e and every terminal path exits 0 — a
  gate bug reports verdict 'error' instead of taking down the verify lane.
- Honest file list: recorded from HEAD^1..HEAD before install/build hands
  the workspace (and .git) to PR lifecycle code; the gate consumes the
  root-owned recorded list and never re-derives the diff.
- Untrusted text stays out of outputs: summaries are fixed text plus
  counters; PR-controlled paths live in flake-gate.log, embedded through
  the publisher's escaping emit_block.

Job timeout raised 150 -> 175 for the gate's ~25m worst case (15m round
budget checked before each invocation + one 10m-capped in-flight run).

* fix(triage): survive the runner wrapper's -e, per-file gate granularity, hardened log staging

Round-1 review + sandboxed-verify feedback, all seven findings:

- set +e after set -uo pipefail: the runner wraps every run: block in
  'bash -e -o pipefail' and set -uo does NOT clear that inherited -e, so
  the first failing test invocation killed the step — fail-open inverted
  to fail-closed for exactly the flaky/consistent-fail populations the
  gate classifies (verify cells C/D). An EXIT trap additionally converts
  any abnormal ending (set -u death) into the fixed 'error' verdict.

- Per-FILE groups: one runner invocation per changed test file, so a
  consistently failing file can no longer mask another file's run-to-run
  divergence behind a shared exit bit.

- Owning-package resolution: nearest ancestor package.json (nested
  workspaces like packages/channels/base are entered themselves) plus a
  vitest-config probe; unsupported runner families (packages/desktop's
  bun test) and */e2e/* specs are logged out-of-scope instead of being
  mis-run as permanent consistent-fail noise.

- Operands are ./-prefixed before %q, so a checked-in filename beginning
  with '-' (e.g. --config=x) can never be parsed as a runner option.

- Log staging moved to a dedicated always() root step after the agent
  exits — the last write to verify-results/flake-gate.log — and the
  publisher pins that exact path instead of find|sort|head, so an early
  agent abort cannot lose the matrix and agent-era PR code (which owns a
  chowned verify-results) cannot control or shadow what is embedded.

- Detection math corrected: N=5 catches a 50/50 flake with ~94%
  (1 - 2*(1/2)^5), not ~97% — all-pass and all-fail rounds both miss.

- New behavioral suite executes the extracted gate and publisher
  fragments under the production wrapper itself (bash --noprofile
  --norc -e -o pipefail) with scripted per-file P/F sequences: pass,
  flaky-next-to-consistent-fail, consistent-fail, missing-list error,
  out-of-scope n/a, nested-package + leading-dash operand, and the
  seven-value one-way demotion — closing the structural blindness where
  YAML-string tests stayed green while the shipped behavior regressed.

* fix(triage): isolate flake-gate rounds, classify infra exits, widen runner resolution (#9130)

Review-round fixes for the deterministic flakiness gate:

- Reset shared state between rounds (restore tracked files, tear down
  test-user processes, fresh per-invocation TMPDIR) so a deterministic
  test cannot fail on its own residue and fake a divergence (R1-8).
- Classify timeout/signal exits (124, 128+N) as infrastructure, not F
  marks, and report the informational timeout verdict instead of a fake
  flaky (R2-3/R3-2).
- Resolve the vitest runner by owning package + vitest's real config
  list (vite.config.* included), keyed on the package lookup instead of
  a packages/* prefix, so webui and integrations workspaces are re-run
  instead of skipped (R2-2/R3-3).
- Narrow the scripts/tests arm to the pinned config's *.test.{js,ts}
  include set so admitted-but-rejected files are skipped, not mis-run
  into a bogus consistent-fail (R3-11).
- Harden the gate-log staging: kill leftover build-user processes, and
  remove a planted destination entry before copying so a FIFO/symlink
  can neither hang the copy nor redirect it (R1-5).
- Cap the embedded gate log at 10000 chars to keep the assembled
  comment under GitHub's 65,536-char limit (R3-4).
- Record changed files with core.quotePath=false so non-ASCII test
  filenames are not silently dropped (R3-5).
- Behavioral tests: hermetic timeout/pkill stubs (the suite no longer
  depends on GNU coreutils, fixing the macOS red), infra-exit and
  round-reset scenarios, trap-abort fail-open, node --test arm,
  FLAKE_ROUNDS clamping, fixed-shape summary, record-step shape pins.

* test(triage): follow the widened special-file strip into the vitest twin pins

Commit 13708068bd widened the verify-lane artifact strip from symlinks
only to \( -type l -o -type p -o -type s -o -type b -o -type c \) so a
planted FIFO/socket/device cannot hang or redirect the collection — but
two pins in scripts/tests/qwen-triage-workflow.test.js still asserted
the old '-type l -delete' literal and went red (the Test job's only
failures). Update both pins to the full new expression; the intent they
guard (strip present, and AFTER the artifact copy) is unchanged, and the
tmux-side pin keeps the old literal because the tmux lane still uses it.

* fix(triage): build-user round resets incl. pre-round-1, artifact-loss-proof flaky demotion, mechanism-anchored pins

Round-4 review, all 22 findings (2 Critical dual-anchored + 20):

- Round-state reset (Critical): run the reset AS THE BUILD USER — a root
  checkout restores node-mutated tracked files as root-owned inodes that
  later node rounds cannot write (EACCES divergence) — add 'git clean
  -fd' (no -x) so untracked round residue is dropped, and run one reset
  BEFORE round 1 so lifecycle-script mutations cannot make the first
  sample differ from the rest. Behavioral scenarios: untracked residue,
  pre-gate tree mutation (both PPPPP/pass), each red without its fix.

- Staging step: continue-on-error (evidence-copying must not let the
  publisher discard a recorded verdict as 'infrastructure failure'), and
  the order chain (pkill -> dir guard -> mkdir -> unlink dst -> cp) is
  now pinned by index comparison, not presence-only regexes.

- Publisher: the flaky demotion now fires in the artifact-download-
  failure branch too — FLAKE_VERDICT travels via job outputs and does
  not need the artifact — instead of a neutral 'results unavailable'.

- Pins anchored to mechanisms, not adjacency (R2-P1): exact-line record
  assignment (kills ;/& status swallowing and covers the -c form), word-
  based no-re-derivation, line-anchored 'timeout -k 30 600 runuser'
  invocation (comment-proof, also pins the per-invocation cap), build-
  user reset lines, agent/gate if-equivalence.

- Unpinned guards now pinned (R2-P2/P3): ACTIONS_* credential strip, the
  env -u runner-file isolation, whole-env key set (a future secret in
  the gate env must be an explicit test decision), intake extension set,
  record->gate handoff filename, child-env line (CI/heap/TMPDIR).

- New behavioral scenarios: wall-budget expiry via a scripted date stub
  (both timeout branches, pinning rounds_done placement), space-bearing
  filename through %q as one operand, bilingual one-way demotion (the
  collapsed Chinese summary is the one verdict a zh reader sees).

* fix(triage): NUL-delimited gate intake, zero-collection class, front-loaded matrix, hardened resets

Round-5 review, all 28 findings:

- NUL-delimited record end to end (git diff -z, grep -z, gate read -d ''):
  quotePath=false only stops quoting of bytes >= 0x80 — ASCII specials
  (backslash, tab, quote, control chars) stayed C-quoted and silently
  failed the $-anchored line grep with no skip-log entry. The raw diff
  never passes through $( ) (command substitution strips NUL). Intake
  extension set gains .mts/.cts (vitest's default include collects them).

- Zero-collection class ('N' mark): a file the runner's include set
  rejects exits 1 every round with 'No test files found' — publishing
  that as consistent-fail claimed 'deterministic, CI owns it' with both
  clauses false. All-uncollected lands n/a; mixed runs pass with the
  not-collected count in the summary. Faking the marker can only
  SUPPRESS a demotion the PR could already dodge — one-way authority.

- Per-invocation detail moved behind the matrix/verdict: the publisher
  embeds the FIRST 10,000 chars, and failure tails (8 KB each) pushed
  the promised per-round matrix past the cap in exactly the flaky runs
  the demotion points at. Plus a bilingual fallback note when the gate
  log could not be staged into the artifact.

- reset_round_state kills FIRST (a live daemon re-dirties the tree after
  checkout), with SIGKILL + a bounded wait replacing the one-shot TERM.

- Behavioral hardening: runner-injection-env and operand-resolution
  guards baked into the default stub (pins the cd and %q for every arm),
  hostile filenames through the generic and node --test arms, mid-round
  budget expiry, flaky-outranks-timeout, infra-exit amid divergence,
  vitest.workspace.ts entry, exact summary counters, step-summary
  read-back, gate status line in the demotion drive.

- Structural pins anchored to mechanisms: exact NUL-record statements,
  continuation-proof no-re-derivation (plus log/show/whatchanged),
  unset-before-invocation ordering, FLAKE_ROUNDS wired to the repo var,
  inv_tmp lifecycle, record/gate if-pins, flake-before-agent order,
  staging line-anchored order chain incl. both guard halves and the
  guard's rm reaction, DOWNLOAD_OUTCOME wiring.

* fix(triage): close the desktop-app runnability hole and the staging replant race; demote in every terminal branch

Round-6 review (2 Critical + 19 Suggestion; 17 applied, 2 declined with
rationale on-thread):

- Desktop/docs-site exclusion (Critical): packages/desktop/apps/* each
  carry a package.json plus a BUILD vite.config.ts, so the generic
  resolver treated bun-family tests as runnable — 102/319 real desktop
  test files misclassified, published under a false include-set-mismatch
  diagnosis while draining the shared wall budget. Explicit skip arm
  ahead of the generic arm; behavioral scenario pins both trees.

- Staging replant race (Critical): the one-shot pkill lost to setsid
  daemons/continuous forkers, and verify-results stayed node-owned — a
  survivor could swap the staged log for a symlink between cp and
  upload-artifact's link-following enumeration: root-readable-file
  exfiltration into the public comment. The kill now uses the bounded
  survivor wait, and the directory is chown -R root:root before the
  copy, revoking the replant capability regardless of the race.

- A recorded flaky now demotes in EVERY terminal publisher branch:
  cancelled and job-failure used to post the neutral notice while
  needs.verify.outputs still carried the verdict (the download-failure
  branch already honored it). Bilingual, with run-log pointers.

- Dead 'uncollected' counter removed (ncoll already counts N marks).

- Tests: ghost-file and desktop-skip scenarios; behavioral child-env
  guards in the default stub (CI/heap/TMPDIR-under-RUNNER_TEMP);
  invocation-count ground truth for the mid-round budget stop; sixth
  drive field pins the Chinese gate status line; adjacency-pinned record
  statements; anchored credential-strip/wait-loop/publisher pins;
  cancelled/failure/download branch pins; upload transport pins;
  workflow/job-level env emptiness pins; guard then-body pin.

Declined (reasoning on the threads): whitespace-spelling re-derivation
evasions (bounded by one-way authority — re-derivation can only narrow
the gate), and an inv_tmp position pin (superseded by the stub's
behavioral TMPDIR guard, which every scenario now enforces).

* fix(triage): per-sample reset equivalence, 125-127/N-transition classification, intake+staging hardening

Round-7 Critical cluster on the flakiness gate:

- Reset runs before EVERY invocation (not once per round) so file i never
  samples what files 1..i-1 left this round; HOME joins TMPDIR in the
  per-invocation isolation; `git checkout HEAD -- .` restores from the
  root-pinned commit instead of the index; `git clean -ffd` also drops
  nested-.git dirs plain -fd refuses; both git calls gain the lane's
  runner-injection strip and a `timeout -k 30 120` wrapper, and a failed
  reset fails open to the fixed error verdict instead of sampling dirty.
- Exits 125-127 (timeout's own failure modes) classify as infrastructure
  like 124/128+N; a per-file collection-state transition (N next to P/F)
  is divergence, no longer collapsed to pass/consistent-fail.
- Record step: diff-filter gains T (typechange); a grep error (status 2)
  fails the step loudly instead of starving the gate to n/a; the
  owning-package walk hands its result through a variable, never a `$( )`
  capture that strips trailing newlines.
- Staging: chown preserves modes, so the root re-own is completed by
  `chmod -R go-rwx` and a post-revoke sweep before the copy.

Behavioral scenarios pin each defect (all fail pre-round, measured):
per-sample equivalence across residue/nested-repo/staged/HOME classes,
exit-127 infra classification, and N-transition divergence.

* fix(triage): give the gate a root-only home and rebuild the upload tree

Round-7 R7-8 (Critical) and the R1-5 line it supersedes, one root cause:
$RUNNER_TEMP's top level is uid-1000 mode-755 on this pool and the job
container's `node` is uid 1000, so PR-controlled code held OWNER WRITE
on the directory holding every gate working file — and each root-side
consumer followed symlinks. Four probe-confirmed exfiltration routes:
the staging source, the recorded list (swapped for /proc/<pid>/environ,
whose NUL-framed KEY=VALUE is exactly the intake framing, landing
ACTIONS_RUNTIME_TOKEN in the published log), the fixed-path round
output, and the destination entry itself (renameable even after the
in-place hardening, so a kill-race survivor could swap the whole tree
for a symlink farm that upload-artifact follows).

Removing the capability rather than patching each consumer:

- All gate working files (list, log, detail, round output) now live in
  $RUNNER_TEMP/flake-gate, created 0700 root:root by the record step
  (rm -rf first: the entry may be a plant from an earlier run on the
  persistent pool). A directory node cannot enter is one whose entries
  it can neither create, unlink, nor rename — routes (a)(b)(c) close by
  construction.
- The gate verifies that home fail-closed before reading anything:
  not a symlink, a directory, owned by the EFFECTIVE user (root in
  production; keeps the extracted script runnable under a harness), and
  mode 700 — otherwise the fixed `error` verdict, exit 0.
- Staging BUILDS a trusted upload tree in that home instead of hardening
  the agent-era one: kill+wait unconditionally (node can unlink the log,
  which must not skip the rebuild), then copy only regular files with
  --no-dereference out of verify-results, then the authoritative log
  last. The artifact now uploads from the rebuilt tree, so the entry the
  enumeration walks was never in a PR-writable directory — route (d).

The publisher's paths are unchanged (same inner layout). Behavioral
scenario pins the fail-closed refusal; the staging pin chain follows the
rebuild order.

* fix(ci): close flake-gate TOCTOU, reset-integrity, and verdict-discard routes (#9130)

* fix(ci): keep sub-2-round gate stops informational and scrub startup env channels (#9130)

The reset-failure early stop and a mid-run home swap both reached
classification with fewer than two completed rounds — one published
`pass` off a single agreeing round, the other discarded samples that
already encoded a computed flaky, letting a PR dodge its demotion by
renaming the gate home. Both stops now degrade to the informational
timeout verdict under two rounds while an observed divergence still
demotes, and a swap after samples keeps the collected results.

The record/gate/staging blocks also scrubbed only PATH, while the
channels consumed at shell/loader startup stayed live: BASH_ENV and
the LD_* loader channels are now blanked at step env (with a
fail-closed check if the blank loses), BASH_FUNC_* imports are
dropped by a one-shot env -i re-exec whose child marker is positional
(an env sentinel would be forgeable through the same file-command
channel), and the GIT_* family is stripped on the reset and sampling
children. Staging additionally validates the run identity stamped by
the record step, and a verify-results stat race degrades to a skipped
copy instead of aborting before the authoritative log copy.

The count pin covered five of the seven stripped node-run commands,
which is what failed the scripts lane; the job timeout budget now
includes the resets the deadline check runs ahead of (175 -> 190).

* fix(ci): close gate re-exec startup races and stale-evidence paths (#9130)

* fix(ci): harden flake-gate startup decisions and close staging swap windows (#9130)

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

* fix(ci): move the flake-gate home out of PR-writable space; pin git metadata

Rounds 8-16 kept re-reporting one class (R8-1, R8-36, R12-2 and its
re-reports, R13-21, R15-1, R16-1) because every fix was a re-validation
of a path that could still be swapped underneath it. The premise, not
the consumers, was wrong: rename(2) and unlink(2) need write permission
on the PARENT directory, never on the entry, and $RUNNER_TEMP's top
level is uid-1000 mode-755 on this pool while the container's node is
uid 1000. A 0700 root:root home there could always be renamed away
wholesale and replaced — so each added check (ownership, mode, inode
anchor, run-id marker, opened-directory re-stat) only narrowed the
window, and the inter-step window before a `uses:` upload could not be
closed at all, because that step cannot run shell.

- The home moves to /flake-gate. `/` is root:root 755 in this container:
  node can neither create, unlink nor rename entries in it, so the
  entry — and everything reached through it — is outside PR-controlled
  reach by construction, with no window left to re-check. The upload
  ships that tree directly. No env override: $GITHUB_ENV is uid-1000
  writable, so a configurable home would be a PR-reachable channel, and
  the record step rm -rf's whatever the home names. The extracted-script
  harness relocates that one constant into its scratch tree instead, and
  the production value is pinned structurally.
- inv_tmp stays under RUNNER_TEMP by necessity (the build user cannot
  enter the root-only home) and gains the :? guard.
- .git is re-owned to root after prepare's workspace chown (R4-1/R4-2):
  with it node-owned, a lifecycle script could plant a smudge filter and
  have root's per-invocation reset execute it every round, or rewrite
  HEAD so the restore installs a tree of its choosing. Non-fatal, since
  the reset already runs git with GIT_* stripped, under a timeout,
  against an OID pinned before any PR code ran.
- The scripts/tests arm gets the same substring-collision labelling the
  generic arm already had, so a same-stem sibling collected by vitest's
  positional filter is named in the attribution (R8-31/R13-28 lineage).

Suites: helper 112/112, twin 134/134 — both run as a NON-ROOT user,
which is what CI does; as root the gate pins its own PATH and the
harness stubs are bypassed by design, so a root run reports false
failures.

* fix(ci): clear the publisher's downloaded results before the download

R16-3: publish-verify runs on the persistent ECS pool and downloads the
artifact into a workspace-relative `verify-results`, which the runner
does not clean between jobs. The publisher treats the presence of
`verify-results/flake-gate.log` as proof that THIS run staged it, so a
previous run's log — possibly from another PR — could be embedded as
this run's evidence. The verify side already applies the same rm-first
rule to its own $RUNNER_TEMP tree; this brings the publisher in line.

Pinned by a structural test asserting the clear step exists and precedes
the download.

* fix(ci): drop the .git re-own — it breaks the build user's own reset

Reverting the .git hardening from 387a843434 after measuring it: the
per-invocation reset runs as `node` (root's git trips the
dubious-ownership guard), and a root-owned .git makes it fail at
`Unable to create '.git/index.lock'` — every round's reset would abort,
which the gate correctly reports as `error`, i.e. the gate would stop
working entirely. Probe: root:root + go-w on .git, reset as the build
user → 'Permission denied' on index.lock.

The R4-1/R4-2/R16-1 surface it aimed at (metadata-steered resets) keeps
its existing defences — OID pinned before sampling, GIT_* stripped from
the reset's environment, timeouts, and the per-round strip of .git
execution vectors — and the residual (that strip is a denylist, so
include/includeIf indirection can still reach it) is tracked as
follow-up rather than closed by a change that disables the gate.

* fix(ci): close flake-gate startup-channel shadowing; blank the upload loader env (#9130)

* fix(ci): close flake-gate startup-window races in the re-exec and verdict path (#9130)

R18-1: anchor the record/gate/staging re-exec snapshots to the inode
bash is executing (fd 255) and re-verify the path against it before the
exec — a swap that lands between bash's open of the runner-written step
script and the snapshot is filesystem state the kill cannot un-land.

R18-2: POSIXLY_CORRECT in all four gate-family step envs — bash then
refuses BASH_FUNC imports named after special builtins at startup, so a
poisoned `set` cannot run attacker code on the body's first command
(the in-script POSIX switch arrives one command late); a poisoned
startup fails the step red, and the abort is the refusal.

R18-3: every pre-re-exec refusal writes through /usr/bin/printf — echo
is a regular builtin, shadowable by a BASH_FUNC_echo%% import even in
POSIX mode (probe-verified).

R18-4: parent-side identity gates query the kernel via /usr/bin/id -u
instead of reading $EUID, which bash imports from the process
environment — one planted EUID line could skip the kill sweeps and the
poisoned-env refusals in every later step.

R16-4 (interim): the publisher's full-report branch maps a missing or
unrecognized FLAKE_VERDICT to a visible fixed-text error line instead
of silently dropping it — on that branch the gate ran and owes a
verdict, so absence means the uid-1000-writable $GITHUB_OUTPUT backing
channel corrupted it in transit. Fixed text only: the raw value is
attacker-influenced on this path and is never embedded.

The behavioral harness now applies the step env block (production
parity), and the two round-15 poison scenarios are re-pinned to the
stronger startup refusal.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-18 15:26:24 +00:00
易良
2785480685
fix(devx): fail with actionable message when unit-test build prerequisites are missing (#9149) (#9171)
* fix(devx): fail with actionable message when unit-test build prerequisites are missing (#9149)

Package-local unit tests in packages/cli import workspace packages
(acp-bridge, sdk, web-templates, channels/*) through their built dist/
output plus the generated git-commit.ts. In a fresh worktree that shares
the main checkout's node_modules, or after a deep clean, those artifacts
do not exist and vitest fails during collection with resolution errors
that blame the wrong thing.

Add a vitest globalSetup guard that checks the prerequisites up front and
exits with a message naming every missing piece and the command that fixes
it (npm run build). Document the prerequisite in the AGENTS.md unit-testing
section.

* fix(devx): address R1 review findings on the unit-test prerequisite guard

- normalize win32 path separators so the guard works on Windows (R1-1)
- use Copyright 2026 Qwen Team header (R1-2)
- drop over-included packages/sdk-typescript; every sdk import in the cli
  test graph is an aliased /daemon* subpath (R1-3)
- add a sync assertion test: every builtin channel dynamically imported by
  channel-registry.ts must stay listed in DIST_PREREQUISITES (R1-4)
- cover the vitest-invoked entry point via exported checkAndReport (R1-6)
- mirror real manifest shapes (exports.default / import variants) in the
  test fixtures (R1-7)
- report missing/unreadable package manifests through the normal exit path
  instead of crashing with a raw stack trace (R1-8)
- derive the package key from vitest-s resolved project root, so
  vitest run --root packages/cli from elsewhere is covered (R1-9)
- probe every exports entry targeting dist/, not only the . entry, so
  missing unaliased subpath builds are reported too (R2-1)

* fix(devx): extend the unit-test prerequisite guard to packages/core

Issue #9149's scope names packages/cli AND packages/core, but the guard
only covered cli: eight core test files (providers/__tests__/presets/*,
provider-config.test.ts) import the bare '@qwen-code/qwen-code-core'
specifier, which resolves through the package's own exports to
dist/index.js — on a fresh checkout 'cd packages/core && npx vitest run
src/path/to/file.test.ts' (the AGENTS.md-documented command) still died
with the opaque 'Failed to resolve entry for package' error.

- Add 'packages/core': ['packages/core'] to DIST_PREREQUISITES
- Wire the same globalSetup guard into packages/core/vitest.config.ts
- Skip wildcard pattern exports entries ('./dist/*') in distEntryFiles:
  core's manifest carries them and they name no individual file —
  probing them literally would block core test runs even fully built
- Generalize the fixture builder to every DIST/GENERATED_PREREQUISITES
  entry, add coverage for the core dist requirement and the wildcard
  skip, and move the 'no known prerequisites' example off packages/core
- Note the core self-import in AGENTS.md

Probe-verified both arms at this commit: dist moved aside -> the guard
prints the actionable message and stops the run; dist restored -> the
test file passes (11/11).

* fix(devx): harden the prerequisite probe and its drift tests

- Probe manifest 'main' entries spelled without a leading './': all
  guarded manifests use "main": "dist/index.js", which the old
  startsWith('./dist/') predicate never matched, so the documented main
  probe silently collected nothing. Normalize before the prefix check.
- Tolerate digits in builtin channel names in the sync test's registry
  regex ('channel-[a-z0-9-]+'), or a future channel with a digit in its
  npm name escapes the drift check.
- Add the reverse sync assertion: every listed packages/channels/*
  prerequisite must map back to a channel-registry import (channel-base
  excepted as the channels' build dependency), so removing a builtin
  channel cannot leave a stale entry that hard-blocks cli test runs
  with a misleading 'fresh checkout' message.
- Cover the main-entry probe with a fixture test.

* fix(devx): fail loud on stale probes, align key derivation under symlinks

Round-3 review findings on the prerequisite guard:

- R3-1: a listed package whose manifest enumerates zero ./dist/ targets
  (require-only or nested-condition entries) passed the probe silently —
  report 'exposes no dist/ entry files to check (guard probe may be
  stale)' instead, so a stale probe cannot resurrect the raw resolution
  error this guard exists to replace.
- R3-3: when SOME dist entry files exist, the missing one is no longer
  diagnosed as 'has not been built' + a plain npm-run-build prescription
  (a successful build can legitimately leave a stale exports entry); the
  message now says the build output is incomplete or exports points at a
  file the build does not emit, and to check the package's exports
  entries when rebuilding does not help.
- Key derivation now realpaths both cwd and root (with a fallback to the
  raw path): repoRoot descends from import.meta.url, which Node resolves
  through symlinks, while vitest resolves root with a plain path.resolve
  — comparing them raw let a symlinked ancestor silently disable the
  guard.
- R3-2: the channel drift-check character class now tolerates digits,
  underscores and dots per npm naming rules.
- R3-4: renamed the test that claimed win32-separator coverage it never
  exercised; its body is the degenerate repo-root silent-yield path and
  the comment now says so.
- R1-6 (partial): added default-export coverage — project.config.root
  extraction and the process.cwd() fallback, asserting no exit in a
  built repo. The exit-1 arm of the default export stays untested: it
  needs a root-injectable seam the entry point deliberately does not
  have; checkAndReport's return-1 and message remain covered directly.

Tests: 21/21; mutation probes confirm the zero-enumeration and symlink
tests catch their regressions.

* fix(devx): hermetic guard tests, alias-aware probe, explicit gitlab build

- drive the default-export tests against a hermetic fixture checkout via
  QWEN_VITEST_GUARD_ROOT (in-process and subprocess), so they hold on an
  unbuilt worktree instead of depending on the real repository state
- skip dist targets the consumer aliases to TypeScript source when probing,
  so a missing-but-aliased dist file no longer blocks runs that would pass
- make the remedy message context-aware: the git-commit hint appears only
  when a generated file is missing, and a stale-probe line gets its own note
- add packages/channels/gitlab to buildOrder: it is a cli channel-registry
  builtin like its siblings and used to build only transitively
- add drift tests pinning the globalSetup wiring in both vitest configs

* fix(devx): ignore commented vitest aliases

* fix(devx): anchor the vitest globalSetup guard to the config file

R1-1: a relative globalSetup path is resolved against vitest's root (the
process cwd without --root), not the config file's directory, so the
prerequisite guard only loaded when vitest happened to run from inside the
package. `npx vitest run --config packages/<pkg>/vitest.config.ts` from the
repository root died with "Cannot find module .../vitest-global-setup.js"
before any test — the cause-hiding failure class this guard replaces.
Resolve it with path.resolve(__dirname, ...) in both packages/cli and
packages/core configs, and update the wiring-sync assertion (now robust to
prettier line-wrapping; flip-verified red when reverted to a bare string).

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-18 13:19:09 +00:00
易良
19a4b973eb
fix(ci): back-port the checkout-heal wipe guard to the triage and serve-ab wipes (#9277)
* fix(ci): back-port the checkout-heal wipe guard to the triage and serve-ab wipes

The "empty the workspace, keep the directory" idiom exists in three
copies; only the review workflow's copy received the #9220 hardening
(canonicalization, trailing-slash strip, RUNNER_WORKSPACE allowlist).
Measured on main for #9265, the two triage guards let non-canonical
spellings of the guarded roots through (/home/, /home/., //usr,
/root/, /var/ all reached the rm), and serve-ab's wipe had no guard
at all — even `/home` or an empty string arrived at `find … -exec
rm -rf`.

Port the reference guard to all three sites, keeping each site's
exit contract: triage fails loud both before and after external
code, serve-ab stays bare under the job's `-eo pipefail` so an
unclearable workspace fails before either checkout builds on top of
the leftovers.

Pin each ported copy with its own tests: bad-path batteries under an
rm recorder (the destructive primitive cannot fire under any edit),
an allowlist-escaping `..` case gated on a GNU-realpath host probe
(the lesson from 90fa6bb4), a realpath-absent trailing-slash
RUNNER_WORKSPACE case, and text pins on the ported layers. Every pin
was mutation-verified red against a deletion of the layer it guards.

* test(ci): pin guarded serve wipe

* fix(ci): close wipe guard fallback gaps

* fix(ci): fail closed without realpath

* fix(ci): keep wipe guards portable

* test(ci): pin wipe-guard RWS layers and unmask the pre-run battery

- run the rewritten pre-run sweep battery under -e -o pipefail so a
  failing sweep can no longer report success (bare bash -c masked it)
- pin the RWS '..' refusal and degenerate-root refusal text in all
  copies, and add RUNNER_WORKSPACE='/' exec cases to both copy suites
- exercise both pre-run and post-run copies in the realpath-absent
  refusal test
- replace the '..' escape vector with a symlink escape that only the
  realpath line can refuse, and correct the mutant-outcome comments
- add the serve-ab wipe-before-checkouts ordering pin from the sister
  suite and a happy-path RWS canonicalization pin

* test(ci): correct wipe-guard mutant-outcome comments for find -P

The symlink-escape comments claimed that with the WS realpath line deleted, find reaches rm through the link target. GNU find's default -P mode does not descend symlink operands: the mutant passes every guard, wipes nothing, and exits 0, so only the non-zero-status assertion catches it — the rm-log assertion passes vacuously. Reword both twin comments (R5-1).

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-18 06:49:40 +00:00
Shaojin Wen
a4a3850fe5
fix(ci): make autofix busy detection fail closed and mark dispatched PRs (#9329)
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
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (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 / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* fix(ci): drop pull_request_review events on closed PRs at the route gate

Reviews on merged/closed PRs have nothing to address, yet each one started an autofix run that spun up a runner only to exit no-op. Observed 2026-08-16: 24+ finding-reply reviews on merged #9222 and 26 runs on merged #9189 within minutes (issue #9296). Add a PR open-state clause to the route prefilter; the scheduled scan remains the backstop, and address-time revalidation already drops targets whose PR closed after dispatch.

* fix(ci): make autofix busy detection fail closed and mark dispatched PRs

Silent API failures in the busy-PR enumeration re-dispatched PRs whose address legs were already running or queued (issue #9296): each duplicate burned one build-cli (~5 min) before cancelling a queued sibling leg through the per-PR group's latest-wins queue.

- Any enumeration failure (run list or per-run jobs view) now empties the scan's candidate set for this pass; a forced dispatch keeps its explicit-override semantics.
- Stamp a pending commit-status marker (qwen-autofix/dispatch-pending) on the PR head at dispatch and treat it as busy while fresher than 30 minutes; the address leg re-stamps it success on checkout. This covers the scan->build-cli window where the matrix leg does not exist in the live-run jobs view yet. Commit statuses only: the check-run creation API needs a GitHub App, and the workflow authenticates with a PAT.

Refs #9296

* fix(ci): keep the dispatch-pending marker from blocking past its TTL

Exempt the marker's status context from the HAS_PENDING_CHECKS gate (a
stranded marker otherwise blocked the PR for up to ~330 minutes, not the
documented 30-minute TTL), release it on the address-time discard path,
guard every status write same-repo and dry-run, narrow the fail-closed
carve-out to explicit workflow_dispatch dispatches, emit enum_failed so
an emptied candidate set cannot flip the scheduled issue phase on, and
carry the enumeration error tail in the fail-closed warning. Pin all of
it behaviorally in the workflow contract tests.
2026-08-18 04:59:03 +00:00
Shaojin Wen
72ae65d78f
fix(ci): route the autofix convergence-brake handoff through failure.md (#9371)
The growth brake tells the address-review agent to stop with a handoff
but never names the output file, so round 14 on #9184 wrote handoff.md
— a file owned by run-agent.mjs that the verdict gate does not accept.
The round was reported as missing its required outputs even though the
defer-to-human was correct (run 32076785809). Name failure.md (the stop
file run-agent.mjs wraps into the handoff comment) as the handoff
target, forbid agent-written handoff.md, and pin the directive in the
workflow contract test.
2026-08-18 04:03:21 +00:00
Shaojin Wen
d9d210eb7a
feat(autofix): seed the takeover round counter with /takeover from N (#9321)
* feat(autofix): seed the takeover round counter with `/takeover from N`

Taking over a PR that has already been through several review rounds
restarted the Critical-only brake from zero: the round counter is
window-scoped, and engaging takeover opens a fresh window, so a PR that
spent nine human rounds getting to "almost mergeable" got five more
suggestion-capable rounds the moment it was managed — the diff grew on
nice-to-haves exactly where it should have been converging.

`@qwen-code /takeover from N` now seeds the window's counter at N, so
CRITICAL_ONLY_AFTER_ROUND is reached in the remainder rather than a full
fresh five. This is the one parameterized command form: the literal
prefix must still match TAKEOVER_COMMAND byte-for-byte, the tail is a
bounded 1-2 digit integer, and the captured value reaches nothing but an
integer comparison. Everything else — a prefixed body, a `stop from N`
hybrid, a substitution payload — still fails closed.

The seed rides as its own `<!-- autofix-round-start N -->` marker on a
separate line of the engage ack, never as a field inside
`<!-- takeover-ack engaged -->`. That literal is matched with jq
contains(), closing `-->` included, at seven read sites — four here and
three in the fleet shepherd's paused/resume detector — so an inline
field would silently break all of them: the window key would fall back
to an older ack and the shepherd would age out a PR that was just
re-armed. Same shape as the existing autofix-redcheck marker.

Both round readers fall back to the seed instead of a hardcoded 0, read
it by created_at equality against the window key (so a superseded
window's seed cannot leak forward), and clamp it strictly below the
effective cap so a seed can never park a PR at its round cap on the very
round it is taken over. The seed is window-scoped like every other
census: `/retry` or a bare re-takeover returns the counter to 0.

Both engage acks and the Critical-only audit record now name the seed
when there is one — otherwise the ack reports "round 4/100" on its first
managed round, and the audit record claims five completed rounds on a PR
the loop has run twice.

The growth brake is deliberately not seeded: its baseline anchors at the
window's first measured round, and a pre-takeover baseline is not
recoverable, so growth stays measured from engagement.

* fix(autofix): address the R1 review findings on the takeover round seed

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

* fix(autofix): address the R2 review findings on the takeover round seed

* fix(autofix): address the R3 review findings on the takeover round seed

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-18 01:53:07 +00:00
Shaojin Wen
90c166585a
feat(release): user-facing bilingual digest for release notes (#9216)
* feat(release): user-facing bilingual digest for release notes

Stable release notes read as a type-bucketed PR list, which users find
hard to scan. The finalize step now asks the model to group changes into
user-facing themes with short intros, mirrors highlights and themes into
a Chinese digest, attaches screenshots found in merged PR bodies (host
allowlist, per-release cap), and collapses the full PR list into an
appendix with normalized titles. Every model failure path keeps today's
v1 output byte-for-byte, and CHANGELOG.md accepts the new v2 marker.

* fix(release): tighten v2 digest fallbacks and changelog skeleton (#9216)

Address review round 1 findings:

- usedAi only counts themes that carry content, so a release whose
  digest has zero model text is no longer reported as AI-generated
- hasChinese is derived from what the Chinese block actually renders,
  not raw model output, so zh-only-on-breaking releases no longer emit
  an empty or English-only section
- a PR repeated inside one theme is deduped instead of discarding the
  whole themes digest with a misleading cross-theme error
- fallback titles in the v2 digest are normalized like the appendix,
  killing the mixed-style look in the degradation case
- normalizeAppendixTitle strips only the conventional types the
  changelog's formatEntry strips, keeping ci/test/security prefixes
- the changelog unwraps the v2 appendix at the same sibling rank as
  v1's Complete Change List instead of nesting it under the previous
  section
- drop a dead summaries max_tokens scaling term and a verbatim copy of
  renderChangeLine's attribution rendering

* fix(release): close digest image breakout and tighten fallback signals (#9216)

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

* fix(release): drop camo proxy and harden digest text validation (#9216)

* fix(release): neutralize markdown breakouts in digest text and images (#9216)

* fix(release): close classification, image-URL, and text-validation bypasses (#9216)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-17 00:12:04 +00:00
易良
f9e6b67aa7
fix(ci): stop triaging the autofix bot's own deferred-finding tracking issues (#9264) (#9271)
* fix(ci): stop triaging the autofix bot's own deferred-finding tracking issues (#9264)

Every PR that defers findings for the first time opens a tracking issue upserted by the autofix bot, and the issues trigger (opened/edited/reopened) ran a full triage agent on that bookkeeping issue per deferral — the authorize gate exempts the issues path as read-only, so nothing stopped it. Condition the triage job's issues clause on the creator not being the autofix bot (the same vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot' identity qwen-autofix.yml upserts under), and route bot-created issues runs to a per-run concurrency group: GitHub evaluates concurrency before the job if, so a run left in the shared per-number group would still cancel an in-progress triage of the same issue before its own skip is evaluated. Pins: the issues-clause guard, the group routing, and the cross-workflow identity sync, all on the parsed document.

* test(ci): pin triage issue guard connectors

* test(ci): harden triage bot guard pins

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-16 17:09:42 +00:00
qwen-code-dev-bot
f7f78fab4a
fix(ci): force-push release branch so retries replace failed attempts (#9076) (#9082)
* fix(ci): force-push release branch so retries replace failed attempts (#9076)

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

* fix(ci): re-validate the release version before force-pushing (#9076)

prepare's doesVersionExist check runs minutes to hours before publish
pushes (validation jobs and the production-release approval gate sit in
between), and --force removed the non-fast-forward rejection that used
to serialize the push itself. Concurrent same-version runs could
therefore diverge the npm artifacts, the git tag, and main. Serialize
publish per release tag and re-validate the unshipped invariant — every
published package, the tag, and the release — immediately before the
push; pin all three invariants in the workflow tests.

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

* fix(ci): unify the push-time release guard and key concurrency by dry-run (#9076)

* fix(ci): fail closed on push-time release probes and test the CLI seam (#9076)

* fix(ci): clarify push-time release refusals and keep benign ones out of autofix (#9076)

The push-time guard refused retries after a partial npm publish without
saying where the version had shipped or how to recover, and every refusal
failed the publish job into notify_failure, filing a "Release Failed" issue
and dispatching the autofix agent against releases that did not fail.

- Scan all published packages in strict mode and name every shipped
  location in the refusal (npm packages, origin tag, GitHub release) with
  partial-publish recovery guidance; a decisive hit ends the check so a
  flaky later probe cannot mask the refusal with a probe error.
- Give the guard distinct exit codes: 3 = already shipped (decisive,
  benign), 2 = probe or usage failure. Exit 1 is reserved for uncaught
  node errors so a crash can never masquerade as the benign marker. The
  push step marks exit-3 refusals via the version_refusal job output, and
  notify_failure skips its issue + autofix dispatch for exactly that
  failure while genuine failures still notify.
- Cover runCli's default dispatch (prepare's path), the exit-code
  contract, and the process.exit wiring end to end.

* fix(ci): fail closed when the release ref predates the push-time guard (#9076)

* fix(ci): keep refusals decisive after shipped hits and skip the POSIX-only test on win32 (#9076)

* fix(ci): write push-time guard error annotations to stdout (#9076)

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-16 16:33:14 +00:00
Shaojin Wen
c031cc6279
fix(ci): self-heal failed checkouts on the reused review runners (#9220)
* fix(ci): self-heal failed checkouts on the reused review runners

A checkout failure on the self-hosted review pool was terminal: either a
transient network drop mid-fetch (curl 92 / early EOF), or a corrupt
persisted workspace whose refs claim objects missing from its object
store, after which every fetch dies in negotiation with 'remote did not
send all necessary objects'. ecs-qwen-runner-64c-23 stayed in that state
for two days (2026-08-13..15), failing seven review jobs on the same
missing SHAs.

Make the first checkout continue-on-error; on failure wipe the whole
workspace (not just .git) and retry the identical checkout once. The
workspace is disposable — later steps reinstall deps and tools.

* fix(ci): heal with the pool wipe idiom and pin the checkout guardrails (#9220)

* fix(ci): pin the heal chain's sudo leg, path guard, and survivor signal

Addresses the 16:40 review round on the checkout self-heal:

- The wipe-failure test leaned on the real sudo, so it covered a
  different branch per lane; replace it with a PATH-stubbed sudo that
  forces both legs to fail hermetically, and pin the survivors left in
  place plus their oncall-visible warning.
- The '|| sudo -n find' escalation leg survived deletion mutants: add a
  stub-sudo test proving the leg actually runs when user-mode find
  fails (leg-deletion and '||'->'&&' mutants both verified red).
- Reuse the triage idiom's suspicious-path guard before wiping.
- Count post-wipe survivors and warn with the count — triage exits 1
  here, but the heal chain must stay alive for the retry.
- Disclose in the step comment that the sudo leg only helps pool
  members with passwordless sudo.

* fix(ci): close the heal guard's trailing-slash hole and name wipe survivors (#9220)

* fix(ci): canonicalize the heal guard's path match and allowlist the runner workspace (#9220)

* fix(ci): strip the heal allowlist root's trailing slashes and pin the guard layers (#9220)

* fix(ci): canonicalize the heal lock fixture and pin the WS strip loop (#9220)

* fix(ci): keep the checkout-heal suite green on a BSD userland

Addresses the 09:38 review round: three of the new tests assume the
wipe script's `realpath -m` canonicalization actually ran, and `-m` is
a GNU coreutils extension — Darwin ships FreeBSD's `realpath [-q]`,
exits 1 on it, and the script's `|| printf` fallback silently keeps the
raw path. The production script is unaffected (the review pool is
Linux-only), but this suite is excluded on win32 alone, so it also runs
on the macOS lane, where the assertions are red for a defect that
cannot exist there.

- Probe the host for `realpath -m` and skip the canonicalization test
  when it is absent, rather than skipping on `platform === 'darwin'`:
  the probe keeps the coverage on a Mac with coreutils on PATH and
  still skips on any other non-GNU userland. Mutation-checked on a GNU
  host — deleting the canonicalization line still turns the test red.
- Spell both halves of the allowlist comparison the same way in the
  lock fixture: it resolved its workspace with realpathSync while the
  runner-workspace root stayed raw, so on a symlinked tmpdir (macOS
  /var -> /private/var) the two sat on opposite sides of the link, the
  guard refused, and the wipe helper threw before any assertion ran.
  Canonicalizing the root keeps both tests running everywhere instead
  of leaning on the GNU-only flag to reconcile them.
- Record in the step comment that `-m` is GNU-only and that off-GNU the
  guard degrades to the strip loop and the allowlist.

Verified by simulating a BSD userland (a PATH-fronted realpath that
rejects -m): the suite goes from 1 failed / 12 passed to 12 passed on
the CI lane's environment, and from 3 failed / 10 passed to 12 passed
with a symlinked TMPDIR, matching the two failure shapes reported.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-16 15:06:54 +00:00
Shaojin Wen
b744248656
fix(ci): keep a fallback comment when the PR review runner dies (#9255)
* fix(ci): keep a fallback comment when the PR review runner dies

A review job that dies abnormally never reaches its in-job fallback
comment step: the runner worker crash in FinalizeJob on the PR #8894
run (EACCES creating under the runner home directory) left the PR with
no review and no explanation.

- Probe write access to $HOME, $RUNNER_TEMP and the runner root at job
  start, repair single-directory ownership with the existing sudo
  pattern, and fail fast with a clear message when repair is impossible
  instead of burning the review budget to die at finalize.
- Add a fallback-comment job on an ephemeral hosted runner that posts
  the retry guidance whenever review-pr fails. It derives the PR number
  from the event payload (dead job outputs do not survive a crash) and
  dedupes on a qwen-review-fallback comment marker plus this run's URL,
  so the in-job step, the ack comment, and re-runs never double-post.

* fix(ci): harden the PR review fallback comment (#9255)

Review-round fixes for the fallback-comment defenses:

- Probe the actual runner root (three levels above the workspace, not
  two) and the _diag subdirectory FinalizeJob writes in; a writable
  parent does not prove an existing subdirectory writable.
- Open the fallback gate on authorize/review-config failures too — the
  incident's trigger can kill those earlier self-hosted jobs first, and
  a failed dependency marks review-pr 'skipped', which the old gate
  never matched. Guarded against resolve dispatch runs, which skip
  review-pr by design.
- Author-scope the dedup lookup (resolved dynamically like
  upsert-bot-comment.sh) so a planted marker cannot suppress the
  fallback, and fail closed with bounded retry when the lookup or the
  state check fails instead of fail-open toward duplicates or a green
  job that never posted.
- Skip the stale fallback when the PR head moved, but only on
  pull_request_target events where the run head is comparable —
  comment/review runs report main's tip, and posting wins over silence
  when the comparison is unavailable.
- Define the marker once in a workflow-level env and pin all of the
  above in the workflow test suite, executing the fallback step's real
  bash against a stubbed gh.

* fix(ci): close the fallback-comment gate gaps from round-2 review (#9255)

- Exclude comment-driven /resolve runs from the fallback gate:
  authorize runs on `@qwen-code /resolve` issue comments where
  github.event.inputs is empty, so the dispatch-only exclusion never
  fired there and a failed resolve run was misdiagnosed as a dead
  review recommending the wrong command.
- Enumerate precheck-pr and delay-automatic-review failures in the
  gate: either failure marks review-pr 'skipped' (a transient API 5xx
  in delay's re-check step, or the fork-PR chain root dying before it
  posts anything), which the old gate never matched — silence,
  against its own "a skipped review is as unexplained as a dead one"
  norm. Both are 'skipped' where they do not apply, so the gate stays
  closed there.
- Anchor the cross-job dedup on the run URL's closing paren: run ids
  grow digits over time, so the unanchored substring let a later
  run's fallback comment (id 123450) suppress an earlier run's (id
  12345) re-run comment; every marker body renders the URL as
  [workflow logs](...runs/<id>), so the id is always followed by ')'.
- Pin the previously surviving mutants in the workflow suite: the
  _diag probe guard polarity, the workflow_dispatch disjunction, the
  ephemeral-hosted-runner placement, and the fallback body's
  marker-link shape the anchor relies on; add executed coverage for
  each head-lookup partial failure and for a distinct run's fallback
  not suppressing this run's comment.

* fix(ci): close round-3 review gaps in the fallback-comment defenses (#9255)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-16 11:36:37 +00:00
Shaojin Wen
a9bff6c9b8
feat(autofix): defer verified out-of-footprint findings to a surviving follow-up queue (#9189)
* feat(autofix): route verified out-of-footprint findings to a surviving follow-up queue

Anti-drift closure for the review loop: a finding that is REAL but whose
fix lies outside the PR's footprint previously had only wrong outcomes —
implement it (scope drift), decline it (the finding is lost when the PR
merges and nobody re-reads its threads), or push it onto a maintainer.

- SKILL gains the fourth disposition, Defer to follow-up: verified +
  out-of-footprint → record {id, path, reason} in
  deferred-findings.json, reply on the thread that it is deferred, leave
  the thread open. Decline stays for what is not worth doing anywhere;
  defer is for what is worth doing elsewhere.
- The report step upserts these into one per-PR 'Deferred review
  findings' issue (marker-keyed, append-only by rc id, agent text
  token-neutralized and length-capped), for BOTH pushed and no-op
  outcomes. Best-effort: an upsert failure never fails a round.
  Deliberately no ready-for-agent label — feeding the bot's own
  deferrals back into its issue queue is a human authorization.
- deferred-findings.json rides the artifact dump and the repair
  cleanup; the neutralization ledger grows to ten sites.
- Tests: shape validation (non-empty array of numeric-id items), line
  building (dedupe by rc id against the existing issue body, newline
  flattening, truncation), and wiring pins for both call sites.

* fix(autofix): rebuild the deferred-findings upsert per review round 1

- Extracted to a trusted staged script callable from ALL outcome paths —
  the failure/handoff path persists verified findings too (a failed
  round's commit dying says nothing about the findings' validity).
- Append-only durability: the tracking issue's body is written once;
  every later round POSTS a comment — no read-modify-write can race a
  maintainer's edits, and a failed body/comments read SKIPS the round
  (never mistaken for empty history). Success is logged only when the
  write call succeeded; failures say NOT persisted.
- Structured lookup: jq filtering over the real bodies (no line-joined
  awk under pipefail), pull requests excluded, lookup failure skips
  rather than creating duplicates.
- Dedupe is line-anchored ('- rc:<id> ' at line start, body+comments
  corpus) with intra-batch unique_by; ids the round resolved in code are
  excluded (a finding cannot be implemented and outstanding at once).
- Shape gate covers path (string when present); path bytes are
  charset-sanitized so a crafted path cannot forge queue bullets.
- Publication-trust posture recorded: the deferred lines are the same
  agent-authored trust class as every other published output — marker
  neutralization, mention-free sanitized charset, length caps, and a
  20-item batch cap bound the surface.
- Tests: the real script runs against a recording gh stub — create,
  append+dedupe (body and comments), PR-carrying-marker exclusion,
  anchored dedupe vs free-text mentions, read-fail skip,
  resolved-exclusion, shape-gate loudness, write-fail honesty, and
  forged-path sanitization; the neutralization ledger returns to nine
  workflow sites with the script-side tenth pinned in place.

* fix(autofix): harden deferred-findings upsert per review round 2

- Pass the known-id corpus to jq via --rawfile: a large corpus in one
  --arg argv element hits Linux MAX_ARG_STRLEN and the swallowed exec
  failure would silently drop the round's deferrals.
- Digest-gate the staged upsert script: record upsert_sha256 at stage
  time (expression context) and verify before each of the three
  invocations; RUNNER_TEMP is agent-writable in between. A mismatch
  skips persistence, never the round.
- Add the gh hygiene preamble (GH_HOST pin, GH_TOKEN unset, fresh
  GH_CONFIG_DIR) to the review-address failure/handoff report step —
  the one PAT-bearing gh step that lacked it.
- Query the tracking-issue lookup with state=all so a maintainer-closed
  issue is appended to instead of forking a duplicate.
- Enforce integer positive finding ids in the shape gate (a float id's
  dot is a regex wildcard in the anchored dedupe and never
  index()-matches resolved ids).
- Clip the 20-item batch loudly and qualify success messages with
  kept/total counts instead of claiming full persistence.
- Tests: digest + hygiene wiring pins; stub knobs for list/comments
  fetch failures; append-write failure, multiline-reason flattening,
  bad-id, loud-cap, and state=all cases.

* fix(autofix): close bash/transport channel gaps and failure-path gate holes (review round 3)

- Sweep BASH_ENV/ENV and imported BASH_FUNC_*%% functions plus proxy
  (HTTPS_PROXY/HTTP_PROXY/ALL_PROXY + lowercase) and SSL_CERT_FILE/DIR
  at all four gh-hygiene sites: both families are GITHUB_ENV-plantable
  and bypass the TRUSTED_PATH pin (child-bash startup) or reroute/
  decrypt PAT-bearing HTTPS. Also de-shadow gate-critical names and
  hash -r, since a planted BASH_ENV runs before the step body.
- Pin PATH to the staged trusted value (guarded for pre-stage crashes)
  and drop the loader trio in the failure/handoff report step — its
  digest gate previously ran under ambient PATH/LD_PRELOAD.
- Failure-path upsert: skip with a plain notice when stage never ran
  (empty digest is not a tamper alarm), and verify the PAT's bot
  identity before writing (POST_HANDOFF's check is skipped on the
  fixed/noop-outcome path); correct the guard comment that claimed
  parity with the handoff guards.
- Fold the twice-pasted digest-gate + upsert block in 'Push and report'
  into a step-local run_deferred_upsert(), matching the
  resolve_and_reply_threads convention.
- Dedupe corpus reads bot-authored comments only, so a third party
  commenting on the public tracking issue cannot suppress a finding.
- Tests: hygiene sweep pins ordered before the first gh call; placement
  assertions (function defined once, called after both resolve arms;
  failure invocation inside the DRY_RUN/STALE/token guard slice);
  digest/identity/notice pins; behavioral cases for foreign-author
  suppression, intra-batch duplicate ids, markerless-issue create path,
  creator/marker anchors, and marker neutralization through the append
  path.

* fix(autofix): isolate deferred-upsert in a clean env -i child, drop the unsound in-shell sweep (review round 4)

Round 4 (R4-1, five Criticals of one class) showed the in-shell
BASH_FUNC/proxy denylist sweep the prior round added is unsound: it
bootstraps trust from the very shell namespace it sanitizes, and a
planted BASH_FUNC_env%%/unset%%/command%%, an expand_aliases alias, a
readonly -f shadow or a DEBUG trap each defeat it — ending in the
staged upsert script executing with CI_DEV_BOT_PAT in env.

- Replace it with sound isolation: both upsert sites (run_deferred_upsert
  in 'Push and report', and the failure/handoff path) run the digest
  gate, the PAT identity check and the staged script in a fresh
  '/usr/bin/env -i … bash --norc -c' child. /usr/bin/env is invoked by
  absolute path — bash never does function/alias lookup on a
  slash-bearing word, so a planted BASH_FUNC_env%% cannot intercept it —
  and env -i drops every BASH_FUNC_*, BASH_ENV, SHELLOPTS, alias and
  trap before any gated work. GH_CONFIG_DIR is minted inside the clean
  child (its mktemp cannot be shadowed there), closing the mktemp-shadow
  hole in the failure step's preamble.
- Remove the sweep and the in-step gh/PATH preamble the prior round
  added to all four PAT gh steps; the three pre-existing steps revert to
  their prior posture. Hardening the pre-existing PAT gh calls
  (handoff/report comment, push, publish) against BASH_FUNC/transport
  plants is noted as separate, out of this feature's scope.
- Script: accept a contract-valid empty array as a clean no-op instead
  of a false 'malformed' alarm; 'set +C' so a planted read-only
  SHELLOPTS=noclobber cannot silently empty the dedupe corpus (belt to
  the env -i child that already drops SHELLOPTS).
- Tests: replace the sweep pins with clean-child pins (absolute-path
  env -i at both sites, GH_CONFIG_DIR/PATH inside the child, failure
  launch inside the guard slice); add no-findings exit-0, empty-array
  no-op and set +C cases. Behavioral probe: a fully tainted parent
  (BASH_FUNC/alias/BASH_ENV plants) cannot reach into the env -i child.

* fix(autofix): strip LD_* before the env -i upsert child; tighten id gate, mktemp/cap guards (review round 5)

- R5-1 (Critical): LD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH is the one channel
  env -i cannot block — ld.so maps a planted library into /usr/bin/env
  itself at execve, before -i wipes anything. Neutralize it with a
  command-prefix assignment (LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH=)
  before /usr/bin/env at both upsert launch sites: a pure shell
  parameter assignment no BASH_FUNC can shadow, applied to env's own
  environment. Probe: a parent LD_PRELOAD=/evil.so no longer reaches the
  env binary or the child.
- R5-3: shape gate rejects integer-valued floats jq renders in
  scientific notation past 2^53 (1e21 -> "1E+21") — the '+' is a
  regex-active byte in the anchored dedupe. Add a <2^53 bound and a
  tostring plain-digits belt.
- R5-4: guard mktemp failure (a known /tmp-exhaustion CI state) so it
  warns and skips instead of a silent exit 0 that violates the header
  contract.
- R5-2: the cap warning no longer promises 're-defer in a later round'
  (impossible — the eval-watermark filters evaluated feedback out
  permanently); it names the dropped bullets for a maintainer.
- Tests: LD prefix, GH_HOST-in-child and gate/exec ordering pins; tie
  the two near-verbatim clean-child bodies together (R5-6); source pins
  for both --paginate sites (R5-5); if-!-echo gate-condition pin (R5-9);
  failure step added to the sweep-removal regression loop (R5-11);
  behavioral cases for sci-notation id, mktemp failure, prefix-colliding
  dedupe boundary (R5-10) and the reworded cap.

* fix(autofix): defuse mentions in deferred bullets, verify child liveness (review round 6)

- R6-9 (Critical): the reason is agent-influenced prose published under
  the bot identity, so a raw @ fired real mentions from the tracking
  issue. Defuse before rendering: @ gets a trailing ZWSP, and the entity
  spellings GitHub decodes BEFORE its mention filter (&#64; &#x40;
  &#0064; &commat;) get their & escaped. Both measured inert against the
  real renderer; \@ and leaving & alone are not. Paths were already
  charset-reduced. Byte-exact probe: 2/2 @ defused, all four entity
  spellings escaped, no raw spelling survives.
- R6-8: LD_* cannot be enumerated — LD_TRACE_LOADED_OBJECTS is
  presence-tested, so even the empty prefix assignment leaves trace mode
  on and /usr/bin/env prints its libs and exits 0 without ever running
  the child (probed). Verify the RESULT instead: the child prints a
  liveness sentinel first and its absence is reported. The inspection
  uses bash builtins only — an external grep would itself
  print-and-exit-0 under trace mode, neutering the check (measured: the
  first grep-based attempt failed exactly this way).
- R6-4: guard the child's own GH_CONFIG_DIR mktemp; an empty value falls
  back to the shared ~/.config/gh.
- R6-6: a line-builder (jq/sed) failure warns instead of exiting
  silently as 'nothing new' — the last path skipping the header
  contract.
- R6-3: write-failure warnings say the findings are LOST (watermark-
  gated, never retried) and name the bullets, matching the round-5 cap
  wording fix.
- Tests: allow-list entry pins (R6-7), sentinel/builtin-inspection and
  child-mktemp pins, identity-check ordering (R6-5), plus behavioral
  cases for mention defusing, reason-type and id-0 gate clauses (R6-2)
  and line-builder failure.

* fix(autofix): defer findings from all three feedback sources; carry them across repair (review round 7)

- R7-1 (Critical): only inline comments carried an id in feedback.md, so
  a verified out-of-footprint finding raised in a review body or an
  issue-level PR comment could not be deferred and was lost at merge.
  Feedback now renders [rv:<id>] and [ic:<id>] alongside the existing
  [rc:<id>], the record takes an optional "source", and bullets anchor
  under a per-source prefix so id spaces cannot collide. The resolved-id
  exclusion stays inline-only (that is what resolved-comments.txt holds).
  SKILL documents all three sources and that only inline findings have a
  thread to reply on.
- R7-2 (Critical): every abort path said "skipping ... this round",
  implying a retry that cannot happen (the eval watermark filters this
  round's feedback out of every later round and the next reset wipes the
  file). All six now report the findings as LOST and dump the raw
  deferrals for manual recovery, with :: neutralized — the dump is
  agent-influenced and a raw :: at line start is a workflow command.
- R7-3 (Critical): 'Repair deterministic rejection' deleted
  deferred-findings.json before any upsert site ran, so run 1's
  deferrals died in a repaired round. It now carries them into a sidecar
  the upsert unions in (merging if an earlier repair left one), and the
  sidecar rides the artifact dump. A/B probe: base arm loses run 1's
  deferral, fix arm persists both.
- R7-4: a present-but-non-string path (false) passed the gate because //
  treats false as absent; the gate now tests .path|type directly.
- R7-5: LD_PROFILE/LD_PROFILE_OUTPUT/LD_DEBUG/LD_DEBUG_OUTPUT are
  non-blocking loader file-write channels the liveness sentinel cannot
  catch, so they join the command-prefix neutralization (probed inert
  when empty).
- Tests: per-source anchoring and dedupe, unknown-source rejection,
  path:false, carry-only and carry-merge cases, LOST dump with ::
  neutralization, plus wiring pins for the carry, the feedback ids and
  the extended LD prefix.

* perf(autofix): bound the deferred-issue lookup and name gh failure causes

Clears the two backlog items the review has re-raised every round since
round 2 (R2-6, R2-10); both live in the file this PR adds.

- R2-6: the tracking-issue lookup ran a full --paginate over every issue
  the bot has ever opened, on every round that defers anything, keeping
  only the first match. It now walks newest-first pages and stops at the
  first marker match: one request in the common case, a short page ends
  the scan (corpus exhausted -> create), and a 10-page cap bounds the
  worst case. Reaching the cap without a match SKIPS rather than opening
  a second tracking issue for the same PR.
- R2-10: every gh call discarded stderr, so a rate limit, an expired PAT,
  a transport error and a 404 rendered identically in the feature's only
  signal. All five calls now capture stderr to one sink and the warnings
  name the cause, :: neutralized like every other echoed API/agent
  content.

Measured on the shipped script with a recording gh stub: first-page hit
1 request, empty corpus 1 request + create, page-2 hit 2 requests, cap
10 requests + skip, and the 403/401 bodies reaching the warning text.

* test(autofix): close the pin gaps the round-8 mutation sweep found

Round 8 raised 15 findings, none Critical: two behavioural, the rest
test pins the reviewer proved vacuous by mutation.

- R8-3: the carry-merge failure branch discarded THIS run's deferrals
  with no raw dump — the one loss path in the feature without recovery
  output. It now prints the set (:: neutralized) before deleting it.
- R8-1/2/5/6/7/8/9/10/11/12/13/14/15: pins that survived their own
  mutations. Notably: the negative sweep pins now assert the PROPERTY
  (no BASH_FUNC / unset -f / hash -r in non-comment lines) instead of
  round 3's exact spelling; allow-list entries must sit inside the
  env -i argument list, not merely somewhere in the step; the identity
  check is pinned whole so a fail-OPEN mutation cannot pass; the repair
  cleanup's deletion pin is spelling-independent and allows exactly the
  one delete that follows a merge; the staging pins are scoped to the
  stage step with cp ordered before the digest record; and the script's
  <!-- escape site gets the count+canonical treatment its workflow
  siblings already had.

Each new pin was mutation-verified: 8/8 injected regressions turn the
suite red (differently-spelled sweep, relocated allow-list entry,
deleted GH_CONFIG_DIR export, fail-open identity check, re-added
cleanup deletion, deleted re-print loops, ascending lookup order,
no-op sed spelling).

* fix(autofix): close the upsert TOCTOU and the per-id deferral collapse (review round 9)

- R9-1 (Critical): the digest gate was check-then-use — sha256sum read
  the staged path and bash re-opened it, two opens of a path this PR
  itself calls agent-writable. The child now reads the script ONCE and
  runs those exact bytes (bash -c "$UPSERT_SRC"), so the bytes hashed
  are the bytes executed. A/B against an inotify-driven same-user
  rename(2) watcher: old shape 20/20 payload executions with the gate
  never firing, new shape 0/20 (legit 20/20, control 20/20).
- R9-2 (Critical): unique_by([source, id]) collapsed DISTINCT findings
  sharing one review-body or issue-comment id — the two sources this PR
  adds — and reported success while losing them. Dedupe identity and the
  corpus check are now per rendered line for those sources (inline
  comments keep id identity and the cross-round anchor). Probe: two
  findings under one review id now both persist ("3 of 3 new"),
  byte-identical records still collapse, inline behaviour unchanged.
- R8-4 (re-raised): fixed structurally instead of by the suggested
  prefix entry, which is a no-op — LD_SHOW_AUXV is presence-tested, so
  an empty assignment still dumps 22 auxv lines (measured; env -u does
  not help either). Loader side channels write to the LAUNCH process's
  stdout, so that stdout is discarded and the child logs to a private
  file; path and read-back are fork-free ($$ expansion, $(<file)) so a
  polluted parent cannot leak noise into the value. Measured: with
  LD_SHOW_AUXV planted the log holds 0 auxv lines and the child still
  runs; with LD_TRACE planted the sentinel is absent and the warning
  fires.
- R9-3/4/5: the manual-recovery dumps now say when they truncate, and
  name the full byte count.
- R9-11: the artifact dump neutralizes :: in the agent-written files it
  prints, like every other echo of them.
- Tests: pins for the single-read exec, the private log, the fork-free
  parent handling, the identity check's ENFORCEMENT (R9-9), an
  allow-list that must hold ONLY the sanctioned entries (R9-10), the
  sentinel comparison inside the re-print loop (R9-13), and R9-2's
  multi-finding cases. Also fixes an argList slice that anchored on a
  comment mention and silently widened to the whole step.

* fix(autofix): keep a poisoned carry from sinking the round; clear the pin backlog

Clears the eleven items carried from round 9.

- R9-18: a carried sidecar that PARSES but fails the shape gate used to
  abort this round's valid deferrals too — asymmetric with the
  unparseable-carry branch, which persists this round only. The gate is
  now a function applied to the merged set, with a retry on this round's
  own file; the carry is dumped and named LOST. Measured on the real
  script: valid own + gate-invalid carry -> own persisted, carry dumped;
  valid own + unparseable carry -> own persisted; invalid own -> loud
  total abort, nothing written.
- R9-20 / R9-7: the union argument order IS the freshness guarantee
  (jq unique_by keeps first-of-group in original order), pinned at both
  sites; measured: a duplicate id keeps this round's text, not the
  carried one.
- R9-3/4/5 follow-up: the three truncation dumps became one dump_file
  helper instead of a fourth copy.
- Pins: --paginate anchored to the comments call (R9-8), the stale
  two-sites comment corrected (R9-6), the no-in-shell-sweep property
  widened past function-unset spellings to alias/trap/proxy forms
  (R9-12), the repair cleanup's deletion pin extended to rm -rf and to
  any second multi-line list (R9-16), runUpsert's spawnSync bounded like
  its sibling harness (R9-17), the explicit review_comment spelling
  covered (R9-19), and the 20-item cap's survivor set pinned from a
  MEASURED run whose sort order and input order disagree (R9-14) — the
  four records written first are the ones dropped.

Mutation-verified 6/6: relocating --paginate, an alias-form sweep, an
rm -rf deletion, either union order swapped, and dropping the
poisoned-carry fallback all turn the suite red.

* test(autofix): widen the denylist-sweep guards past their word-boundary hole

`\btrap -\b` cannot match `trap - ERR EXIT`: the boundary sits between
`-` and a space, both non-word characters. The same hole was in two
sibling guards — `\bunset -f\b` misses `unset -fv name`, and
`\bhash -r\b` misses any suffixed spelling. Drop the trailing
boundary on all three.

Mutation-verified: injecting `trap - ERR EXIT INT TERM`, `trap -- EXIT`
or `unset -fv sha256sum` into a PAT-bearing step now turns the suite
red; each passed before.

* fix(autofix): two -e-fatal paths, an escape-order dedupe hole, and a rewording duplicate (review round 10)

Six Criticals; four were defects this PR introduced.

- R10-19 + R10-22 (Critical): the PAT steps run under 'bash -eo
  pipefail' (defaults.run.shell: bash). Measured: 'rm -f' on a planted
  DIRECTORY at the predictable log path exits 1 and kills the step, ': >'
  onto one likewise, and $(<missing) is fatal in a way NEITHER '|| true'
  NOR 'if !' rescues. Creation is now 'rm -rf' + '(set -C; : >)' with a
  warn-and-skip, and the read-back tests -f/-r first while staying
  fork-free. Probed all four planted shapes (fresh/file/symlink/dir):
  the step survives each.
- R10-5 (Critical): the <!-- neutralization ran in a sed AFTER the jq
  corpus comparison, so an rv/ic line carrying <!-- compared its RAW
  rendering against the ESCAPED stored form — never matched, republished
  every round. Escaping moved inside jq, before the compare.
- R10-17 (Critical): rv/ic identity was the exact rendered line, so any
  reworded re-emission (routine: the repair flow re-runs the agent)
  published a permanent duplicate. Identity is now a normalized digest —
  case-folded, punctuation-collapsed, trimmed, capped — which absorbs
  phrasing churn while keeping distinct findings apart. The tension with
  R9-2 is real and resolved deliberately toward a visible duplicate over
  a silent loss.
- R10-1 (Critical): the sweep tripwire is a spelling denylist over an
  unbounded space. Reframed as what it is — a drift alarm, not the
  boundary (the boundary is the env -i child, pinned separately) — and
  aimed at the ENUMERATION PRIMITIVES a sweep needs (compgen -e,
  declare -x, env pipes, export -n) instead of more name vocabulary.
- R10-6 (Critical): the jq stub hardcoded /usr/bin/jq, which does not
  exist on macOS; it now resolves through the original PATH.
- R10-13: the child's log comes from agent-writable RUNNER_TEMP, so ::
  is neutralized on re-emission via parameter expansion (no fork).
- R9-5 leftover: the third truncation dump now names the clipped size.

Mutation-verified 5/5 and behaviour-probed 5/5 (escape-before-compare,
reworded re-emission, distinct sibling, R9-2 no-regression, planted-path
shapes).

* fix(autofix): make the deferral identity lossless; keep the unmerged set on disk (review round 11)

Two Criticals, both defects this PR introduced, plus the eleven items
carried from round 10.

- Identity key (Critical): the normalized key stripped every non-[a-z0-9]
  byte and capped at 160 chars, so CJK siblings collapsed to one key (this
  repo is bilingual) and a long path pushed the reason out of the identity
  entirely — silent loss, the exact outcome the feature exists to prevent
  and the opposite of what its own comment claimed. The key now normalizes
  case and PUNCTUATION only, keeping every letter of every script and no
  cap, at both the build and corpus sites. Rewording tolerance is
  unchanged. Probed: 2 CJK siblings -> 2 of 2; 2 siblings on a 200-char
  path -> 2 of 2; reworded duplicate -> 1 of 1.
- Repair merge failure (Critical): the branch deleted
  deferred-findings.json before 'Show run artifacts' and the artifact
  upload ran, so its own pointer at the artifact dump was false past the
  4000-byte clip. It now renames the set to deferred-findings.unmerged.json
  (kept in WORKDIR, added to the dump list) on the failure path and deletes
  only on the merge-success path. Probed: 6245 bytes preserved where the
  dump clipped at 4000.
- R10-12: neither side is known to be the corrupt one (jq -s fails if
  either input is unparseable), so both merge-failure warnings say that
  instead of blaming this round.
- R10-18: a second identity anchor — the derived title — so an edited body
  that loses the marker no longer orphans the issue into a duplicate; the
  marker still wins when both are present, and a same-titled PR is still
  never adopted.
- R10-3: the carry branch is unreachable in today's topology (WORKDIR is
  wiped at run start, one repair step); kept as defensive with that stated.
- R10-10: the builtins-only discipline is scoped to the child-output
  INSPECTION, which is what it always meant.
- Pins: delimited-token allow-list incl. UPSERT_LOG's value (R10-7), every
  respelling of executing the staged path (R10-8), the tripwire extended to
  the issue-autofix failure steps (R10-11), launch-line anchoring in both
  steps (R10-15), flagless rm counted (R10-16), and behavioural cases for
  the 200/500 caps (R10-4), CJK and long-path siblings, and the title
  anchor.

Mutation-verified 7/7.

* refactor(autofix): remove the agent-writable paths the upsert depended on (review round 12)

Rounds 9-12 each closed one hole in a design that read the staged script
from an agent-writable path and buffered the child's output through
another. Round 12 found four more of the same class (TOCTOU on the log
reopen, a plantable FIFO and an unbounded read on each path). Rather than
patch a fifth time, remove both paths.

- The script travels as CONTENT: the stage step captures it from the
  trusted checkout into a step output (random heredoc delimiter), and the
  clean child runs `bash -c "$UPSERT_SRC"`. With no agent-writable copy
  there is nothing to verify — the digest gate, its check-then-use
  window, the staged cp and the FIFO/huge-file read all disappear.
- The child's messages travel on fd 3, which the parent captures, while
  fd 1/2 are discarded. Every loader side channel writes there, so the
  noise still cannot reach the parsed output — and there is no log file
  to plant, race, bound, or clean up. Probed with LD_SHOW_AUXV and
  LD_TRACE planted: clean output, sentinel behaviour unchanged.
- RC-1: resolved-comments.txt went to jq as one argv element, the exact
  MAX_ARG_STRLEN failure the neighbouring comment describes and that
  `known` already avoided. Both corpora use --rawfile now. Measured on a
  348 KB corpus: the old form dies with "Argument list too long", the new
  one publishes normally.

Net -113 lines, and the pins follow: no-path invariants replace the
digest/log battery. Mutation-verified 6/6.

* fix(autofix): reject multi-document deferral files; survive a base without the script (review round 13)

- Multi-document JSON (Critical): `jq -e` without -s evaluates each
  document in turn and its exit status reflects only the LAST, so
  `[valid]\n[]` exited 0 silently (findings lost, no warning) and
  `[bad-id]\n[valid]` passed the shape gate outright. A single_doc gate
  now runs first, with the asymmetry the earlier rounds settled on: a bad
  OWN file is a total abort, a bad CARRY costs only the carry.
- R13-7: the stage step reads the script from the TRUSTED BASE, where it
  does not exist until this PR merges — under -e that killed every
  pre-merge pull_request-triggered round (true of the old cp too, so this
  has been latent since the script was added). It now tolerates the
  absence and lets the consumers' own empty-content guard skip the round.
- R13-2: the carry union requires both inputs to BE arrays; `add` on two
  non-arrays yields whatever they add to.
- RA1R4-B: the resolved-corpus test is -f/-r, so a directory or FIFO at
  that path is treated as unusable rather than present.
- R13-1: nine rationale comments still described the staged copy and the
  digest gate that round 12 removed.

Probed: both multi-document shapes are rejected loudly with zero writes;
a bad carry still leaves this round publishing 1 of 1.

* docs(autofix): retire the last stale rationale comments (R13-1)

Six of the nine locations were in the test file: comments still describing
the digest gate, the staged copy and the read-once invariant that round 12
removed. Same honesty issue as their three workflow siblings.

* fix(autofix): strip BSD wc padding; let wrapper warnings stay annotations (review round 14)

- BSD wc (Critical): `wc -l` pads its count with leading spaces on
  macOS, and TOTAL_NEW is interpolated into the cap warning and the
  success line — the sibling `wc -c` already stripped it, this one did
  not. Tested with a padding `wc` stub, since GNU wc never pads and the
  regression is invisible on Linux CI otherwise.
- The re-emit loop demoted the feature's own failure signal: every `::`
  became `;;`, including the wrapper's trusted messages. Wrapper-authored
  lines now carry a marker and are emitted VERBATIM (so they render as
  annotations again); the script's output, which interpolates agent
  content, stays neutralized.
- \b on `declare -x`/`export -n` had the same word-boundary hole already
  fixed for `trap -`/`unset -f`.
- Pins: the heredoc CLOSING delimiter, the merge-failure quarantine
  rename, and an allow-list comparison that is a sorted multiset rather
  than a Set — a symmetric same-name addition is exactly what that check
  exists to catch and a Set hid it.

Mutation-verified 5/5.

* fix(autofix): un-truncate the sibling identity; align the carry precedence (review round 15)

Zero Criticals this round; five behavioural findings among the pins.

- The rv/ic intra-batch identity was derived from the RENDERED line, i.e.
  after the 500-char reason cap, so two siblings differing only past the
  cap collided and one vanished silently — the same silent-loss class as
  the CJK and long-path entrances. It now comes from the uncapped
  path+reason; the corpus check still compares rendered forms (that is
  all the issue stores), so cross-round the cap can cost a duplicate,
  never a loss.
- The repair carry union put the OLDER set first, inverting the
  newer-wins precedence the script documents for its own union.
- The resolved-id parser dropped any line with stray surrounding
  whitespace, so a padded `rc:<id>` no longer suppressed its finding.
- The clean child's catch-all warning lacked the trusted marker added
  last round, so the feature's most common failure message was still
  demoted out of annotation form.
- The truncation notice pointed at the artifact dump even when the
  dumped file is a merge temp outside WORKDIR, which is never uploaded.

Pins: the stage step's `id: 'stage'` (the link whose break empties every
UPSERT_SRC), the empty-content skip branch, the capture's `|| true`, and
the trusted marker on every warning inside the child.

Mutation-verified 6/6.
2026-08-16 11:28:05 +00:00
Shaojin Wen
337da2143c
fix(ci): stop dropping agent settings in resolve and follow-up workflows (#9252)
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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(ci): stop dropping agent settings in resolve and follow-up workflows

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

* test(ci): pin remaining agent-settings guard gaps from review

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-16 03:27:00 +00:00
Shaojin Wen
f9bc8cb250
fix(autofix): re-anchor growth divergence on measurement time and external head moves (#9192)
* fix(autofix): re-anchor growth divergence on measurement time and external head moves

Tightens the growth-divergence comparability window (PR #9104 follow-up,
tracked as #9114):

- measured_at (R2-6): the growth-now marker now carries the prepare-time
  measurement instant, and the divergence read filters on it instead of
  the comment's created_at. The report posts the marker only after the
  agent's ~120-minute run, so a round in flight when a concurrent base
  update landed would otherwise pass a created_at filter while carrying
  sums measured against the old base.

- external head move (R2-8, subsumes R6-3): prior sums are measured
  against origin/main, so any commit an external actor (author push) or a
  stale-base merge added since the bot last evaluated the branch inflates
  this round's sum relative to them. BASE_UPD_AT only tracks the bot's own
  update-branch merge; the new GROWTH_NOW_CUTOFF also re-anchors (drops all
  prior sums) whenever the checked-out head is not the bot's last judged
  head (LIVE_RED_HEAD), covering author pushes and base updates alike.

The reader now dedups/orders per run by measured= (a re-run's fresh
measurement wins). Contract tests cover the measured-based cutoff, the
external-head-move re-anchor (both branches), and the writer→reader
round-trip with the new field. 172/172.

R6-6 (markers don't store the effective budget, so a mid-window budget
raise counts old rounds against the new regime — fail-safe, one round
early) stays tracked in #9114.

* fix(autofix): drop the head-move re-anchor, keep the measurement-time filter

Review found the head-move half of this change broken in three ways
(all probe-verified), so it is withdrawn and returned to #9114 rather
than patched under review:

- R1-1 (regression): `autofix-redcheck` records the head the agent was
  GIVEN, frozen before its push — so after any pushing round the next
  round's head differs and the cutoff was set to now, dropping every
  prior sum. In the push regime OVER_ROUNDS_PRIOR could never reach the
  threshold and the #9104 handoff would never fire at all.
- R1-2: the cut was stateless — the round after a correct re-anchor fell
  back to an empty cutoff and re-admitted every pre-move sum.
- R1-3: with no redcheck marker (a crash round) the `-n` guard skipped
  re-anchoring across a genuine external move.

A correct version needs both a bot-authored-move test and a PERSISTED
cut; that is its own change.

What remains is the measurement-time filter (R2-6), which stands on its
own: the marker carries the prepare-time instant and the divergence read
filters/orders on it instead of the comment's post-agent created_at.

Also from this review:
- R1-4: `measured=` is OPTIONAL in the scan, falling back to the
  comment's created_at, so deploying does not blank an in-flight
  window's census.
- R1-9: the per-run collapse now runs BEFORE the over/window/cutoff
  filters — a re-run whose fresh attempt came back under budget was
  still represented by its stale over=true attempt.
- R1-7: comments corrected — run= is the DEDUP identity, measured= the
  ORDER key (four sites).
- R1-8: recorded as a known residual next to the sibling growth-base
  reader, which still filters on created_at; tracked in #9114.
- R1-5/R1-6: fixtures decouple created_at from measured=, cover a
  legacy marker (with and without the cutoff), and pin the measured_at
  source line in prepare.

* fix(autofix): keep the failure-path growth marker scannable when prepare never ran

* fix(autofix): prefer explicit measured= over created_at fallback in the per-run growth collapse

* test(autofix): pin the explicit-measured preference in the per-run collapse

cbb7186fb6 made the collapse prefer a marker with an explicit measured=
over one falling back to created_at, but nothing pinned it: dropping
`.explicit` from the max_by key shipped green. The failure path posts an
inert over=false marker with no measured= when prepare never ran, and its
fallback timestamp is ~2h later than the prepare-time measured= of the
same run's real attempt — so without the preference the collapse keeps
the inert marker and the over filter then drops the run entirely, losing
the count (#9192 R3-1).

Mutation-checked: reverting to max_by(.measured) turns this fixture from
'false 1' to 'false 0'.

* fix(autofix): gate the measured= stamp on a real measurement (#9192)

An unmeasured re-run attempt still emitted measured_at, so its report
posted an explicit over=false marker that the per-run collapse preferred
over the same run's real over=true measurement — erasing the count the
explicit-measured preference was added to protect. Emit the stamp only
when NET_MEASURED=true and let the push/no-op writers omit measured=
like the failure path already does.

Also pin the strict > cutoff boundary with an equality fixture (the
>= mutant shipped green), drop the per-run-collapse fixture that
duplicated the strictly-stronger one from cbb7186fb6, and document the
three self-limiting deploy-transition residuals: the legacy-straddle
collapse, the two-clock PREV_SUM skew, and the fetch->stamp base-update
window.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-16 03:26:58 +00:00
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