Commit graph

475 commits

Author SHA1 Message Date
易良
e8e15e3fb0
fix(ci): rename triage status marker to avoid duplicate-guard collision (#7723)
* fix(ci): rename triage status marker to avoid duplicate-guard collision

The lifecycle status comment used <!-- qwen-triage stage=status --> which
the triage agent's duplicate guard sometimes matches as a prior stage
marker (stage=N), causing it to exit without posting the actual triage
analysis. Rename to <!-- qwen-triage-lifecycle --> so the guard never
matches infrastructure comments.

Fixes the probabilistic silent-triage gap seen in #7713.

* fix(ci): update test assertions for renamed triage lifecycle marker

* fix(ci): sync triage finalize status marker

* test(ci): cover triage status marker parity

* fix(ci): preserve triage marker consumers

* test(ci): pin triage lifecycle marker checks

* fix(ci): add bot-author filter and startswith to triage status lookups

qwen-triage.yml's two status-comment lookups matched any comment containing
the marker — a human reviewer quoting the marker would have their comment
overwritten by the bot PATCH. Add select(.user.login == $bot) (already
present in qwen-triage-finalize.yml) and resolve BOT_LOGIN via gh api user.

Both workflows used contains() for marker matching; startswith() is a strict
improvement since every status comment body begins with the marker. This
prevents the demonstrated defect where the finalize step resolved EXISTING_ID
to a bot-authored Stage comment that merely quoted the marker.

* fix(ci): guard triage status bot lookup

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-26 15:51:22 +00:00
jinye
9bdc62c74b
perf(cli): replace comment-json settings parser (#7747)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-26 14:42:51 +00:00
Shaojin Wen
e3cf1c3b45
revert: drop the stale-base un-park recovery (#7602) (#7640)
Reverts #7602. The Fleet Shepherd (qwen-fleet-shepherd.yml) now keeps the
bot fleet within 25 commits of main by proactively update-branching,
and #7595 already retries a stale-base gate rejection at ANY behind
distance instead of parking — so a PR no longer parks because its base
went stale. That leaves #7602 firing only on a PR parked by a GENUINE
failure that later drifted behind main, where re-arming it just re-runs
a real failure on a fresh base — speculative, near-zero value, and it
carried its own autofix-handoff marker plus scan/report logic and tests.
The retroactive cases it was built for (PRs parked before #7595) were
already recovered by hand.

Keeps #7595 (reactive stale-base gate recovery below the shepherd's
threshold) and #7554 (check-driven stale-base sync) — both cover the
sub-25-behind window and triggers the shepherd does not.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-26 14:26:46 +00:00
Shaojin Wen
883094da36
feat(triage): add sandboxed /verify deep-verification lane (#7710)
* feat(triage): add sandboxed /verify deep-verification lane

@qwen-code /verify on a PR now runs a local-verification-style evidence
round in the isolated /tmux sandbox contract (container, token-free agent
env, loopback model proxy, author-write gate) and publishes the report via
a separate PR-code-free job:

- new verify job: merge-ref checkout at depth 2 (base tip + PR head for
  A/B), skills pinned from base so the tree under test can never rewrite
  its own verifier, PR-planted tmp/*-verify-* artifacts dropped, git
  exec-vector sweep for the persistent workspace, agent verdict
  allowlisted before it reaches workflow outputs
- new publish-verify job: upserts one marker comment (running status ->
  final report), HTML-escapes the untrusted report, reports skip/na/
  prepare-fail/infra outcomes explicitly since /verify is always an
  explicit request
- new verify-pr skill: A/B load-bearing proof, vacuity check on new
  tests, mock-free wire-oracle harnesses, targeted gates, fixed report/
  verdict/assertions artifact contract, counts-are-sacred rules
- triage skill Stage 2c now names /verify (not just /tmux) as the trigger
  to recommend when a PR's central claim needs behavioral evidence

The verify check-runs ride the issue_comment event, which the finalize
workflow's event == "pull_request" universe structurally excludes, so
they cannot pollute the CI table or the deferred-approval gate.

* feat(triage): teach /verify round continuity and artifact-matched methods

Fold two more hand-verification patterns into the verify lane:

- round continuity: the resolve step snapshots the previous verify report
  (if any) into the agent context before the status upsert overwrites it,
  and the skill re-checks each prior finding at the new head
  (fixed/stands/superseded), scoping new probes to the delta
- harness quality: prefer configuration seams over module interception,
  encode the upstream's real semantics in the fake peer, add decoy targets
- artifact-matched methods: per-commit load-bearing tables for multi-commit
  PRs; workflow/CI PRs get embedded-script replay against real data, repo
  lint gates, and day-one trigger cost math from real event history; every
  new config knob must trace to an observable effect, and default-path
  dispatch combinations get probed
- findings quality: blockers enumerate blast radius, demonstrate the
  sharpest consequence end-to-end when budget allows, and carry a collapsed
  minimal suggested fix preserving the original commit's intent

* feat(triage): host /verify evidence images and encode quantified-A/B rules

Borrow the image-evidence and quantified-verification patterns from
hand-run rounds (#7265, #7471, #7686 r2 and the pr-assets convention):

- publish-verify now hosts agent-produced evidence/*.png on the pr-assets
  branch (verify/pr<N>-<run>-<attempt>/) and appends them below the
  escaped report. Untrusted-payload discipline: strict filename allowlist,
  8-image / 2 MB caps enforced in the find predicates, racing-push retry,
  and every failure degrades to a text-only comment. VERIFY_ASSETS_REMOTE
  is a test seam; the block was dry-run against a local bare remote
  covering hosting, hostile filenames, oversize files, dotfiles, missing
  branch, and no-image runs
- skill: evidence images are named as kebab-case captions binding image to
  claim, before/after pairs over lone after-shots; follow-up rounds lead
  with a previous-finding status table (fixed/stands/superseded/declined,
  with adjudication) and re-measure instead of diffing the old report;
  size/perf claims get measured-metric Δ tables with residual deltas
  accounted for; unreachable branches get the configuration that reaches
  them constructed; defensive guards get their accept path checked against
  real production artifacts, not just mocked rejects

* fix(triage): address /review suggestions on the verify lane

- skill: local invocation resolves --repo and passes it to every gh call
- skill: call out the dependency confound when the base A/B side reuses
  the PR-installed node_modules and the PR touches package.json/lockfile
- workflow: document the pin step's bootstrap logic — issue_comment jobs
  run the default branch's YAML, so base always carries the verify-pr
  skill by the time this job exists

* fix(triage): harden /verify gate, comment budget, and evidence hosting per review

Address review round 5078770575 items 1-3 plus the cheap follow-ups:

- authorize: /verify now requires write from BOTH the PR author (whose
  code runs) and the commenter (who spends a scarce runner slot + model
  budget) — a drive-by account can no longer burn 45 minutes of ecs-qwen
  on someone else's PR; duplicates check once; /tmux and /triage gates
  unchanged. Replayed 8 principal scenarios against a stubbed gh
- authorize acks /verify with the eyes reaction from the always-hosted
  job, so a queued/saturated sandbox pool no longer means total silence
- publish: emit_block escapes FIRST and caps the escaped size (45 KB for
  the report) — a raw-side cap let dense <>& content inflate past
  GitHub's 65,536-char comment limit, 422 the post, and strand the
  running status with no report at all; iconv -c keeps a UTF-8 sequence
  split by the byte cut (likely, given the mandated 中文 summary) from
  shipping broken; replayed: 50 KB dense report -> 45,873-byte body
- publish: image cap is byte-exact (-size -2097153c; find's -2M rounds
  sizes UP to MiB, silently making the documented 2 MB cap 1 MiB), bytes
  must carry the PNG magic (extension is attacker-choosable), duplicate
  sanitized names dedupe instead of overwriting + double-rendering, and
  dropped images are reported in the comment instead of vanishing
- publish: weak terminal notices (cancelled/infra/skipped/n-a) only
  replace this run's own running status; a previous round's real report
  survives as the marker comment and the notice posts fresh
- publish: report.md/assertions.json lookups pin the artifact-dir shape
  and sort (bare find -name order is filesystem-dependent); the verify
  job's verdict.txt lookup sorts likewise
- verify: global npm install runs from RUNNER_TEMP (the persistent
  workspace still holds the PREVIOUS run's tree, whose .npmrc would
  apply to a root install); both cleanup passes remove leftover tmp/
  worktrees (git worktree prune alone only drops metadata); the run step
  no longer re-chowns 50k node_modules files; pr-assets clone sets its
  committer identity once so the racing-push rebase retry can commit
- skill: worktree guidance now tells the agent to remove its base tree
  itself, with the workflow sweep as backstop only

* fix(triage): close runtime-plant and stale-RUNNER_TEMP channels in /verify

Address review round 2 (comment 5079157987) and the CHANGES_REQUESTED
round on the verify lane:

- run step re-sweeps tmp/*-verify-* AFTER npm ci/build and before the
  agent starts: the pin step's sweep runs before PR lifecycle scripts
  (postinstall etc.), which could re-plant a fake artifact dir whose
  zeroed timestamp deterministically wins the sorted collector. From the
  sweep on, only the agent writes those dirs; a steered agent forging its
  own artifacts remains the documented advisory-report residual
- RUNNER_TEMP verify-results/verify-context are rm'd before mkdir: the
  pool is persistent and runner temp hygiene is runner-managed — a stale
  report or previous-report.md from ANOTHER PR must never ride along
- symlinks are stripped from verify-results before upload:
  actions/upload-artifact dereferences them, so a node-planted link would
  exfiltrate whatever it points at into the artifact
- a trusted commenter invoking /verify on a PR whose author lacks write
  now gets an explanation comment from the hosted authorize job instead
  of total silence (the commenter is checked first; drive-by accounts and
  API errors still get nothing); job timeout 45->60 so a slow install can
  never let the JOB limit kill the agent past its own graceful 25m budget
- stale tmp/base-tree (skill's canonical scratch worktree) is removed by
  name at job start — a plain dir isn't git-registered, so the worktree
  sweep alone misses it and the next worktree add would fail
- scripts/tests/qwen-triage-workflow.test.js gains a verify-lane describe
  block: an 8-arm stub-gh replay of the dual principal gate (drive-by
  deny, author-without-write deny + explain flag, self-comment dedupe,
  404 fail-closed, /tmux and /triage unchanged) plus guards for the
  post-prepare sweep placement, the symlink strip, and the RUNNER_TEMP
  resets — the replay found this commit's sweep edit had silently not
  applied, which is exactly the regression class it exists to catch

* fix(triage): close proxy-hijack, gate-bypass, and false-verdict paths in /verify

Address the Codex /review round (19 findings) and the bot's follow-up.
Each fix was replayed locally; the proxy fix has a decisive A/B.

Gate and routing:
- the shell command match is case-insensitive: GitHub Actions expression
  comparisons ignore case, so `@QWEN-CODE /VERIFY` reached the step and
  fell through to the commenter-only branch — running the PR author's
  code with the author never checked
- the verify ack and denial notice require github.event.issue.pull_request:
  /verify on a plain issue was acknowledged but could never report
- publish-verify joins the verify job's per-PR concurrency group, and a
  failed PATCH falls back to posting fresh instead of going silent

Untrusted-input paths:
- the model proxy binds an EPHEMERAL port, reports it through a
  root-owned file, and its health check must echo a per-run nonce with
  the recorded PID alive. A/B with a squatter on 8787: the old code's
  proxy dies EADDRINUSE yet still reports enabled and points qwen at the
  squatter; the new code comes up unaffected on an ephemeral port
- worktree-scoped git config is deleted before hooksPath is resolved:
  `extensions.worktreeConfig` is allowlisted and .git/config.worktree is
  invisible to `git config --local`, so a prior run could set
  core.hooksPath=/ and make the hook sweep's recursive delete walk / as
  root (verified locally). The sweep now also refuses any hooks path
  outside the repository's git dir
- marker-comment lookups accept only bot-owned comments that START with
  the marker: any user can paste the marker and divert the bot into
  PATCHing a stranger's comment
- the upload staging dir is re-flushed after npm lifecycle scripts

Honest verdicts:
- the docs-only classifier no longer uses a pipeline (grep -q made the
  writer take SIGPIPE, so under pipefail a long file list with an early
  code file classified a code PR as docs-only and skipped verification),
  and executable markdown/YAML (.qwen, .github/workflows, scripts) is
  classified as behavioral before the extension rule
- tee's status is checked alongside qwen's: a full results volume made a
  truncated evidence stream publish as pass
- 137 is split by elapsed budget into watchdog timeout vs crash/OOM
- the agent's verdict is honored only for VERDICT=pass with a report and
  zero failed assertions; otherwise the process outcome headlines and
  the scope paragraph says the run did not complete
- verdict.txt is read through a bounded prefix (SIGPIPE under pipefail)

Skill contract corrections: per-commit tables only when the commits are
reachable at depth 2 (else aggregate + Not covered); internal workspace
symlinks must have their realpaths asserted before a base control is
trusted; repo lint gates and event-history cost math are qualified to
what the token-free container can actually run; --repo is never inferred
from `origin` (a fork holds a different PR under the same number).

Tests: 9 new guards, all mutation-verified (reverting each fix turns one
red), including an executable escaping/size-cap/UTF-8 test for the
publisher's own emit_block and a fix to the earlier command-file test,
which matched the tmux job's identically named step.

* fix(triage): re-establish the /verify trust boundary after PR code runs

Third review round (31 findings). The unifying defect: everything the
lane pinned or swept happened BEFORE npm ci/build executed PR-authored
lifecycle scripts as node, so each control could be undone afterwards.

Trust boundary, rebuilt in order before the agent starts:
- kill every process owned by the build user and fail the step if any
  survives — a detached postinstall child could otherwise wait out each
  one-shot sweep and re-plant afterwards
- re-pin .qwen from the base commit again, now root-owned and read-only:
  the prepare step chowns the workspace to node, so a lifecycle script
  could rewrite the very skill that defines /verify-pr
- give the agent a fresh HOME/QWEN_HOME: qwen loads user-scope file
  commands from $HOME/.qwen, and /home/node belongs to the build user,
  so a planted commands/verify-pr.toml could shadow the pinned skill
- the model proxy now requires a per-run bearer token, closing the
  blind-localhost-scan path to an unauthenticated signer for the real
  model credential (a command the agent itself launches still inherits
  it — documented residual, not closed)

Authorization and lifecycle:
- re-verify the PR author's write permission at execution time and pin
  the authorized head OID; refuse if the checked-out HEAD^2 differs, so
  a push during the runner wait cannot smuggle in unreviewed code
- validate each principal separately: an empty author vanished in word
  splitting and left only the commenter checked
- honor MAINTAINER_ECS_RUNNER_DISABLED with an explicit notice instead
  of queueing forever against a disabled pool
- status comments carry a machine state marker; inferring 'running' from
  prose let a report quoting that sentence be overwritten
- previous-report.md snapshots the newest substantive report, never a
  weak/cancelled notice, so prior findings survive into the next round
- bot-identity lookup failures fail closed instead of widening the
  ownership filter to every user's comments
- publish-verify uses a per-run concurrency group: a per-PR group holds
  only one pending job, so a second /verify could cancel a completed
  run's pending publisher

Correctness:
- install/build failures are classified: signals, ENOSPC, registry and
  network errors are infra-error, not a PR verdict
- watchdog classification measures the child's own elapsed time, not
  shell-global $SECONDS which includes proxy setup
- assertions.json must be three non-negative integers with a positive
  total and total == pass + fail before it counts as evidence
- the proxy keeps its upstream deadline armed until the body ends and
  aborts upstream when the client disconnects
- cleanups remove .qwen/tmp itself: PR code can make it a symlink, and
  globbing below it deleted the target's contents as root (verified)
- emit_block materializes the escaped text and truncates on a character
  boundary via node — iconv -c passes an incomplete trailing sequence
  through on BSD (measured), which the new test caught

Skill: local mode requires the same isolation CI provides and must not
assume HEAD^1/HEAD^2 on a plain head checkout; shallow boundaries make
rev-list counts unreliable for per-commit claims; never run
scripts/lint.js with no arguments (it runs prettier --write and rewrites
the tree under the harnesses); a vacuity check must fail the intended
assertion, not the import. pr-workflow.md now says both sandboxed lanes
need the author to have write, so triage stops recommending a
guaranteed denial on external PRs.

Tests: 9 more guards, all mutation-verified, including executable
replays of the docs-only classifier (SIGPIPE + executable-markdown
cases), the uppercase-command gate, the empty-principal deny, and the
untrusted-image hosting path against a bare pr-assets remote.

* test(triage): pass the classifier fixture through a file, not argv

The new docs-only classifier replay passed on macOS and failed on CI
with `Cannot read properties of undefined (reading 'trim')`: its
60,001-entry fixture is ~889 KB and was passed as a single argv element.
Linux caps one argument at MAX_ARG_STRLEN (128 KB), so the spawn failed
with E2BIG and stdout was undefined; macOS has no per-argument limit and
only a ~1 MB total, so the same call succeeded locally (verified both).

Write the list to a temp file and pass the path. The harness now also
asserts the spawn succeeded, so a future spawn failure reports itself
instead of surfacing as a TypeError on undefined output.

* fix(triage): make the /verify report match what the run actually produced

Three publisher findings, all introduced by my own previous round:

- an artifact download failure (the step is continue-on-error) let the
  full-report path run with no results: the headline read 'completed' and
  the scope paragraph claimed the A/B, the harnesses and the gates had
  run when nothing had been delivered. The download outcome is now an
  input, and its failure gets its own body saying the results could not
  be retrieved
- the prepare-failure branch ignored the verdict the prepare step had
  just computed, so an install killed by a registry outage or OOM
  (classified infra-error) still told the author 'this is treated as a
  PR failure verdict rather than an infrastructure failure' — the exact
  opposite. It now branches on the verdict, and an infra-classified
  prepare failure is a weak body that cannot overwrite a real report
- weak notices were being snapshotted as the follow-up round's
  previous-report.md: they lack the running marker, so 'newest
  non-running comment' selected them. Bodies that carry findings now
  mark themselves (qwen-triage:verify-substantive) and the snapshot
  selects on that marker. A/B on the real jq: report A then cancelled B
  now snapshots A (101), the old filter picked B (102)

Tests: 4 more guards, all mutation-verified — the publisher is rendered
for each outcome with a stubbed gh and the assertions read the body it
would post, and the snapshot test runs the workflow's own jq program
verbatim against a paginate-shaped fixture.

* fix(triage): stop PR build output from masquerading as an infra failure

Two review findings plus a test-helper hazard:

- classify_failure grepped the prepare log for bare words like ENOSPC
  and ETIMEDOUT, but that log is written by PR-controlled code: a
  genuine build failure that merely prints 'expected ETIMEDOUT to equal
  ok' would be published as an infrastructure incident, telling the
  author to re-run something that fails identically. The patterns are
  now anchored to lines only npm's reporter or the kernel emits
  ('npm ERR! code E…', 'npm ERR! network …', kernel OOM, bare 'Killed');
  a signal exit still needs no log evidence. Replayed 10 cells: four
  PR-authored logs quoting infra words stay 'fail', five real
  diagnostics and one signal exit are 'infra-error'
- the two execution-time controls added last round — re-verifying the
  author's permission after the runner wait, and refusing a head that
  moved since authorization — had no tests. Both are now executed:
  the re-auth snippet against a stubbed permission API (write proceeds
  and pins head_oid; read skips with a publishable reason), and the pin
  step against a real git repo with a real merge commit (matching head
  proceeds, moved head exits non-zero)
- add a stepIn(job, step) test helper. Several step names exist in both
  the tmux and verify jobs, and the unscoped step() returns the first
  match, so a verify-lane assertion silently tests the tmux copy — that
  has now bitten this suite three times, including in this commit.

* docs(triage): teach verify-pr test-only PRs, differential oracles, gate liveness

Fold techniques from the round-2 verification on #7620 (an ANSI parser
PR) that the skill had no equivalent for:

- test-only PRs get their own method: a mutation A/B across TEST FILES
  (same mutants of the unmodified production file, only the test file
  swapped), reporting killed/total on both sides, requiring that no
  mutant regressed from killed to survived, checking that the killing
  assertion is the one the commit claims to have strengthened, and
  adjudicating every survivor as coverage gap or defect with independent
  evidence rather than by inspection
- when the code emulates a known implementation, that implementation is
  the oracle: feed identical input to both and report disagreement
  counts per side, lift reference tables verbatim out of the shipped
  dependency, and build the corpus from bytes captured off a real
  producer alongside synthesized sweeps
- prove a gate is live before citing it: plant a violation the linter
  must catch, confirm it is reported, remove it — a linter that matched
  no files exits 0 exactly like one that passed
- attribute pre-existing failures by byte-identical failing file AND
  test names on both sides, with deltas, not just totals
- when the base is far behind, verify the merge: trial-merge into
  current main, confirm it is conflict-free, and re-run the affected
  suite on the merged tree
- round continuity gains its one legitimate shortcut: a production file
  proven byte-identical (sha256 quoted at both heads) carries prior
  evidence forward by construction

* style(triage): reflow verify-pr skill to prettier's markdown wrapping

The previous commit's added paragraphs were hand-wrapped and prettier
--check flagged the file; the repo runs prettier over all of it.

* test(triage): cover the disabled-runner-pool notice

The kill-switch path had no test: a refactor could drop the notice and
leave a /verify request acknowledged with 👀 but permanently unanswered,
since the verify job refuses to start and publish-verify skips with it.

Fold the step into the existing PR-guard loop (now scoped through
stepIn, so it cannot match a same-named step in another job) and assert
the parts that make the answer useful — the kill-switch and permission
conditions, both languages, the alternative it points at, and the verify
job's own exclusion of the disabled pool. All three mutations turn it
red: removing the step, dropping its PR guard, or letting the verify job
queue against the disabled pool.

* fix(triage): repair a step-killing PIPESTATUS read and six forgeable controls

Sixth review round, 12 findings. Several are regressions from my own two
previous rounds; the first would have broken every single run.

- `AGENT_STATUS=${PIPESTATUS[0]}` is itself a command and resets
  PIPESTATUS, so the next line's ${PIPESTATUS[1]} was unset and `set -u`
  aborted the step immediately after the agent finished — before artifact
  collection, the verdict, or anything else. Verified by replaying the
  exact structure: 'PIPESTATUS[1]: unbound variable'. Both elements are
  now snapshotted in one command
- concurrency predicates were broader than the job conditions they guard,
  and GitHub evaluates concurrency BEFORE the job `if`: a /verify comment
  entered the triage job's shared per-PR group (where it could displace a
  pending /triage and then skip), and a /verify queued while the runner
  kill switch was on did the same to a real verification. Both predicates
  now match their job's runnable set exactly
- an outward-resolving .git/hooks entry was only warned about and left in
  place, so the next root-owned git command would run it. It is now
  unlinked without traversing its target, a root-owned hooks directory is
  restored, and core.hooksPath is unset
- the second .qwen pin re-derived HEAD^1 from git metadata after the
  workspace, including .git, had been handed to the build user. The base
  OID is now recorded while .git is still root-owned and the re-pin
  archives that content-addressed OID
- classify_failure took both of its inputs from PR-controlled sources: a
  lifecycle script can exit with a signal status and can print any line
  the log patterns matched, turning its own deterministic breakage into
  'infrastructure, please re-run' — which hid the failure and preserved a
  stale report. No infra verdict is derivable there, so the prepare step
  reports `fail` and lets the embedded log speak for itself
- cleanups descended through PR-writable parents: `.qwen` itself can be a
  symlink, and the worktree sweep trusted git metadata with only a lexical
  prefix check. Symlinks are unlinked without traversal and worktree paths
  must canonicalize inside the workspace. Replayed all three escapes
- skipped and docs-only outcomes upload no artifact, so the new
  download-failure branch pre-empted them and made their real reason
  unreachable; they are answered first now
- a run that crashed before writing report.md still claimed the
  substantive marker, letting a headline overwrite the previous round's
  evidence. The marker now requires a report

Skill: the byte-identical shortcut needs the whole input closure, not one
file hash; the credential-free local path cannot call `gh` at all (fetch
the metadata outside and mount it read-only); and the A/B base is
`baseRefOid` in local mode, not `HEAD^1`.

Tests: 7 new guards plus 4 updated to the new shapes, all
mutation-verified (50/50).

* fix(triage): answer dropped /verify requests and prove the proxy rejects

Maintainer review (yiliang114), 7 items:

- a third /verify while two runs are in flight is dropped by the
  concurrency group with no job and therefore no comment. The hosted
  authorize job now counts this workflow's other in-flight runs and says
  so; an API hiccup leaves the request alone rather than denying it
- the proxy's bearer check had no executable test. It now starts the real
  proxy against a real upstream and issues real requests: no header and
  a wrong token are 401, this run's token is 200, and a route other than
  /chat/completions is 403 — with the health endpoint echoing the nonce
- the 502 path forwarded the raw upstream error, which can name resolved
  hosts and TLS detail to PR code. It logs server-side and returns a
  generic failure
- publish-verify inherited the 360-minute default; it downloads one
  artifact and posts one comment, so it is bounded at 10
- removing the log classifier last round left the comment block it
  replaced, which still said failures are classified from the exit status
  and the log. Deleted
- that removal also left every install failure reported as the PR's
  fault, including a registry outage. There is exactly one signal here PR
  code cannot write — asking the registry ourselves, as root, with the
  container's resolver — so an install failure is downgraded to
  infra-error only when that probe fails. It proves reachability now
  rather than at failure time, so it can only ever downgrade, never
  confirm; a build failure has no equivalent and stays the tree's problem
- the skill's local-invocation warning ran into the preceding sentence,
  which GFM renders as one paragraph

Tests: 5 new guards, all mutation-verified (55/55).

* fix(triage): resolve hooks hermetically and mirror symlink guards at job end

Maintainer review round (doudouOUC), 6 findings. Two were Critical and
both reproduced:

- the hooks sweep resolved its path with the ambient git config in play.
  With a global core.hooksPath set — which the reviewer has and I do not,
  which is why my earlier replay showed a false pass — `git rev-parse
  --git-path hooks` returns that global path, the in-git-dir guard reads
  'outside', and a planted `.git/hooks` symlink survives untouched. A/B:
  old code leaves the symlink under a global hooksPath, new code removes
  it in both environments and never touches the link target. Resolution
  now runs with GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM pointed at
  /dev/null
- the END-of-job cleanup still used the bare `rm -rf .qwen/tmp` that the
  start-of-job cleaner was hardened against two rounds ago. The agent
  executes PR code between the two, so the end is no safer than the
  start: it now unlinks symlinks without descending and canonicalizes
  worktree paths inside the workspace before deleting

Plus four suggestions, all valid:

- the saturation notice counted this workflow's in-flight runs across
  every PR while the concurrency group is per-PR, so a run on another PR
  would trigger a warning about a queue that does not exist. It now
  matches on the PR title (the only per-PR handle an issue_comment run
  record carries) and stays silent when that cannot be resolved
- the skill recommended `require.resolve` for the workspace-realpath
  check; these packages are ESM-only with import-only exports, so it
  throws ERR_PACKAGE_PATH_NOT_EXPORTED and reads like a missing module.
  Verified, and replaced with `readlink -f node_modules/@qwen-code/...`
- the symlink-escape test inherited the developer's git config, which is
  what hid the first finding. It now runs with global/system config
  neutralized AND repeats the case with a global core.hooksPath planted
- the publisher's build-phase arm was never rendered by any test (every
  case used 'install'), so a typo in that command name would have
  shipped. Now covered, along with an unrecognized phase

Mutation-verified 4/4. The hooks guard needed a discriminating assertion:
git's own `*.sample` files must survive the sweep, because the
outward-path fallback removes the whole directory and would otherwise
satisfy a bare 'planted hook is gone' check.

* fix(triage): count only /verify runs for saturation, and test the PATCH arm

Bot review round, 2 suggestions, both valid:

- the saturation notice matched runs by PR title, which narrowed to this
  PR but not to /verify. /triage and /tmux live in their own concurrency
  groups, so two of those in flight would warn about a verify queue that
  is actually empty. It now also requires the run to have a job named
  'verify' — the run record carries no command, but its job list does.
  Replayed: two non-verify runs stay silent, two verify runs warn
- every publish fixture returned an empty comments listing, so the PATCH
  arm was never executed: a broken PATCH would have stranded the running
  status comment and posted a duplicate below it, with the suite green.
  The publisher now runs against a stubbed listing and the test asserts
  which verb went to which comment id — bot-owned live status is PATCHed
  in place, an absent comment posts fresh, and a marker comment owned by
  someone else is left alone and posted around

Mutation-verified 3/3: counting every command, never PATCHing, and
accepting foreign-owned markers each turn one test red.

Two stub bugs found while writing these, both mine and both silent:
${*#pattern} applies per positional parameter rather than to the joined
string (yielding a wrong run id), and the paginate fixture needs one
array per page, not an array of pages.

* fix(triage): fix the real silent drop and drop the step built on a wrong premise

Review round 4. The blocker was mine twice over: the saturation notice I
added last round had GitHub's concurrency semantics backwards, and the
silent drop it claimed to cover was somewhere else entirely.

- GitHub cancels the OLDER pending run in a group and admits the new one
  (confirmed against the workflow-syntax reference). My step told the
  person who had just typed /verify that their request might be dropped,
  when theirs is the one that runs — and said nothing to the person whose
  queued run actually died. This PR already had it right in
  publish-verify's own comment, so the file contradicted itself and the
  user-facing copy followed the wrong half. The step is removed rather
  than reworded: with the fix below there is nothing left for it to warn
  about, and it cost 2+N API calls on every /verify.
- the actual drop: a verify job cancelled while still PENDING never
  reaches a runner, so its outputs block — where the
  "|| github.event.issue.number" fallback lived — is never evaluated.
  publish-verify then read an empty PR_NUMBER, hit its own guard and
  exited 0, making the cancelled branch unreachable in exactly the
  scenario that produces cancellations. The fallback now lives where the
  value is read. Reproduced both arms by executing the real step: with a
  number the cancelled notice posts, with an empty one it only warns.
- same one-line class in publish-tmux, fixed alongside.

Two copy defects from the classifier removal, both mis-attribution
pointed the other way:

- the infra-error body still named a signal/OOM kill and a full disk,
  none of which the current prepare step can produce — infra-error now
  requires npm ci to fail AND the registry probe to fail. It names that
  condition only, and offers a re-run instead of asserting it is the fix.
- the code comment above it still described the deleted classifier.

Also fixes the indentation break an earlier scripted edit left in the
publish body builder, and replaces the saturation test with one that
executes the cancelled path. Mutation-verified 2/2; the copy needed its
own guard, since reverting the wording alone left every test green.

* docs(triage): teach verify-pr survivor accounting and observability regressions

Fold techniques from the re-verification on #7709 that the skill had no
equivalent for:

- the mutation matrix must report the mutations that changed NOTHING, not
  only the ones that failed. Each survivor gets classified as an ordinary
  coverage gap or as dead code — a guard whose deletion leaves every test
  green is one of those two, and the difference is what the author needs.
  Survivors mirroring a pre-existing gap are labelled as such, and the set
  is framed as completeness reporting rather than merge conditions
- the sharper case that report demonstrates: a test that passes for the
  WRONG REASON. If deleting the new guard leaves its own new test green,
  that test is pinned by an earlier early-return, not by the change, and
  asserts nothing about it. Name what actually pins it
- and do not generalize from one dead guard to its siblings: the same
  report shows a clause that is unreachable on one path while being the
  only protection on another. Check each, report the contrast
- observability regressions: when a change suppresses output, follow the
  value before calling the suppression correct. A bare catch on the path
  plus a field with no readers anywhere in the repo means the cause is now
  unobservable even in devtools — a real loss that no behavioural
  assertion can see
- report structure gains a Corrections section: when an earlier round or
  bot comment described the code inaccurately, state the correct fact with
  evidence and label it as a correction to the description, not a request
  to change code. A wrong description left standing costs the next reader
  more than the original finding did

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-26 13:44:28 +00:00
Shaojin Wen
172a567a30
ci(autofix): number the status comment like every other round message (#7748)
`effective_round` counts rounds already finished, and "Push and report"
posts ROUND + 1 as the round it just performed. The status comment used
the raw value, so the same round showed two numbers in one thread —
observed on #7724 at 10:10 UTC: the report said "round 6/100" while the
status said "AutoFix round 5 finished".

Display ROUND + 1 in both status messages, guarded so a missing or
non-numeric round degrades to the raw value instead of failing the step.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-26 12:38:19 +00:00
Shaojin Wen
cfd7c104bf
ci(autofix): show a live-progress status comment while a round runs (#7738)
* ci(autofix): show a live-progress status comment while a round runs

Takeover engages, and then the PR thread goes quiet: review-address runs
the agent for up to 80 minutes plus a verification gate, but nothing
reaches the thread until "Push and report" at the very end. Observed on
#7731 — 43 minutes of silence with no way to tell a working round from a
stuck one. The agent's output already streams live to the Actions log;
only the link was missing.

Announce the round up front with that link, and flip the same comment to
a terminal state when the round ends. Upserted by marker so one comment
per PR is edited each round (edits notify nobody) instead of stacking
against a 100-round cap. Both steps are gated on the stale-duplicate
flag: the per-PR concurrency group runs a discarded duplicate AFTER the
real round finalised, so an ungated finalize would overwrite that round's
"finished" with its own "ended without publishing". Best-effort
throughout — a failed status post warns and never costs a round.

* ci(autofix): hand the status comment id to the finalize step

Addresses review feedback: the announcement and the finalize each ran
their own paginated comment scan, twice per round on a PR that can
accumulate hundreds of comments over 100 rounds.

The announcement already knows the id — it either found one or just
created one — so it now writes it to $GITHUB_OUTPUT (capturing the id
of a freshly posted comment via --jq) and the finalize consumes that.

The finalize's scan is removed outright rather than kept as a fallback:
an empty id means this round never announced, so no comment claims the
round is working, there is nothing to flip, and a previous round's
comment is already terminal (the next round's announcement re-PATCHes
it either way). One scan per round instead of two, and less code.

* test(autofix): assert finalize step error guards and dry_run gate (#7738)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix[bot]@users.noreply.github.com>
2026-07-26 08:13:34 +00:00
Shaojin Wen
0f56e35c0a
ci: keep the critical-audit gate honest when npm cannot answer (#7743)
npm retired the `security/audits/quick` endpoint, which now 400s for this
package tree. `npm audit` exits 1 for that exactly as it does for a real
critical finding, so every PR in the repo went red on 2026-07-26 with
"Invalid package tree" — a failure no branch can fix.

The exit code alone cannot gate a merge: treating every non-zero as
vulnerable blocks the repo on an npm outage, and ignoring it retires the
gate. The payload separates them — a real audit always carries
metadata.vulnerabilities, a transport failure carries the request error.

Classify on that: a finding still fails, an unreachable endpoint warns and
passes, and anything unrecognised fails closed so a payload-shape change
gets a human rather than a silently disabled gate.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-26 06:59:13 +00:00
Shaojin Wen
3d395606ae
fix(triage): only the bot's own approval counts as already approved (#7737)
A maintainer approved PR #7620 three minutes before re-triggering
`@qwen-code /triage`. The run reviewed the PR, scored it 5/5, and then
reported " Approved (5/5) — existing approval from prior run still
valid, head SHA unchanged" without ever calling the approve API. The
approval it read was the maintainer's, on the same head commit; the
bot's own latest review there was a `/review` downgrade to COMMENTED,
and its earlier approval had been dismissed by a push. The PR sat at 1
of the 2 required approvals with nothing in the run log marked wrong.

The skill documents an "already exists, skip re-submitting" rule for
CHANGES_REQUESTED only, and that snippet filters on the bot's login.
Nothing covered approvals, so the rule was generalized to them with the
author filter dropped. Since the maintainer's habit is to approve and
then ask triage for the second vote, this reproduces on every re-run.

Spell the approval check out instead of leaving it to inference: the
skip applies only to the bot's own APPROVED review pinned to the exact
reviewed commit — another account's approval is a different vote, a
DISMISSED review is not an approval, and an approval on an earlier
commit was already voided by the push. Keep the skip itself, so three
re-runs still don't stack three approvals.

Back it with a workflow check, since the failure is silent by nature.
"Notify silent triage re-run" already detected that no review was added;
it just said so in wording that read as a normal ending. It now reports
whether the bot has a review of its own on the head commit, and warns
when it does not.

Also paginate the CHANGES_REQUESTED probe. An unpaginated read sees only
the first page, and re-runs happen on exactly the heavily-reviewed PRs
where the gating review has scrolled past it.

Co-authored-by: verify <verify@local>
2026-07-26 03:43:34 +00:00
jinye
8fa8085036
perf(core): Lazy-load first-use dependencies (#7686)
* perf(core): Lazy-load first-use dependencies

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

* test(core): Fix simple-git loader mock

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

* test(core): Cover abort during xterm load

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

* fix(core): Address lazy-loader review feedback

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

* fix(core): Validate lazy dependency module shapes

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-26 03:04:33 +00:00
OrbitZore
62e009a952
feat(channels): GitHub polling adapter with notification-as-wakeup architecture (#7632)
* feat(channels): add GitHub polling adapter with notification-as-wakeup architecture

Introduce a GitHub channel adapter that monitors notifications and
responds to @mentions on issues/PRs by posting comments. Uses
last_read_at as a per-thread watermark for comment enumeration,
replacing the unreliable latest_comment_url approach.

Foundation changes to ChannelBase:
- sendThreadMessage for thread-targeted delivery (IM adapters unchanged)
- Envelope.metadata appended to prompt after command parsing
- chat_thread session scope (channel:chatId:threadId) prevents
  cross-repo session collision
- polling-helpers: testBotMention/stripBotMention (separate detection
  from stripping, no whitespace collapsing), cursor persistence,
  abortableSleep

GitHub adapter design:
- Notifications as wake-up signals only (unread filtering)
- listComments enumeration with last_read_at watermark
- Bot self-comment filtering, case-insensitive mention regex
- In-memory recentlyProcessed set for mark-read failure dedup
- First-contact: new issue body @bot triggers processing
- Error comment + cursor advance on handleInbound failure
- pollInterval minimum 60s, exponential backoff 2s-30s

* refactor(channels): extract PollingChannelBase from polling-helpers

Replace the loose polling-helpers module with a PollingChannelBase<Cursor>
abstract class that encapsulates the poll loop, cursor persistence (JSON,
atomic write), exponential backoff, and start/stop lifecycle. Subclasses
implement only pollOnce() and createInitialCursor().

- Delete polling-helpers.ts (cursor fns + abortableSleep moved into base)
- Move mention utilities (testBotMention/stripBotMention) to github pkg
- GithubAdapter now extends PollingChannelBase<{ lastProcessedAt }>

* fix(channels): remove Gitea/GitLab mention from sendThreadMessage JSDoc

* fix(channels): match /pulls/N in notification subject URL

GitHub PR notifications use /repos/{owner}/{repo}/pulls/{N} in
subject.url, not /issues/{N}. The regex only matched /issues/,
causing PR notifications to be skipped and marked read.

Also sets threadId to 'pr:N' for PRs (was always 'issue:N').

* test(channels): add PR body first-contact unit test

Verify that PR notifications with @mention in the body (not a comment)
correctly trigger the first-contact path: extractFromSubjectUrl matches
/pulls/N, listComments returns empty, tryFirstContactBody fetches the
PR body and dispatches to handleInbound with threadId 'pr:N'.

* feat(channels): read pollInterval from channel config in PollingChannelBase

Move pollInterval config reading from GithubAdapter to the base class.
The user's configured pollInterval in settings.json is now respected
directly without a minimum enforcement. Defaults to 60000ms when not
configured.

* fix(channels): prepend metadata before prompt text

Agent sees issue/PR context (type, title, URL) before the user's
request, improving comprehension. Metadata is still appended after
slash-command parsing so commands are not affected.

* refactor(channels): route all ChannelBase delivery through sendThreadMessage

Replace all internal sendMessage calls with sendThreadMessage, passing
envelope.threadId (or target.threadId / undefined) so polling adapters
can deliver to the correct thread. IM adapters are unaffected — the
default sendThreadMessage falls through to sendMessage.

* docs(channels): document sendThreadMessage delivery architecture

* fix(channels): address review findings

- Cap recentlyProcessed Set at 10k entries to prevent unbounded growth
- Validate cursor JSON shape (non-null object) in loadCursorFromDisk
- sendThreadMessage falls through to sendMessage when threadId is
  undefined instead of silently dropping
- Remove duplicate pollInterval from GithubConfig (now in ChannelConfig)
- Fix chat_thread routing key trailing colon when threadId is undefined

* docs(channels): fix metadata JSDoc — prepended, not appended

* fix(channels): use recentlyProcessed dedup for first-contact body

Replace the fragile createdAt-vs-cursor check in tryFirstContactBody
with the recentlyProcessed set. The cursor advances globally based on
notification updated_at — when a different notification with a later
updated_at is processed first, the cursor can advance past the issue's
created_at, causing the first-contact check to incorrectly skip the
issue body (forget reply bug, found in E2E TC-2b).

* refactor(channels): two-layer dedup for GitHub adapter

Layer 1: global cursor filters notifications by updated_at (sorted
ascending, old first). Layer 2: server-side last_read_at filters
comments by created_at (sorted ascending).

- Delete recentlyProcessed Set (no longer needed)
- Sort notifications by updated_at ascending before processing
- Sort comments by created_at ascending before processing
- Pass latest comment created_at to markThreadAsRead as last_read_at

* fix(channels): address review findings on GitHub adapter

Blockers:
- sessionScope: add defaultSessionScope to ChannelPlugin, apply in
  parseChannelConfig so router and adapter agree on 'chat_thread'
- channel-registry.test.ts: add 'github' to expected type list

Should-fix:
- Replace per-thread markThreadAsRead (PATCH) with bulk
  markNotificationsAsRead (PUT /notifications + last_read_at).
  API errors stop the batch without marking failed notifications
  read; handleInbound errors still advance (error comment posted).
- connect() throws on bot identity failure instead of failing open
- metadata appended after promptText (inside sender attribution)
- isSharedSessionTarget includes 'chat_thread' scope

Nits:
- startPollLoop re-entrancy guard
- clean-package-build-artifacts.js includes github
- index.ts re-exports GithubChannel

* fix(channels): use max updated_at of all fetched notifications as last_read_at

Prevents re-fetching the same notifications in the next poll cycle.
The bulk PUT /notifications marks all fetched notifications as read
up to the max updated_at, regardless of per-notification success.

* fix(channels): address review round 2 findings

- #12: loadCursorFromDisk rejects arrays
- #13: pollInterval validates positive finite number
- #19: first-contact gate uses dispatchedMention flag (not newComments.length)
- #25: stripBotMention no longer trims (preserves indentation)
- #27: remove adapter-level requireMention, unify on GroupGate
- #31: add chat_thread SessionRouter routing key tests
- #33: clear metadata on collect-mode synthetic envelope
- #35: fix PollingChannelBase.test import path
- #36: add @octokit/rest to 15-channel-adapters.md dependencies

* docs(channels): document known limitations for GitHub adapter

- First start skips existing unread notifications (cursor = now)
- Requires classic PAT (fine-grained PATs lack notifications API)
- PR review comments not enumerated (issue comments only)

* fix(channels): address review round 3 findings

- #9: buildMetadata derives web URL from baseUrl (GHE support)
- #12: sendThreadMessage throws on invalid threadId format
- #19: mention lookbehind matches cc:@bot and "@bot" patterns
- #23: cursor file name uses sha256 hash to prevent collision
- #26: test verifies cursor persistence to disk
- #31: postErrorComment double-failure logs to stderr
- #45: tests use mkdtempSync isolation instead of real QWEN_HOME

* fix(channels): pass threadId through pairing flow + sendResponseMessage test

- #13+16: onPairingRequired receives envelope.threadId and passes it
  to sendThreadMessage, so pairing codes are delivered on threaded
  channels (GitHub) instead of throwing
- #6: add test verifying sendResponseMessage resolves threadId from
  router.getTarget and passes it to sendThreadMessage

* fix(channels): pass proxy to Octokit for daemon-worker environments

- #44: read this.proxy from ChannelBaseOptions and pass
  HttpsProxyAgent to Octokit request.agent, matching the
  Telegram adapter pattern

* fix(channels): address review findings — immutable senderId, comment time window, validateCursor, retry wrapper

- senderId uses immutable user.id; allowedUsers resolved to IDs at connect
- Comment filter upper bound: updated_at <= maxUpdatedAt (batch window)
- Per-notification errors use continue (best-effort), not break
- validateCursor() virtual hook for subclass cursor shape validation
- sendThreadMessage/postErrorComment wrapped in githubApi() retry
- webOrigin handles default api.github.com → github.com
- Docs: classic PAT only, markNotificationsAsRead, dedup claims removed
- Tests: threadId priority, metadata consumption, defaultSessionScope,
  QWEN_HOME isolation, persistent mock rejection

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

* fix(channels): mark notifications read before processing to prevent duplicate replies

Bot's own replies bump notification updated_at past the pre-captured
maxUpdatedAt, so markNotificationsAsRead(maxUpdatedAt) failed to mark
them read — the next poll re-fetched the same comments and replied
again.

Move markNotificationsAsRead + cursor advance before the processing
loop (best-effort delivery). This is safe because bot's own comments
do not flip notifications back to unread. Update docs to reflect the
new poll cycle order and best-effort semantics.

* fix(channels): update sender gate after allowedUser ID resolution and harden tests

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

* fix(channels): cursor-based comment window to prevent duplicate replies

PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.

Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window: (windowSince, maxUpdatedAt].
Comments already eligible in a previous poll are excluded regardless
of whether the mark succeeded. Zero new persistent state.

* fix(channels): cursor-based comment window to prevent duplicate replies

PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.

Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window, with per-notification last_read_at
as the preferred lower bound when available (server-side per-thread
watermark). Comments already eligible in a previous poll are excluded
regardless of whether the mark succeeded. Zero new persistent state.

* fix(channels): address review findings — null guard, cursor validation, metadata dedup, abortable sleep, docs

- Guard against null notification.subject.url in pollOnce
- Validate lastProcessedAt is a parseable date in validateCursor
- Add metadata: undefined to second collect-mode drain path
- Refactor abortableSleep as protected method on PollingChannelBase
- Fix docs: requireMention is nested under groups.*
- Add tests: chat_thread shared session, dispatchedBodies eviction,
  cursor enumeration window, last_read_at in mention tests

* docs(channels): sync docs with implementation — cursor shape, error handling, GitHub adapter tables, first-contact

- Design doc: update Cursor to { lastProcessedAt, dispatchedBodies? }, add
  validateCursor date check, abortableSleep protected method, break-on-error
  semantics, subject.url null guard
- Developer docs: add GitHub to adapter table and adapter matrix
- User guide: add first-contact step to How It Works, clarify mark-before-process

* fix(channels): address review round 2 — error dedup, abortable retry, backoff reset, window test

- Record dispatchedBody on first-contact handleInbound failure to prevent
  duplicate error comments when mark-read async hasn't taken effect
- Use abortableSleep instead of raw setTimeout in githubApi retry so
  disconnect() can interrupt rate-limit cooldowns
- Reset consecutiveErrors in startPollLoop so stop/restart cycles don't
  inherit stale elevated backoff
- Add test for cursor window client-side lower-bound exclusion filter

* fix(channels): address review round 3 — cursor validation, error dedup, sender gate, bot-self body

- validateCursor: normalize falsy non-array dispatchedBodies (false/0/""/null)
  to [] instead of passing them through to .includes() which throws TypeError
- Set dispatchedMention after postErrorComment to prevent first-contact from
  posting a duplicate error comment on the same thread
- Only set dispatchedMention when the sender passes the sender gate, so a
  disallowed commenter's mention no longer suppresses a valid first-contact
  body from an allowed issue author
- Skip bot-authored issue bodies in tryFirstContactBody to prevent
  self-response loops under open sender policy

* fix(channels): address review suggestions — test coverage, cursor filename, assertion precision

- Pairing flow: add threadId pass-through regression test
- pollInterval: add table-driven edge cases (0, -1, NaN, Infinity, string)
- Add null-URL notification followed by valid notification batch test
- Fix comment window test to assert paginate call 3 (listComments) not call 2
- Truncate cursor filename encoded prefix to 200 chars (filesystem 255 limit)
- Assert mark-read uses batch maxUpdatedAt, not just { read: true }
- Assert real GitHub plugin declares defaultSessionScope chat_thread
- Add invocationCallOrder assertion for mark-before-process ordering

* fix(channels): address review round 4 — allowedUsers throw on resolve failure, crash table fix, mark-read failure test

* fix(channels): address review round 5 — created_at filter, retry-after NaN guard, retry/sendThreadMessage tests, docs fixes

* fix(channels): address ci-bot review 4778587403 — reconnect idempotency, github type enumerations, retry/webOrigin tests

* chore(channels): align channel-github version to 0.21.0 after upstream merge

* chore(channels): update package-lock.json for channel-github 0.21.0

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

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: OrbitZore <orbitzore@users.noreply.github.com>
2026-07-25 09:31:50 +00:00
jinye
8f667f5bdc
feat(integrations): add retrieval-only external context search (#7586)
* feat(integrations): add direct external context provider

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

* fix(integrations): harden external context failure handling

Preserve provider timeout classification, reject ambiguous Mem0 statuses, release rejected response bodies, and clarify credential and workspace deployment boundaries.

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

* refactor(integrations): narrow external context to retrieval

Limit Phase 1 to one provider-bound search tool, remove hooks and writes, and document the direct profile's actual permission and isolation boundaries.

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

* fix(integrations): harden external context deployment

Pin the managed MCP source through an administrator-owned command-line configuration, document the Direct Profile trust boundary, and remove unused logging/runtime abstractions.

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

* fix(integrations): preserve external context results

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

* fix(integrations): honor provider proxy settings

Install an environment-aware dispatcher before the external context MCP server starts so enterprise egress proxy and NO_PROXY settings apply to provider requests. Document the managed launcher environment and cover startup wiring.

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

* fix(integrations): diagnose invalid proxy settings

Classify proxy dispatcher construction failures as sanitized configuration errors so managed deployments can identify an invalid proxy environment without exposing proxy credentials.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-25 08:18:27 +00:00
Shaojin Wen
638dc9a1c3
fix(triage): resolve finalize PRs from the open-PR list, not the commit association (#7706)
Live verification of the finalize loop (#7693) on its first real fork PR
caught the deferred approval being silently dropped: CI landed green, the
approve-on-green marker was in place, but commits/:sha/pulls returned an
empty list for the PR's current head — the association endpoint is not
reliable for fork-branch commits (workflow_run.pull_requests is likewise
empty for forks). The run logged 'No open PR; nothing to finalize' and
exited, so the approval never posted.

Resolve PRs by filtering the open-PR list on head.sha as the primary
source — it cannot miss the PR a current-head firing belongs to — and keep
the association endpoint as the second source (when it works it also
surfaces PRs whose head moved past the SHA, which powers the stale note).
Union both, deduped.
2026-07-25 07:20:45 +00:00
Shaojin Wen
52f6eaf8f0
fix(triage): resolve stage comment ids by marker at patch time, harden model injection (#7703)
* fix(triage): resolve stage comment ids by marker at patch time, harden model injection

Two hardenings from shepherding #7693, plus a wording fix:

- Re-run comment updates now resolve the target comment id by its stage
  marker (bot-author-filtered, startswith match) immediately before each
  PATCH, instead of trusting remembered ids or list positions. Observed on
  a real re-run: the agent PATCHed the stage=3 comment with stage=1 content
  mid-run before self-correcting — with four bot comments in the thread,
  remembered-id bookkeeping is fragile.
- The model-name injection step previously no-opped silently if the
  'qwen3.7-max' literal ever left the skill (shipping the wrong signature
  in every comment), and corrupted the skill text on model names carrying
  sed metacharacters (/ & \). It now fails the job loudly when the target
  literal is missing and escapes the replacement. Covered by a behavioral
  test that runs the extracted step script against fixture files.
- The finalize status text said 'stage comments above', but the status
  comment is created first, so the stage comments are below it — now
  'in this thread'.

* chore(triage): drop unrelated formatting churn from qwen-triage.yml

The previous commit let prettier rewrite untouched lines (runs-on quoting,
comment spacing) while formatting the edited step. Restore those lines to
main's form; the diff now carries only the injection hardening and the
status wording fix.

* test(triage): pin the stage_comment_id recipe's load-bearing constraints

Guards the startswith match and the bot-author filter in the skill's re-run
comment-id recipe against silent regression — a contains match or a dropped
author filter re-introduces the wrong-comment-overwrite bug this PR fixes.

* test(triage): shim BSD sed only on darwin in the injection test

The extracted step script uses GNU 'sed -i' (the step only runs on ubuntu
runners), but this suite also runs in the macOS merge-queue job where BSD
sed needs an extension argument after -i. Rewrite to sed -i '' on darwin
only — on GNU sed a separated '' parses as the sed script, so the
unconditional rewrite would break the Linux runs that mirror production.
2026-07-25 07:20:01 +00:00
Shaojin Wen
1f9318f974
feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes (#7693)
* feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes

The triage agent's Stage 2b polled pending checks for up to 10 minutes, but
this repo's unit suite runs ~30 minutes, so the poll always burned its full
budget, gave up with 'CI still running', and Stage 3 could then approve before
the suite finished (observed on a PR approved 12 minutes before its Test job
completed).

Split the wait out of the agent entirely:

- pr-workflow.md Stage 2b now forbids polling: fetch check-runs once, report
  pending checks honestly, and wrap the CI table in qwen-triage-ci region
  markers keyed to the reviewed SHA.
- Stage 3 defers a clean-verdict approval when checks are still pending: the
  comment carries an approve-on-green marker instead of an immediate APPROVE.
- New qwen-triage-finalize.yml fires on workflow_run completion of 'Qwen Code
  CI' / 'E2E Tests' and, with plain bash over the API (no model, no checkout),
  rewrites the marked table region with the settled results and posts the
  commit-pinned approval only when every check landed green — failing closed
  on red checks, a moved head, or a closed/draft PR, and flipping the triage
  status comment to say which way it resolved.

Markers are honored only in comments authored by the bot identity itself, and
check names (attacker-influenced on fork PRs) go through the same HTML-escape
chain the skill mandates for file paths.

Stage comments now land ~10 minutes sooner and the approval, when deferred,
lands at CI completion with full evidence instead of before it.

* fix(triage): address finalize review — broken red gate, table truncation, dead trigger

Review findings on the finalize workflow, all reproduced before fixing:

- Blocker 1: the RED jq used the array-first membership form, where | rebinds
  . and .conclusion indexes an array — jq exits 5 every run, RED comes back
  empty, [ "" -gt 0 ] errors, and control falls through to the approve path:
  a red CI auto-approved. The gate now binds the conclusion before the
  membership test (IN(...)), and the counters are numeric-validated so any
  future jq failure reads as 'cannot attest', never 'approve'.
- Blocker 2: the table rendered raw check-runs — on a real PR (96 runs, 35
  names, 68 skipped) alphabetical sort + head -60 truncated away every actual
  test job. table_rows now dedups per name (latest run), drops skipped rows,
  and sorts running/non-green first so the cap can only cut green rows.
  Replayed against the same PR: 96 rows -> 16, unit suite present.
- The approval gate now reads workflow runs filtered to event=pull_request
  (deduped per workflow) instead of head-SHA check-runs, which also carry
  long-running bot orchestration jobs that would wedge PENDING above zero at
  the exact moment the last CI workflow fires — silently dropping the
  deferred approval forever. The skill's Stage 3 PENDING count matches.
- E2E Tests had no pull_request trigger (dead entry); the workflows list is
  now exactly the six pull_request-triggered workflows, so the last finisher
  always re-fires the job.
- Head/state re-check moved before the red/deferred verdicts so a
  cancel-in-progress firing on a stale SHA cannot stamp a red status over
  the new head's comment; the still-deferred branch now updates the status
  comment instead of staying invisible.
- replace_region fails closed when the end-marker text only precedes the
  begin marker (awk END guard) — previously that shape truncated the comment
  body, eating the signature and reviewed-commit footer.
- Region content is deterministic (no run URL) so the no-op cmp works;
  empty run list or unavailable gate skips approval; comment wording fixed
  (workflow_run jobs are attributed to the default branch, so the self-check
  exclusion is belt-and-braces, not load-bearing).

Tests now execute the decision logic, not just grep for it: gate_counts and
table_rows run against fixtures covering every conclusion class, non-PR
events, re-run dedup, skipped filtering, ordering, and both marker-order
failure shapes. 30/30 passing.

* fix(triage): keep a stale finalize firing from clobbering the newer review's status comment

The status comment is deliberately not SHA-scoped (the triage workflow
creates it unscoped; scoping only the finalize side would orphan the
pairing), so a finalize firing for an old SHA that loses the race against a
newer head's green approval would overwrite the  status with a stale
warning. Guard the stale path: when the current head already carries bot
sha= markers (a re-review owns the status comment), stay silent; when the
head moved with no re-review yet — triage does not auto-rerun on
synchronize — the stale note is accurate and still posts. Closed/draft PRs
now just log instead of flipping the status.

* fix(triage): close the guardrail bypass and align the finalize table with the gate

Second review round, all four findings reproduced or confirmed before fixing:

- The approve-on-green marker was emitted in Step 1 while the fork-refactor
  GUARD only ran in Step 2 — a marker that slipped out on a fork refactor
  would have been honored by the finalize job on green CI, bypassing the
  guardrail entirely. GUARD now computes in Step 1 and gates the marker's
  emission, and the finalize job re-asserts it structurally from the PR
  state it already fetched (null head.repo = deleted fork = blocked), with
  a 'guarded' status message instead of an approval.
- table_rows now restricts check-runs to the suites of the same deduped
  event=pull_request workflow runs the gate trusts. Without it, 5 of 8
  rendered rows on this PR's own head were bot plumbing presented as CI
  evidence; with it, 115 raw check-runs reduce to exactly the 3 CI rows.
- A firing that saw PENDING>0 after the approval landed flipped the status
  comment back to 'deferred' with nothing to ever right it; the
  already-approved branch now repairs the status.
- Zero surviving table rows (failed runs fetch, missing suite ids) skips
  the region rewrite instead of blanking the agent's table, and
  replace_region refuses an empty region file (an unchecked getline would
  have deleted the region and its markers unrecoverably).

Nits: the house github.repository guard on the job, the table header
matches the skill template, and the run-URL stays out of the region so the
no-op cmp keeps working.
2026-07-25 00:58:16 +00:00
易良
65b4a5a383
fix(ci): update qwen in the runner's active npm prefix (#7689)
* fix(ci): update qwen in runner npm prefix

* test(ci): cover writable runner prefix install
2026-07-24 19:11:41 +00:00
qqqys
b1ce0c2087
refactor(autofix): extract review verification runner (#7644)
* refactor(autofix): extract review verification runner

* test(ci): follow extracted autofix verifier

* docs(autofix): document the review verification runner env contract (#7644)
2026-07-24 16:46:24 +00:00
Shaojin Wen
66da87d7ad
ci(triage): surface live progress via an early status comment (#7654)
`@qwen-code /triage` runs the agent as one long workflow step, so the PR
thread stays silent until the first stage comment lands — the maintainer
can't tell it started or how far along it is. The agent's output already
streams live to the Actions log; the run link was only surfaced at the end.

Post a `stage=status` comment up front carrying that live run link, and
finalize the same comment (by marker, so a re-run reuses one comment) to a
terminal state at the end. Covers manual, auto (pull_request_target), and
dispatch triage. Best-effort — a failed status post never fails triage.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-24 06:27:53 +00:00
Shaojin Wen
d99ad15af2
docs(autofix): let the agent escalate a maintainer's decision, not decide it (#7636)
The address-review classification had only two dispositions — fix, or
decline-with-reason — so when a finding turned on a judgment that is the
maintainer's to make (a v1 tradeoff, two reviewers wanting opposite
things, whether the problem is worth solving at all), the agent was
forced to either quietly implement one contested direction or decline it
as "out of scope" — both of which ARE deciding.

Add a third disposition: escalate. The agent names the decision, gives
the options and its recommendation, and leaves the thread unresolved so
the maintainer reads a question, not a verdict already reached. It is not
a failure and not "could not address": everything else is addressed this
round and the answer arrives as ordinary new feedback the next round — no
new marker or state, it just rides along in the summary. Distinguishes
decline (the change is not worth doing) from escalate (the call is not
the agent's to make).

Pins the new disposition in the existing SKILL policy test.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-24 05:54:51 +00:00
Shaojin Wen
09f01de881
ci: label a PR that closes an issue its own author opened (#7630)
* ci: label a PR that closes an issue its own author opened

Some PRs fix an issue the PR author themselves reported — self-reported
and self-fixed. That is not wrong, but the problem was never
independently validated, so a reviewer wants to check the issue is real,
not only that the fix is correct. This applies a `review/self-reported`
label so that shows at a glance and can be filtered.

A small pull_request_target workflow, metadata only (it never checks out
the PR's code): it reads the PR's closingIssuesReferences ("Fixes/Closes
#N" plus the Development-sidebar links) and, if any of those issues was
opened by the PR author, adds the label; it removes the label if the
link is later re-pointed or dropped. PR-controlled values reach the
script only through env, never interpolated into the run body.

* ci: single-quote workflow string values for yamllint

The repo's .yamllint.yml enforces quoted-strings (quote-type single,
required). The initial workflow left name, on/types, permissions,
concurrency group, runs-on, and the env values unquoted, failing the
Test job's yaml lint. Single-quote them (double where a value contains
single quotes, block scalar for the if), matching the qwen-fleet-shepherd
style. No behaviour change.

* fix(ci): never strip self-report label on a failed GraphQL query

Track whether the closingIssuesReferences query succeeded (API_OK) and gate label removal on it, so an API blip can no longer masquerade as "no self-reported link" and strip a correct label. Also re-run on synchronize so a commit-message "Fixes #N" link updates the label, add a fail-open regression test, and use the root yaml dependency in the test.

* fix(ci): add timeout-minutes and labelCreated test assertion (#7630)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-07-24 05:50:47 +00:00
Shaojin Wen
868d195b95
docs(autofix): apply Simplicity First when addressing review feedback (#7643)
Review rounds ratchet code upward — each round tends to ADD (a guard, a
comment, a test) to satisfy a finding, with no counter-pressure to
simplify, so PRs accrete over-defensive, over-commented bloat. AGENTS.md
already forbids this ("Simplicity First ... No error handling for
impossible scenarios", "Comments: Default to none"), but the
address-review flow's "implement each valuable finding" never invokes it.

Wire it in: when addressing findings, apply Simplicity First and the
Comments rule (smallest change, no impossible-case guards, no
restate-the-code comments) and, since rounds only add, ask each round
what the change lets you REMOVE. A suggestion whose only effect is more
defense, config, or narration is a Decline, not an auto-implement. The
pre-commit self-audit now also rejects bloat, not just defects. Points at
AGENTS.md rather than duplicating it; pins the wording in the SKILL test.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-24 05:47:06 +00:00
qqqys
cb98102149
ci(autofix): add cross-package contract verification (#7642) 2026-07-24 05:13:04 +00:00
jinye
edfb43e954
fix(sdk-java): Harden daemon transport reliability (#7603)
* fix(sdk-java): propagate daemon event epochs

Pair SSE cursors with the daemon event epoch, learn validated response epochs, and fail closed when the epoch changes during prompt observation.

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

* fix(sdk-java): harden daemon reliability follow-ups

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

* test(sdk-java): cover duplicate SSE event epoch headers

* test(sdk-java): restore retryable admission coverage

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-24 04:22:05 +00:00
jinye
86ecbe04a1
perf(cli): Propagate compile cache to ACP children (#7594)
* perf(cli): propagate compile cache to ACP children

Publish the serve process compile-cache directory so spawned ACP processes can reuse it while preserving user overrides and disabled or unsupported runtimes.

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

* test(cli): handle compile cache enable failures

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

* codex: address PR review feedback (#7594)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-24 04:06:14 +00:00
Shaojin Wen
1697416a90
feat(autofix): auto-recover a PR parked on a stale base (#7602)
* feat(autofix): auto-recover a PR parked on a stale base

#7595 catches a stale-base build failure DURING an address-review round,
but a PR already parked when the base moved under it never gets a round
for it to fire in: green PR checks, no new feedback (the handoff advanced
the watermark), no conflict — so the scan skips it forever. Five managed
PRs were stuck this way, 29-86 commits behind main, each "build failed on
the agent-committed fix"; recovering them took a manual update-branch +
/retry per PR.

The gate-rejection handoff now drops a head-scoped autofix-handoff marker
(only when a real fix was rejected — not a crash/timeout, which produced
no fix to re-verify). The scan reads it and, while it still matches the
live head and that head is behind main, merges main in and re-arms so the
loop re-reads the feedback on a fresh base. Self-limiting: a push clears
the head match, and the update makes it current so behind-main cannot
re-fire. Every API call is fail-safe.

The retroactive counterpart to #7595, closing the "parked when the base
went stale" blind spot.

* test(autofix): cover both-empty heads guard in stale-base unpark (#7602)

* fix(autofix): add fail-safe handler to stale-base recovery comment (#7602)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-23 16:54:20 +00:00
Shaojin Wen
5003ee7a7c
feat(autofix): auto-update a PR red only from a stale, since-fixed base (#7554)
Some checks failed
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (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 / Real daemon E2E / Java 11 (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 Python / Classify PR (push) Has been cancelled
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* feat(autofix): auto-update a PR red only from a stale, since-fixed base

A PR can be red purely because it merged a main that was broken then and
is fixed now — observed twice today: a web-shell TS break and an
agent-registry test, each stranding healthy PRs on a failure with nothing
to do with them. The recovery was manual: merge current main and let CI
re-run. The scan now does that automatically via GitHub's update-branch
(a merge, never a rebase, so no force-push and no dismissed history).

The single safety gate is that the SAME failing check is passing on
current main. That one condition proves both halves at once: the red is
base-inherited (green on main = not the PR's own bug) AND main is healthy
on that check right now (so the merge cannot import a fresh breakage). It
acts only when the PR is also BEHIND main (compare status behind/diverged)
— otherwise the update is a no-op and the red is not stale-base after all.

Self-limiting: after the update the PR contains main's head, so it is no
longer behind and the next scan will not re-update. A failed update (merge
conflict) is logged and the PR is left for a human. Runs before the
feedback logic because a stuck-on-stale-base PR often has no new feedback
at all — it just sits red — which is exactly what stranded #7490.

* fix(ci): move pipefail fallback outside command substitution (#7554)

* fix(ci): guard stale-base update-branch with expected_head_sha (#7554)

* test(ci): pin fail-closed behavior for empty MAIN_HEAD and CMP_STATUS (#7554)

* fix(ci): guard stale-base update-branch with DRY_RUN (#7554)

* test(autofix): repair the merge-resolution test breakage

Resolving the base conflict kept this branch's older CONSECUTIVE_FAILURE
and handoff-decision tests (which predate main's PREPARE_OUTCOME env
plumbing), so both broke, while it correctly re-anchored the stale-base
and infra block extractions.

Take main's test file wholesale — its consec-fail, handoff, infra and
bilingual tests are all current — then re-add this PR's one intentional
test (the stale-base auto-update), and re-anchor the infra test's block
extraction onto the "# Auto-rerun a check that died on INFRASTRUCTURE"
comment so it stops at that block instead of over-extracting past the
now-adjacent stale-base block. Full suite green bar the pre-existing
load flakes (eligibility recheck, permanent API failures terminal).

* fix(autofix): fall through to feedback on failed update-branch; assert CAS param (#7554)

* fix(autofix): address review — fix stale-base gate source, add base dimension, bound repetition (#7554)

* fix(autofix): drop self-contradictory predicate 2, add state/PR_HEAD_OID tests (#7554)

* fix(autofix): address review — identity-gate the stale-base write, correct the green-checks safety claim, per-selector guard test, gate the compare call (#7554)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
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 Bot <qwen-code-bot@users.noreply.github.com>
2026-07-23 15:49:34 +00:00
jinye
e7097d0ef6
feat(sdk-java): Add daemon transport (#7463)
* feat(sdk-java): add daemon transport

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

* codex: address PR review feedback (#7463)

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

* test(sdk-java): stabilize lifecycle lock test

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

* codex: address PR review feedback (#7463)

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

* codex: address PR review feedback (#7463)

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

* codex: address PR review feedback (#7463)

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

* fix(cli): preserve prompt cancellation during context propagation

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-23 12:38:34 +00:00
Shaojin Wen
5cad7fe642
feat(autofix): update a stale base when the gate rejects a behind-main fix (#7595)
A fix that fails to build is not always the fix's fault. #7471 stalled
when the agent's verification gate failed with "Cannot find module
'update-notifier'" — a dependency main removed in #7515, still imported
on a branch 32 commits behind. The loop could not tell a stale-base build
failure from a genuine one, so it advanced past the feedback and asked a
human to take over.

In the gate-rejection branch, before the handoff, compare the checked-out
head with main; if it is behind or diverged, update-branch (a CAS on
REPORT_HEAD) merges main in and the round retries (sentinel ts keeps the
feedback live). It self-limits: after the update the PR is current, so a
next-round rejection is no longer "behind" and falls through to the human
handoff — a genuine fix failure costs at most one base-update. The round
is exempt from the consecutive-failure breaker (not the PR's fault), and
every API call is fail-safe.

This is the agent-gate sibling of #7554, which only sees PR status checks,
never the gate's own build.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-23 11:26:29 +00:00
Shaojin Wen
9d029835fc
test(autofix): single-source the infra-signature list from the workflow (#7565)
* feat(autofix): auto-rerun a check that died on infrastructure, once

A failed check can be red because the machine died, not the code — a
self-hosted runner losing the server, the disk filling. #7490's E2E
failed with "runner lost communication with the server" and went green
on a rerun. The scan now reruns such a check's failed jobs automatically.

Detection is a conservative annotation whitelist (INFRA_FAILURE_SIGNATURES)
— only unambiguous machine failures, never a test-level timeout, which
could be a real regression. The one-shot guard is run_attempt, not a
marker: a run already retried to attempt 2 and still infra-failing is
persistent, so it is left for a human; after a rerun the attempt
increments, so the next scan will not rerun it. Every step is fail-safe
(any API error → no rerun), it runs only when the PR actually has a
failed check, and the gate carries the same review-address carve-out as
the other check selectors so the loop never reruns its own runs.

This is the transient-infra sibling of #7554 (stale-base): that merges
current main when a check is base-inherited; this reruns when a check
died on the runner. Neither touches a check that is a genuine failure.

Note: rerun-failed-jobs needs the PAT to hold `actions: write`.

* fix(autofix): use POSIX ERE groups in infra-failure regex, cover all signatures in tests (#7562)

* fix(autofix): also treat a git fetch/clone transport death as infra

#6506's checkout died mid-transfer — "fetch-pack: invalid index-pack
output" and "RPC failed; curl 92 ... CANCEL" — which then hung the job
into the 20m limit. That is infra, not the PR (it only touches a doc),
and a re-run made it green. But the infra-signature whitelist did not
cover it, so the auto-rerun did not fire and it waited on a human.

Add `invalid index-pack output` and `RPC failed` — the two canonical
git-transport-death phrases — to INFRA_FAILURE_SIGNATURES. A co-present
job-timeout line does not block the match (one matching line classifies
the run), and a BARE timeout with no transport signature is still left
alone, since it can be a real regression. Both new signatures are pinned
in the test's per-signature loop, plus a case on #6506's real composite
annotation and a bare-timeout-is-not-rerun guard.

* test(autofix): single-source the infra-signature list from the workflow

The infra-rerun test re-typed INFRA_FAILURE_SIGNATURES as an inline
mirror of the workflow's env value. Two copies that must be hand-synced
can drift — the test could keep passing against a stale list while
production changed, or vice versa. That is exactly the copy the
git-transport follow-up had to remember to update in two places.

Extract the list from the workflow source instead, the same
extract-from-source idiom the file already uses for NON_BLOCKING_CHECKS,
so there is only one copy and drift is impossible. A toContain guard
fails loudly if the env is renamed or the regex breaks, rather than
letting an empty pattern match every line and silently pass.

* fix(autofix): paginate annotations and filter Autofix runs in infra-rerun loop (#7562)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-23 08:45:36 +00:00
Shaojin Wen
d9f7e1fbe1
feat(autofix): auto-rerun a check that died on infrastructure, once (#7562)
* feat(autofix): auto-rerun a check that died on infrastructure, once

A failed check can be red because the machine died, not the code — a
self-hosted runner losing the server, the disk filling. #7490's E2E
failed with "runner lost communication with the server" and went green
on a rerun. The scan now reruns such a check's failed jobs automatically.

Detection is a conservative annotation whitelist (INFRA_FAILURE_SIGNATURES)
— only unambiguous machine failures, never a test-level timeout, which
could be a real regression. The one-shot guard is run_attempt, not a
marker: a run already retried to attempt 2 and still infra-failing is
persistent, so it is left for a human; after a rerun the attempt
increments, so the next scan will not rerun it. Every step is fail-safe
(any API error → no rerun), it runs only when the PR actually has a
failed check, and the gate carries the same review-address carve-out as
the other check selectors so the loop never reruns its own runs.

This is the transient-infra sibling of #7554 (stale-base): that merges
current main when a check is base-inherited; this reruns when a check
died on the runner. Neither touches a check that is a genuine failure.

Note: rerun-failed-jobs needs the PAT to hold `actions: write`.

* fix(autofix): use POSIX ERE groups in infra-failure regex, cover all signatures in tests (#7562)

* fix(autofix): also treat a git fetch/clone transport death as infra

#6506's checkout died mid-transfer — "fetch-pack: invalid index-pack
output" and "RPC failed; curl 92 ... CANCEL" — which then hung the job
into the 20m limit. That is infra, not the PR (it only touches a doc),
and a re-run made it green. But the infra-signature whitelist did not
cover it, so the auto-rerun did not fire and it waited on a human.

Add `invalid index-pack output` and `RPC failed` — the two canonical
git-transport-death phrases — to INFRA_FAILURE_SIGNATURES. A co-present
job-timeout line does not block the match (one matching line classifies
the run), and a BARE timeout with no transport signature is still left
alone, since it can be a real regression. Both new signatures are pinned
in the test's per-signature loop, plus a case on #6506's real composite
annotation and a bare-timeout-is-not-rerun guard.

* fix(autofix): paginate annotations and filter Autofix runs in infra-rerun loop (#7562)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-23 07:13:42 +00:00
Shaojin Wen
80784e645c
fix(autofix): make the review-address report wrapper lines bilingual (#7569)
The agent's address-summary.md / no-action.md already ends with a
collapsed Chinese translation, but the workflow-appended wrapper lines
around it — the "Addressed/Reviewed the latest feedback" lead-in, the
"Base-conflict check" line, and the "Re-review when you have a moment"
footer — were English-only and sat outside that block. So the posted
comment was only half translated, unlike the takeover-ack comments
(full collapsed Chinese block) and the "model/模型" sign-off in this
same report (already inline-bilingual).

Give each wrapper line an inline Chinese translation, matching the
model/模型 idiom. The English halves are preserved verbatim — the
streak-reset detector globs on "Addressed the latest review feedback"
and "no changes needed", and a test extracts these lines — so behaviour
is unchanged and old English-only comments still match. A new test pins
each English-Chinese pair so a future reword that drops the Chinese
fails. The terminal handoff/failure comment is left English-only for
now (SKILL.md keeps it so by design); that is a separate change.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-23 07:07:44 +00:00
callmeYe
b436855a40
feat(core): propagate trusted daemon invocation context (#7279)
* feat(core): propagate trusted daemon invocation context

* test(cli): update ACP startup expectation

* refactor(core): centralize ACP capability env key

* test(cli): update worktree ACP core mock

* test(integration): run daemon context smoke on PRs

* test(ci): update no-AK smoke expectation

* test(core): cover invocation context isolation

* fix(cli): compare ACP capability safely

* fix(docs): restore GitHub action input names

* fix(core): sanitize private ACP capability from child env

* fix(core): reuse private ACP capability env constant

* test(cli): cover malformed trusted invocation context

* test(acp-bridge): assert exact child environment

---------

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: 易良 <1204183885@qq.com>
2026-07-23 06:49:11 +00:00
Shaojin Wen
5df71c8fd1
fix(autofix): retry an agent timeout instead of advancing past its feedback (#7563)
A timeout evaluated NOTHING — the agent ran out of budget before finishing,
so nothing was committed and the feedback is unaddressed. It was treated as
an evaluated verdict (real ts, watermark advances), which strands that
feedback: the next scan sees "nothing new" and never retries. Observed on
#7471 (round 13/100), a heavily-reviewed 1871-line PR: rounds 11 and 13
timed out, but round 12 pushed — so a timeout is transient far more often
than not, and advancing past it left the round-13 feedback unhandled.

run-agent.mjs now drops an `agent-timeout` signal on result.timedOut, and
the handoff routes it like a pre-verdict crash: sentinel ts (feedback stays
live) and a retry, with a headline that names the real fix at the cap
(split the PR or raise the budget). A PR that PERSISTENTLY times out is
bounded by the round cap and the consecutive-failure cap, so this cannot
loop forever — it just stops treating a one-off budget blip as a verdict.

The loop guard stays terminal (a tool-call loop is a real defect, not a
budget blip). An API error still routes to its own model-key handoff; the
timeout signal is written only when NOT an API error.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-23 05:13:35 +00:00
Shaojin Wen
51cf26e30a
fix(autofix): retry a skipped-Prepare instead of stranding the PR terminal (#7490)
* fix(autofix): retry a skipped-Prepare instead of stranding the PR terminal

A base/infra failure BEFORE the agent runs was misread as an agent crash
and terminated the PR forever. When an early step fails — installing or
building the trusted base, checkout, node setup — the `Prepare branch and
feedback` step is skipped, so NEWEST is empty, and the report step's
"crashed before reading feedback" branch fired: MARK_ROUND=MAX_ROUNDS,
terminal, scan skips it on every future tick.

Observed: a web-shell TypeScript break on `main` failed `Install
dependencies and build` (which builds the trusted base) across a whole
scan batch, and SIX healthy PRs were stranded terminal at round=100 in
one run — including ones at round 9 and 11 that had nothing to do with
the break. `round=100` there is a terminal sentinel, not 100 attempts.

NEWEST-empty now splits on steps.prepare.outcome:
- 'skipped' (an earlier step failed, the agent never ran) is infra/base
  and transient: retry with a sentinel ts so the feedback stays live,
  incrementing the round so a PERSISTENTLY broken base is still bounded
  and stops at the cap (recoverable with /retry).
- 'success'/'failure' (Prepare ran, no feedback produced) is a genuine
  pre-read agent crash: unchanged terminal behaviour.

This is the reverse of the asymmetry #7482 addresses: that bounds a
crash AFTER reading that retried forever; this stops a transient failure
BEFORE reading from going terminal after one.

* docs(autofix): note a pre-Prepare cancel also retries intentionally (#7490)

* fix(autofix): also retry a cancelled/empty prepare outcome, not just skipped

A previous review comment on this PR noted that a job cancelled before
Prepare should retry too. It was right about the intent but the code did
not do it: `steps.prepare.outcome` is 'cancelled' for a cancel and '' for
a job that stopped before Prepare entered the step context — both DISTINCT
from 'skipped', so `== 'skipped'` sent them to the terminal branch, the
same over-termination this PR exists to fix.

Match on "not a real Prepare run" (`!= 'success' && != 'failure'`)
instead, so skipped, cancelled, and empty all retry; only a Prepare that
actually ran to a verdict (success/failure) with no feedback stays
terminal — the genuine pre-read agent crash. Test extended to drive the
cancelled and empty cases (retry) and both real-run outcomes (terminal);
mutation-verified that reverting to `== 'skipped'` reddens the cancelled
case.

* test(autofix): update the pre-read-crash case for the broadened retry

The prior commit broadened NEWEST-empty retry to skipped/cancelled/empty
but left the older 'replays the handoff decision' test asserting the old
terminal behaviour for an unset PREPARE_OUTCOME (which now retries). That
test's terminal cases now set PREPARE_OUTCOME=success/failure explicitly —
the only outcomes that still terminate — so it exercises the genuine
pre-read agent crash rather than the infra/cancel path.

* test(autofix): anchor the skipped-Prepare extraction past the CONSEC block

CI reddened `retries a skipped-Prepare` after main's consecutive-failure
cap (#7482) merged into this branch: that block was inserted between this
decision block and the report `{`, and it calls `gh api`. The test's
`{`-anchored regex over-captured through it, so the extracted script ran
the unstubbed `gh api` and failed. Anchor the end on the same
`# Consecutive-failure` comment the sibling gate-crash test already uses,
so the extraction stops at this decision block's own closing `fi`.

* fix(autofix): exempt skipped-Prepare from the consecutive-failure breaker

A broken base build skips Prepare, producing no API error file — so the
consecutive-failure breaker ran on the new retry path and, after 5
scans, re-introduced the exact mass-stranding this PR exists to prevent.
Exempt pre-agent infra failures (skipped/cancelled/empty outcome) from
the breaker, mirroring the transient 429/5xx exemption: same failure
class (not the PR's fault, self-heals, hits the whole batch). The round
cap + sentinel-ts /retry recovery already bounds a persistently broken
base.

Also trim "checkout" from the retry headlines (checkout failures do not
land in this branch) and hoist the duplicated MARK_TS assignment.

* fix(autofix): reset the consecutive-failure streak on prior infra-failure markers

The streak walker counted prior infra-failure headlines ("AutoFix could
not start —…") as failures, inflating the consecutive-failure count on
subsequent rounds.  A PR with 3 real agent failures, then 3 rounds of
base-build infra failures, then 1 more real failure would trip the
cap-5 breaker even though only 4 rounds were the PR's fault.

Add the two infra-failure headline patterns as reset strings in the
streak walker, alongside the existing push and no-op resets.  The
genuine agent-crash headline ("AutoFix could not start evaluation —…")
is deliberately excluded — it is a real failure and must still count.

* fix(autofix): clarify infra-failure headlines and else-branch comment (#7490)

Address review nits: the retry headline now mentions cancelled runs,
the cap headline says 'reached the round cap' instead of overstating
'could not start for N rounds', the else-branch comment says 'prepare
itself crashed' instead of 'agent crash', and the streak-reset pattern
is simplified now that both infra headlines share the same prefix.

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-23 04:25:30 +00:00
jinye
7c73768fa5
perf(startup): lazy-load Google GenAI SDK on first use (#7512)
* perf(startup): lazy-load Google GenAI SDK on first use

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

* codex: address PR review feedback (#7512)

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

* codex: address PR review feedback (#7512)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-23 02:07:39 +00:00
易良
ce40e33a65
fix(ci): autofix route checks existing labels on non-trigger label events (#7481)
* fix(ci): autofix route checks existing labels on non-trigger label events

When triage adds multiple labels in sequence, per-issue concurrency
cancels earlier runs. If the last label is not a trigger label
(e.g. scope/build-system), the surviving run skips the issue phase
even though the issue already has autofix/approved +
status/ready-for-agent.

Before ignoring a non-trigger label event, check ISSUE_LABELS_JSON
for both required labels. If present and the issue is open, proceed
with the issue phase. Trust was already established when the trigger
labels were applied (both require triage+ permission).

* fix(ci): require trusted sender for label fallback
2026-07-22 13:58:48 +00:00
Shaojin Wen
ca084dd11f
feat(autofix): stop a PR that fails to push for N rounds in a row (#7482)
* feat(autofix): stop a PR that fails to push for N rounds in a row

Under takeover the round cap is 100, which is right for a PR that needs
many PRODUCTIVE rounds. It is wrong for one that fails every round: #6723
ran 7 consecutive failed rounds (3 agent timeouts at 50 min, 4 gate
rejections whose fix broke tests) over 8 hours, heading for round 100,
because it is a 5700-line, 47-file, 5-day-old PR racing a fast-moving
main — every round re-resolves a conflict it cannot finish or that fails
the gate. Retrying at the same per-round budget will not converge; a
human has to rebase or split it.

Adds CONSECUTIVE_FAILURE_CAP (5), distinct from the total round cap. The
handoff step already runs only when a round did NOT push, so it counts
the unbroken run of prior failure markers — stopping at the first push
("Addressed the latest review feedback") or legitimate no-op ("no
changes needed"), either of which proves progress and resets the streak.
At the cap it forces the terminal round even under takeover, with a
handoff that names the real fix (rebase/split, then /retry). Cause-
agnostic: a timeout and a gate rejection both count.

* fix(autofix): address review feedback on consecutive-failure circuit breaker (#7482)

- Fix misleading comment: the walk is oldest-first (API order) with
  reset-on-success, not newest-first with early stop
- Prefer the already-fetched ic.json over a redundant gh api call,
  falling back to the API only when the file is missing
- Filter eval markers by re-arm window (win=) so pre-re-arm failures
  do not immediately re-terminate a re-armed PR
- Add test coverage for the MARK_ROUND == MAX_ROUNDS guard and for
  window-scoped streak counting

* fix(autofix): exempt transient model errors from consecutive-failure breaker (#7482)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-22 12:14:24 +00:00
Shaojin Wen
cbf1c55595
docs(autofix): require evidenced pre-commit verification, not a bare "verified" (#7486)
* docs(autofix): require evidenced pre-commit verification, not a bare "verified"

The skill already said to run build/typecheck/lint/Vitest before
committing, but softly — and #7408 committed a fix with a TS error the
gate then rejected while its summary claimed "verified all 3 commits".
A self-assessment the gate contradicts wastes a whole round.

Strengthens the address-review contract from "run the checks" to:
- actually run them, do not assert them from reading the diff;
- if typecheck or a touched-package test fails, do NOT commit — treat
  the feedback as unresolved (failure.md);
- end address-summary.md with a `## Verification` section listing each
  command run and its result; a bare "verified" is not acceptable.

The framing is structural, not etiquette: the deterministic gate re-runs
the same commands and discards the round on any failure, so skipping them
only moves the rejection later. Pinned by a test so it cannot soften back.

This is the checkable half of "audit before committing" — the
undirected/reverse-audit-until-clean practice does not transfer to an
unsupervised agent (no verifiable stopping condition, and it worsens the
timeouts seen on large PRs), but "run the gate's own checks first and
show the evidence" does.

* fix(autofix): clarify Verification section precedes collapsed Chinese translation (#7486)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-07-22 11:59:59 +00:00
易良
b1eeaae8c1
fix(release): exclude mobile-mcp from core version bump (#7474)
mobile-mcp has its own release cadence and CODEOWNER (@LaZzyMan).
Bumping it in every core release PR forces an extra approval from
LaZzyMan even though the change is a routine version bump.

Exclude @qwen-code/mobile-mcp from the workspacesToExclude list in
scripts/version.js, following the same pattern as @qwen-code/sdk.

Closes #7462
2026-07-22 05:23:48 +00:00
Shaojin Wen
86b4b281ee
fix(autofix): keep a still-red check visible until its head is judged (#7438)
* fix(autofix): keep a still-red check visible until its head is judged

A red check is a persistent STATE, but the scan only counted checks that
failed AFTER the watermark. The moment the watermark passed the failure
the PR went quiet while still red. Measured on the live fleet:

  #6451  watermark 10:55  3 reds completed 09:30, 09:30, 09:51
  #7357  watermark 09:18  1 red  completed 07:59
  #7390  watermark 11:27:37  red completed 11:27:37 — a strict `>` hid it
                             the instant it appeared

All three sat red for hours while every scan logged "nothing new", and
#6451 wrote two consecutive no-ops whose reasoning never mentions the
three failures, because they were not in its feedback at all.

A currently-red check now counts as feedback until the head it ran
against has been evaluated. The address job records that head in its own
`autofix-redcheck` marker — carried inside the eval comment, so no
ts/acted/round parser changes and the agent still never sees it as
feedback — and the scan skips a PR whose recorded head still matches.
That bounds this to ONE look per head rather than every scan, which is
what keeps a permanently-red PR from being re-selected forever.

The head comes from the REMOTE, not local HEAD: after a rejected push
the two differ, and recording a sha that never landed would suppress the
reds on the head that actually exists. Empty on failure — matches no
marker, so the reds stay visible.

Two existing count assertions are replaced by the property they stood
for: every check selector in the scan carries the address carve-out.

* fix(autofix): pair REPORT_HEAD with the steps that emit its marker

Review found the assignment had landed in issue-autofix's "Report dry-run
/ failure" step, which emits no redcheck marker and has no ${PR} in scope
— dead code plus a malformed, swallowed API call. Verifying it surfaced a
second half the review did not state: review-address's OWN handoff step
emits the marker at line 3362 with REPORT_HEAD never assigned in that
step, since shell variables do not cross step boundaries. Neither step
sets `set -u`, so it expanded empty and the marker recorded no head —
fail-open, but the handoff path never recorded one.

Deletes the dead assignment, adds the missing one, and rewords the scan
log so the two overlapping counts no longer read as a sum.

The test now asserts the PAIRING per step block — emits iff defines —
rather than counting each kind. Counting was what let this through: both
counts were "right". The first fix for it keyed the sets by step NAME,
which merged the two identically-named "Report dry-run / failure" steps
and still passed with the bug reintroduced; keying by step block catches
it.

* fix(autofix): note fail-closed asymmetry on empty LIVE_HEAD (#7438)

* fix(autofix): close three state-transition gaps in persistent red-check tracking (#7438)

- Forward persistent red checks into agent feedback: the scan selects
  via N_RED_NOW but the prepare renderer only showed checks that failed
  AFTER the watermark, leaving the agent with an empty Failed checks
  section. Add a Still-red checks section with the complement filter.

- Omit the redcheck marker on sentinel/retry handoffs: a sentinel ts
  means the agent evaluated nothing, so recording a judged head would
  suppress the retry the handoff promises.

- Record the checked-out head, not the report-time remote head: capture
  the SHA in prepare before agent mutations and forward it as a step
  output, so a mid-run branch move cannot stamp an unevaluated head as
  judged.

* fix(autofix): test empty-LIVE_HEAD fail-closed path (#7438)

* fix(autofix): discard no-op same-head duplicates in the queued-job stale gate (#7438)

Two near-simultaneous scans can both enqueue the same PR with the same
watermark. When the first serialized job ends in a no-op, it records a
redcheck marker for the head it judged but leaves both the eval timestamp
and the round UNCHANGED — so the live-watermark/round revalidation never
fires, and the second job re-runs the agent and posts a duplicate report
for the same head.

Parse the latest live redcheck marker during prepare (mirroring the scan's
RED_HEAD parse) and add its head match against CHECKED_OUT_HEAD as a third
stale-duplicate signature, reusing the existing "nothing newer" revalidation
so newer feedback or a live conflict still keeps the target actionable.

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-22 01:08:23 +00:00
jinye
042f6373b7
perf(startup): Load undici lazily behind package-local dynamic imports (#7455)
* perf(startup): load undici lazily behind package-local dynamic imports

* fix(web-search): preload undici before building runtime fetch options

Address review: web search builds fetch options outside the content
generator preload path, so 33 web-search tests (and any standalone
search invocation) hit the requireUndici fail-loud guard. Also redact
and rethrow proxy dispatcher install failures, guard early promise
rejections against unhandledRejection, and pin the guard message with
a test.

* test(cli): cover loadUndici interop and gitUtils proxy path

Address review suggestions: add parameterized tests for the CJS
unwrap normalization used by both core and cli loadUndici helpers,
and verify getLatestGitHubRelease instantiates ProxyAgent when a
proxy argument is passed.

* fixup! test(cli): fix loadUndici test type errors

Export UndiciModule type and loosen test helper typing so the cli
package builds under tsc --build.
2026-07-22 00:25:52 +00:00
Shaojin Wen
c06da8da28
feat(autofix): feed the gate's rejection back so the retry can fix what it broke (#7368)
* fix(autofix): retry a verification-gate crash instead of burying the agent's fix

A gate failure had two very different meanings collapsed into one outcome. When
the gate DECLARES a verdict (outcome=failed) it evaluated the agent's attempt
and rejected it, so advancing the watermark is right — the same feedback would
reproduce the same rejection, and MAX_ROUNDS bounds it. But when the gate dies
WITHOUT a verdict it never judged the work at all, and advancing buries a fix
the agent had already written: the next scan sees "nothing new" and the PR sits
until a human deletes the marker by hand.

That is exactly how the nested-package ENOENT stranded #7329 and #7336. Both
agents had implemented the review feedback — the handoff even quoted the
implemented changes — but the gate crashed on its own bug while resolving
packages/channels/*, the commit was discarded, and the PRs read as "Could not
address the latest feedback automatically".

Two halves:

- The review-address gate now declares every rejection it can legitimately
  reach: build, typecheck, lint and the per-package tests each call a
  `reject_fix` helper that writes outcome=failed before exiting. (The resolver
  call is deliberately left undeclared — a resolver error IS a gate bug.)
- The handoff treats an EMPTY outcome on a non-success job as the gate's own
  crash and routes it to the existing sentinel/retry path, so the feedback
  stays live and the next scan retries. The round still increments, so a
  persistently crashing gate is bounded exactly as before, and the headline
  names the real cause ("hit a verification-gate error before reaching a
  verdict") and, on the final attempt, points at the gate logs.

Unchanged: a declared rejection still advances and reads as before, a
no-output crash keeps its own wording and retry, and a crash before the
feedback was read stays terminal.

Tests: the real extracted decision block is replayed under bash across declared
rejection (advances to NEWEST), gate crash (sentinel + retry + round+1), no
output (sentinel, original wording), the round cap (operator fix), and a
successful job (never a crash); plus the reject_fix helper is driven for real
to prove a rejection writes outcome=failed. Both mutation-verified — dropping
the crash arm, or unwiring one known rejection, turns them red.

* feat(autofix): feed the gate's rejection back so the retry can fix what it broke

#7208 was handed to a human over a two-character fix. The agent implemented two
review findings, the gate refused the commit because it did not compile
(TS4111: `truncated` comes from an index signature, use `['truncated']`), and
the loop stopped there — round 5/100, "A human should take over this PR".

Nothing in the loop could have recovered on its own, because the reason was
never carried anywhere the loop could read it:

- the handoff comment showed only the agent's optimistic summary, so neither a
  human nor the next round could see WHY it was refused;
- the feedback filter (correctly) excludes the bot's own comments, so a retry
  re-read only the original review points;
- so `@qwen-code /retry` would have re-run the same agent against the same
  input and produced the same non-compiling change.

The compiler had already said exactly what was wrong. The loop just threw it
away. Three pieces carry it instead:

- Each deterministic check now runs through `run_check`, which tees its output
  to a gate log; `reject_fix` writes the label plus the tail of that output to
  gate-rejection.md. (A four-backtick fence keeps captured ``` output from
  breaking out when this is posted as a comment.)
- The handoff comment carries that block between
  `<!-- autofix-gate-rejection-start/end -->` markers, so a human sees the real
  reason next to the summary instead of a report that reads like success.
- `Prepare branch and feedback` lifts it back out of the bot's newest comment
  and puts it at the top of the next round's feedback: "Your previous attempt
  was REJECTED by the verification gate — fix this first."

So a mechanical rejection now closes inside the loop, which is the point of
takeover. A rejection the agent cannot fix still burns rounds and ends at the
same handoff, bounded exactly as before.

Tests: the round trip is exercised end to end — a failing check's compiler
output lands in gate-rejection.md with its label, the handoff delimits it, and
the prepare step recovers the text (markers stripped) from the newest bot
comment while a round that pushed yields nothing to replay. Both halves
mutation-verified. #7351's verdict test is retargeted to run_check.

* fix(autofix): declare the gate verdict before writing its detail file

CI caught this and macOS could not: reject_fix wrote gate-rejection.md
first and outcome=failed second, so a failure to write the detail took
the verdict with it. An empty outcome on a failed job is the signal for
"the gate never reached a verdict" — a crash, which is RETRIED — so a
clean rejection whose detail write failed would be re-attempted every
round instead of being reported once.

The verdict is now written first and the detail write is non-fatal.

The ordering is pinned by a STATIC assertion, not only the behavioural
one: bash 3.2 suspends set -e through a `||`-invoked function and bash 5
does not, so the wrong order runs clean on macOS and aborts on a Linux
runner. That is exactly how it shipped green locally and red in CI, and
a guard that depends on the reviewer's bash would let it happen again.

* fix(autofix): escape the gate-rejection detail for real

The gate-rejection publish site used `sed 's/<!--/<!\-\-/g'` — single
backslashes, which sed reads as escaped literal `-`, so the replacement
is byte-identical to the match and the whole command is a no-op on both
GNU and BSD sed. The other four publish sites use `\\-\\-` correctly.

That mattered: the detail is `tail -c 3000` of build/typecheck/lint/test
output, published verbatim in a bot-authored comment. The scan parses
markers by matching the literal `<!-- autofix-eval ts=`, and it only
counts markers in bot-authored comments — so any check output containing
that string would have been parsed as a real eval marker.

The existing test counted the CORRECT spelling and asserted there were
four of them. A fifth site with the wrong spelling did not match the
counted string, so the count stayed at four and the test stayed green.
It now asserts every `s/<!--/…/g` site is byte-identical to the correct
form, which fails on exactly this bug.

Reported by qwen-code-ci-bot on PR #7368.

* chore(autofix): correct stale "ALL FOUR" escape-site comment to five (#7368)

* chore(autofix): document the head/tail byte-limit invariant (#7368)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-21 13:33:39 +00:00
Shaojin Wen
e6980841a3
feat(autofix): raise the strict round cap from 5 to 10 (#7412)
* feat(autofix): raise the strict round cap from 5 to 10

Measured across the last 40 bot-authored PRs: 17 finished at round 0, 12
at 1, 4 at 2, 2 at 3, 1 at 4, and 3 reached the cap of 5. All three that
reached it merged AT it rather than stalling — and one of those spent two
of its five rounds on the verify-gate ENOENT that #7330 has since fixed.

So the ceiling was never the thing that stopped a PR, but it sat close
enough to bind on a bad day with no headroom. 10 gives that headroom.
The cap exists to stop an unproductive LOOP, not to ration ordinary
iteration; a genuinely stuck PR still stops, just later.

Deliberately not larger: retries for a transient model or gate failure
increment the same counter, so the cap also bounds how much one bad
provider window can spend. API_AUTH_MAX_ROUNDS stays at 3 and still
short-circuits the errors only a maintainer can fix.

Replaces the literal `MAX_ROUNDS: '5'` assertion with the ordering the
numbers must satisfy — auth cap < strict cap < takeover cap — so the
values stay tunable and a cap that stops binding fails instead.

* fix(scripts): anchor round-cap regex with word boundary (#7412)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-21 13:32:43 +00:00
Shaojin Wen
837358f637
fix(ci): tell a triage action crash apart from a silent agent (#7418)
Both no-response cases produced the same error, and its advice — "check
the 'Run Qwen Triage' step stderr above for diagnostics" — pointed at
diagnostics that do not exist for one of them: the action installs the
CLI with `npm --silent`, so an install failure prints an exit code and
nothing else. Observed on a PR whose triage died at exit 243 during that
install, leaving a maintainer told to read an empty log.

The two causes need opposite responses, and steps.triage.outcome already
distinguishes them:

- action failed  -> no model call happened, nothing about the PR can
                    explain it, and the fix is to re-run the job
- action succeeded, empty summary -> the agent ran and returned nothing,
                    which IS worth reading the step output for

Measured baseline for the retry advice: of 19 executed triage runs, 4
failed, 3 of them on unrelated PRs, and one PR both succeeded and failed
within an hour on the same head.

The outcome is passed through env like RESPONSE already is, and the
script is replayed under bash for both branches.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-21 11:07:11 +00:00
Shaojin Wen
32ef628f95
perf(autofix): stop the feedback gate waiting on the LLM review check (#7416)
The scan skips any PR with checks in flight so a FAILED check can be read
as feedback. `review-pr` is not that kind of check: its output is a
REVIEW, delivered by its own real-time pull_request_review trigger and
counted by the review path, so its conclusion carries nothing the loop
acts on — but the PR stayed invisible to the scan for its whole duration.

Measured over 32 completed review-pr runs: median 49 minutes, p75 78,
p90 123, max 158. That is what a PR waited before autofix could touch it,
even when it already had unaddressed feedback.

Only `review-pr` is excluded, by name, via NON_BLOCKING_CHECKS.
Build/test/lint still block, because a failed one IS feedback. So does
`resolve-pr`, which mutates the branch. A test pins each excluded name to
a real job id in qwen-code-pr-review.yml — a rename there would silently
restore the wait with nothing failing.

The behavioral replay caught a real bug before it shipped: the first
version wrote `$nonblocking | index(.name)`, where `.` is already the
array, so `.name` indexed the wrong object and jq errored out — which
would have made HAS_PENDING_CHECKS empty and skipped nothing at all.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-21 11:05:52 +00:00
Shaojin Wen
1b41cbb516
fix(ci): serialise the two workflows that push to a PR head branch (#7392)
`@qwen-code /resolve` and the autofix loop's own conflict path both merge
the base branch and push to the PR's head, but they live in different
workflows, so their per-PR concurrency groups only guarded each against
itself. On #7355 they ran together: /resolve pushed at 03:51, the autofix
leg pushed at 04:05 and was rejected `fetch first`, discarding a full
agent run and leaving no marker behind.

GitHub concurrency groups are repository-scoped, so both jobs now use the
same `qwen-pr-head-write-<pr>` group and queue behind each other instead
of racing. The prefix has to be a literal in both files — job-level
`concurrency` cannot read the `env` context — so a test pins the two
equal; renaming one side alone would silently re-open the race.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-21 07:40:56 +00:00
jinye
636971efad
perf(telemetry): lazy-load the SDK and split OTLP exporter chains by protocol (#7276)
* perf(telemetry): lazy-load the SDK and split OTLP exporter chains by protocol

* fix(telemetry): close lazy SDK init/shutdown races and make load failure non-fatal

Addresses PR #7276 review feedback: shutdown now awaits an in-flight init before tearing down (was racing past the sync flag and leaking a started SDK whose buffered spans/logs never flushed); the dynamic imports now sit inside init's try so a chunk-load failure degrades telemetry instead of aborting daemon runtime startup. Also breaks the sdk<->sdk-impl import cycle via a leaf otlp-urls module, hardens the sdk-node exporter stub for thenable/interop probes with a unit-tested separator-independent resolve, lists the HTTP exporter packages explicitly in the bundle guard, and adds lazy-init lifecycle tests.
2026-07-21 07:35:30 +00:00
Shaojin Wen
7b70d6df5f
perf(autofix): raise fleet simultaneity from 3 to 5 (#7396)
max-parallel is the only place different PRs wait on each other — the
per-scan target budget (10) and the candidate-inspection budget (60) are
both far from binding at the current pool of 15. On the scan that
selected 7 PRs the legs ran exactly 3 at a time, each new one starting
3-4 seconds after a slot freed, so the 7th waited 81 minutes for a slot.

Replaces the literal `max-parallel: 3` assertion with the invariant it
was standing in for: a bound must exist and must still bind below
MAX_TARGETS_PER_SCAN. Pinning the number only detected edits — it would
not have caught the key being deleted outright, which is the actual
regression, and it forced this tuning change to touch a test.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-21 07:22:35 +00:00
Edenman
c33ca7227a
fix(web-shell): restore scheduled task reference interactions (#7313)
* fix(web-shell): restore scheduled task reference interactions

* chore(web-shell): remove PR screenshot artifact

* fix(web-shell): refine scheduled task tag removal

---------

Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
2026-07-21 07:10:26 +00:00
Shaojin Wen
0d5eab418a
fix(ci): stop /resolve reports from being guillotined mid-sentence (#7389)
* fix(ci): stop /resolve reports from being guillotined mid-sentence

Every substantive /resolve summary was hitting the 2000-byte cap exactly
and stopping mid-word: #2993, #4256 and #6206 all ended at 2100 bytes
total, cut inside a sentence, with nothing saying the report had been
clipped rather than abandoned.

Two causes, both fixed:

- The contract asked for a file-by-file inventory, which duplicates the
  diff and grows without bound. It now asks for what only the resolver
  knows — the root cause on the base branch, whether the merge was
  semantic or merely textual, what the resolution's correctness rests
  on, and what it could not verify (this command runs no tests and may
  not touch non-conflicted files, so a merge that breaks an untouched
  test can only be reported).
- The cap was silent and too low. It is now 6000, above the 4000 the
  prompt asks for, and a report that still exceeds it says so.

Also adds the project's collapsed Chinese section to the contract; no
/resolve report had one.

* fix(ci): make the truncation test fatal-decode real and link the run in the notice (#7389)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-07-21 07:02:19 +00:00
Shaojin Wen
afd56117e4
fix(autofix): retry a model API error instead of stranding the PR (#7247)
* fix(autofix): retry a model API error instead of stranding the PR

When the agent's qwen subprocess dies on a model-side [API Error]
(403 access denied, a 429 quota, a 5xx), run-agent.mjs wrote a
handoff/failure.md, so the handoff step treated it as an EVALUATED
handoff — it advanced the watermark and the next scan saw 'nothing
new', stranding the PR until a manual re-arm. But the agent never
actually evaluated the feedback; the model was unreachable.

#7220 hit exactly this: fork-takeover engaged and ran the agent, the
model returned '[API Error: 403 Model access denied]' (the autofix key
lacks access to qwen3.8-max-preview), and the PR was left with an
advanced watermark that will not retry.

Fix, mirroring #7229's no-output-crash handling:
- run-agent.mjs extracts a [API Error: 4xx/5xx] from the captured
  output tail, includes it in failure.md, and drops an
  marker file.
- The handoff step reads that marker and routes the failure to the
  sentinel-ts (retry) path — the watermark does NOT advance, so the
  next scan retries; the round still increments so a PERSISTENT model
  failure is bounded by MAX_ROUNDS. The headline names the model error
  and, on the final attempt, tells the maintainer to check the autofix
  model key/access and re-arm — instead of a generic crash message.

Tests: run-agent.mjs flags a model [API Error] (marker + failure.md)
and does NOT flag a generic failure; the handoff replay treats an
API-error handoff as sentinel|retry (not a watermark advance) with a
model-aware, cause-specific headline. 62/62 + 12/12.

* fix(autofix): scope + broaden the retryable model-API detection (review)

Addresses wenshao's review on #7247:

- Behavioral (1): the agent-api-error marker was written on ANY non-zero
  exit whose output tail contained an API-error string — so a loop
  guard, a timeout, or an agent-written failure.md (a real verdict)
  would wrongly retry and, worst case, silently discard a verdict. The
  write is now scoped to the bare-failure branch and guarded by
  !timedOut, so only an un-evaluated model failure retries.
- Coverage (2): the old regex only matched a LEADING status digit, so
  it missed the canonical rate-limit render, the (Status: …) form, the
  bad-key 401, the Chinese quota text, and the unwrapped Qwen OAuth
  quota — i.e. most real errors this targets. Detection is now a
  whitelist of RECOVERABLE errors (401/402/403/429/5xx + rate-limit /
  quota / api-key / RESOURCE_EXHAUSTED / overloaded phrasings, plus the
  standalone OAuth-quota form); a 400/404 stays terminal.
- Test gap (3): a writer↔reader contract test now runs the REAL
  run-agent.mjs to write the marker, then the extracted workflow reader
  block against that same workdir — a rename on either side (proven
  with the YAML-only mutation) now fails the suite.
- Smaller: API_ERROR_DETAIL is comment-escaped (sed) and capped
  (cut -c1-200) since it derives from agent stdout; the marker match is
  single-line ([^]\n]) so a multi-line render can't smuggle a newline;
  agent-api-error is added to the run-artifacts list.

Non-recoverable 4xx (400/404) deliberately stay terminal; the live
401/403 config cases retry and self-heal once the key/access is fixed.
79/79 across both suites.

* test(autofix): cover the timeout guard and the OAuth-quota fallback (review)

Two coverage gaps from the ci-bot review on #7247:
- The !result.timedOut guard was only asserted indirectly — no test
  emitted an [API Error] AND timed out. Added a case (spawnSync +
  QWEN_TIMEOUT_MS=100): qwen streams [API Error: 503] then hangs past
  the budget → killed → no marker. A refactor to !loopDetected now
  fails here.
- The standalone Qwen-OAuth-quota fallback (unwrapped, no [API Error:])
  had no test. Added a case emitting bare 'Qwen OAuth quota exceeded
  (limit: 100/min)' → marker written, wrapped as
  '[API Error: Qwen OAuth quota exceeded …]'.

* fix(autofix): anchor the API-error code, split retry budget by cause, keep the headline UTF-8

Addresses the review on #7247.

Classifier (points 2 and 4): the status code is now read from its POSITION in
the render (`[API Error: <code>`) instead of matched anywhere in the message.
Matching anywhere retried permanent failures forever — `400 Invalid value for
max_tokens: must be <= 512` matched a bare \b5\d\d\b and `400 context length
exceeded` matched a bare `exceeded`. `exceeded` now only counts as part of
`quota`. A 404 whose message says the model "does not exist or you do not have
access to it" — the OpenAI-compatible render of what a 403 reports — is no
longer terminal.

Retry budget (point 3): the marker now carries the cause class. A transient
429/5xx self-heals and keeps the full round budget; an auth/access error that
only a maintainer can fix is capped at API_AUTH_MAX_ROUNDS (3) and then goes
terminal with the "check the autofix model key/access, then re-arm" headline —
instead of ~100 agent runs and ~100 PR comments over ~17h on a takeover PR.
The terminal round is stamped so the scan's round gate skips the PR while the
sentinel ts keeps the feedback live for a re-arm.

Headline (point 1): `cut -c` counts bytes under GNU coreutils and the
classifier deliberately matches CJK renders, so the 200-byte cap could split a
multi-byte character and emit invalid UTF-8. Guarded with
`iconv -f utf-8 -t utf-8 -c || true`, matching the sibling publish site (the
`|| true` is required — iconv -c exits 1 when it discards).

Minor (point 5): documented that detection is best-effort because apiError is
derived from the last 20 KB of output; `head -1` -> `head -n 1`; tests added
for a permanent 400 carrying a 3-digit number >= 500 and for a >200-byte CJK
render staying valid UTF-8.

* test(autofix): cover the auth-capped retry budget and Chinese API-error patterns (#7247)

* fix(autofix): short-circuit 400 as terminal and classify only the last API error (#7247)

* fix(autofix): treat transport-level API failures as retryable

#7365 stranded at round 2/100 on this render:

    [API Error: terminated (cause: read ECONNRESET)]

The connection to the model dropped mid-run. That is as transient as a 429, but
the classifier never saw it that way: a transport failure never got far enough
to have an HTTP status, so it fell through to the keyword arm, and the keyword
arm only knew about rate limits and quotas. It was classified terminal, the
watermark advanced, and a PR that needed nothing but a re-run was handed to a
human.

Verified against the shipped classifier before the fix — every transport render
came back terminal:

    terminated (cause: read ECONNRESET)   -> terminal
    fetch failed                          -> terminal
    socket hang up                        -> terminal
    connect ETIMEDOUT                     -> terminal

Adds a transport arm to the code-less branch: ECONNRESET, ECONNREFUSED,
ETIMEDOUT, EPIPE, EAI_AGAIN, socket hang up, fetch failed, terminated.

ENOTFOUND is deliberately excluded. A hostname that does not resolve is a
misconfigured endpoint, which repeats forever — the same reasoning that keeps a
bad model name terminal.

Coded errors are unaffected: the arm sits after the status-code branch, so the
400 short-circuit added in 719991a3b still runs first.

* fix(autofix): address review — OAuth fallback override, comment accuracy, display clamp (#7247)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-21 07:01:56 +00:00