mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-09-11 19:46:21 +00:00
795 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4ec9f63d0a
|
feat(core): add CodeModeOnly tool execution (#10607)
* feat(core): add CodeModeOnly tool execution * fix(core): preserve CodeModeOnly execution semantics * fix(core): drop the tool_search reminder in CodeModeOnly CodeModeOnly hides `tool_search` and binds every deferred tool — schema included — into the `exec` description, so progressive discovery has nothing left to do. The session prelude still announced those tools as "reachable via `tool_search`", pointing the model at a tool that is never declared and re-billing the text on every cached prefix. Worse, tools already callable as `tools.web_fetch(...)` looked gated behind a lookup step that does not exist. The subagent and fork-resume callers already opted out, but the main-session prelude sites did not, so the gate now lives where the prelude is built rather than in each caller. * chore: refresh settings schema * chore: refresh companion notices * fix: register exec tool metadata * fix(cli): localize exec tool display name * fix(core): stop offering ToolSearch lookups in CodeModeOnly CodeModeOnly hides tool_search and binds every deferred schema into the exec description, so text that tells the model to look a tool up describes a surface it cannot reach. Return an empty deferred summary from the registry itself - this also covers the fork-resume reminder that bypassed the earlier call-site gate - and point the image zoom hint at tools.zoom_image. * feat(core): teach the system prompt CodeModeOnly's tool surface In CodeModeOnly the model can only call exec, so the tool guidance and the worked examples described a surface it does not have: bare tool names it cannot call, an instruction to issue several tool calls in one response, and examples in a direct tool-call syntax that does not exist there. Give "Using Your Tools" a code-mode branch that routes every tool through tools.<name>, states which tools are direct controls instead, and replaces multi-call parallelism with batching inside one exec program. Swap the four model-family example sets for one shared exec set, since the syntax differences they exist for do not apply. The mechanics of exec itself stay in its tool description; the prompt carries only policy. * test: include code mode host in standalone fixture * fix(core): return code mode media as multimodal output * feat(core): support image tool results in code mode * fix(core): carry deferred tool schemas in the exec description CodeModeOnly hides tool_search and never surfaces a nested call as a history functionCall, so a signature collapsed to Record<string, unknown> could never be filled in later: parameter names were reachable only by guessing, or by reading them off validation errors one at a time. Deferred tools keep their registry state; only the generated signature gains the schema. * fix(node-repl): restore the bin exec bit after rebuild npm chmods dist/index.js when it links the bin at install time, but the build deletes that exact file and tsc re-emits it as 0644. After any rebuild node_modules/.bin/node-repl-mcp therefore points at a non-executable target and spawning it fails with EACCES (exit 126) before the shebang is ever read. OR the exec bits into the emitted mode instead of setting 0o755, so a restrictive umask is preserved rather than widened. * fix(cli): pin patched Ink version * feat(cli): add collapsible tool call details * fix(dev): use CLI tsconfig outside repository * fix(cli): preserve expanded tool details after completion * fix(cli): fail headless runs on model API errors * fix(core): expose shell timeout bounds in code mode * feat(core): add code mode timeout helpers * feat(cli): expose code mode in settings * fix(mcp): preserve structured results for model and exec consumers * fix(core): preserve code mode output and truncate inline * feat(core): port scoped context tools to code mode * fix(build): synchronize pnpm lockfile and capability docs * fix(core): recover from rejected Responses encrypted reasoning |
||
|
|
d66ad3e3a2
|
feat(review): focus automatic static navigation reviews (#11456)
* feat(review): focus automatic static navigation reviews * fix(review): address round-1 review of the docs-nav profile (#11456) - QWEN_REVIEW_AUTOMATIC is operator-only: added to PROJECT_ENV_HARDCODED_EXCLUSIONS and read through automaticReviewRequested(), which refuses file-sourced values - SKILL.md: the docs-nav profile passes the cumulative findings file to compose-review as findingsPath, so an unruled candidate caps at Comment instead of posting as a verified blocker - the classifier treats top-level key order as structure (a reorder keeps the full review); the design doc records the decision - assessResume reports a moved head as head-moved even on a profiled report; both profile guards read the shared DOCS_NAV_PROFILE constant - the mechanism-health note no longer reads the profile's by-design anchor withhold as a stopped chain - the coverage gap subject names the omitted full review and reverse audit instead of the review that ran - buildRoleBrief welds no context pointer for a malformed plan prNumber * fix(review): address round-2 review of the docs-nav profile (#11456) * fix(review): address round-3 review of the docs-nav profile (#11456) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(review): close focused navigation review gaps * fix(review): strengthen focused navigation contracts --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
7556821e65
|
fix(ci): retry transient E2E build artifact upload failures once (#11364) (#11375)
* fix(ci): retry transient E2E build artifact upload failures once (#11364) * test(ci): harden the e2e-build upload retry contract pins (#11364) Address review findings on the retry helper suite: scope the two-attempt assertions to the archive instead of every upload-artifact step and bind the attempts by step name, pin the pack-to-upload handoff (archive name and step order), pin the build job's absent job-level continue-on-error, pin the retry gate by whole expression so a failure() conjunct cannot silently disable it, compare the two with: blocks wholesale (minus the intentional overwrite asymmetry) plus the action pin, and compare downloaded names against every workflow upload instead of restating the four known legs. Also add the suite to the yamlSuites roster in ci-platform-lanes.test.js so it can never join the dependency-free fast lane undetected. * fix(ci): correct the upload-retry rationale and name-key its contract (#11364) Real-runner probes on the PR showed the comment's two reasons for the retry's overwrite do not reproduce: a stalled upload aborts before finalize and reserves nothing, and name uniqueness is scoped per run attempt so a manual re-run does not 409 either. Reword both comments to the window overwrite actually covers — a first attempt that finalized e2e-build and only then reported failure. The contract test picked "the archive" by position (uploads[0]), contradicting its own comment: an unrelated upload step placed before the archive upload reddened the suite with misleading messages. Key on the literal e2e-build name instead — already the pinned contract in scripts/tests/e2e-workflow.test.js — in both the filter and the download-leg assertion. Mutation probes: a decoy upload step before the archive now stays green, while decoy+retry-deleted, retry-deleted, retry-renamed, and overwrite-dropped all still fail. * test(ci): pin the pack-to-upload directory in the e2e retry contract (#11364) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): pin the exact upload path in the e2e retry contract (#11364) Round 3's handoff pin closed the pack side but left the upload side pinned to a directory prefix, so moving only the two upload with.path values into a subdirectory of runner.temp kept the suite green while the runtime upload found no file and redded the build job. Compare first.with.path to the exact '${{ runner.temp }}/' + archive literal, reusing the derived archive binding rather than restating the name. Mutation probes: the upload-side drift now fails on the new assertion (16/1, expected-vs-actual), the workspace-escape and pack-side drift mutations stay red on their existing assertions, and the same upload-side mutation under the old prefix assertion stays green — the exact pin is the load-bearing one. Also reword the overwrite rationale in both the workflow comment and the test comment to the form the shipped action source supports: the record is created before any bytes move, so a failed first attempt can leave the e2e-build name taken, and overwrite is delete-then-upload and a no-op when nothing exists. The previous wording asserted a stall reserves nothing, which the create-then-upload lifecycle contradicts. And name the retry contract's actual ungated siblings (run-e2e-tests.sh's sandbox-image build retry, build-and-publish-image.yml's image build retry) instead of the budget-gated sandbox:none shard retry the comment over-claimed parity with. * fix(ci): pin the unpack archive name and restore the overwrite rationale (#11364) Round 4's handoff pin derives the archive name from the upload with.path, so renaming the tarball at the pack step and both upload paths while the four legs' unpack arguments stay put kept the suite green while every leg would fail closed on main (review R1-3). Pin each 'Unpack build artifact' run's trailing argument against the derived name; the legs download into runner.temp/e2e-build/, so the comparison is a suffix match, not the upload's full path. Mutation probe: a producer-only rename now fails the contract with 'a leg unpacks a different archive than the build job uploads' (16 pass / 1 fail), the unmutated workflow stays green (17/17), and the same mutation was green before this pin (17/17). Also restore the overwrite rationale that |
||
|
|
5a9c9b3406
|
feat(goal): stop a Goal at a turn or an active-time budget (#11457)
* feat(goal): stop a Goal at a turn or an active-time budget The token budget is a runaway-spend guard, sized for that job: 30,000,000 tokens by default, which a healthy long run reaches late. It is not what a user reaches for to keep a Goal short -- people say "at most twenty turns" or "at most half an hour". Neither had any effect, and the objective template invites the first phrasing while the user doc has to explain that writing it configures nothing. Add two ceilings beside the token one, both off by default: `model.goalMaxTurns` counts finished Goal turns and `model.goalMaxActiveMinutes` counts the wall time the Goal spends active. The meters were already on the record; only the ceilings and the stop were missing. A Goal that reaches either takes the path a spent token budget already takes: one wind-down turn to hand off, then `usage_limited` with a new `limitKind` (`turn_budget` or `time_budget`) and a reason naming the budget. A resume authorizes another window on top of what was used and moves only the ceiling that ran out. Ceilings are read at the continuation boundary, so the turn that crosses one still finishes; a turn crossing several reports one reason, token first. - `spentBudget` replaces the runtime's direct token check, so the continuation gate, the settle, and the no-progress bound cannot disagree about whether a Goal is out of allowance. The no-progress pause yields to every spent budget, not just the token one, so a cadence stop still gets its hand-off. - The reducer's budget-resume branch keyed off the literal `token_budget`; it now keys off `isGoalBudgetLimitKind`, or a turn-stopped Goal would resume still rendering "ran its turn budget" as the reason it is active. - The wind-down line stops naming the token budget, since the hosts carry a plain `windDown` flag and cannot say which ceiling was reached. It points at the budget line above it, which grew to report turns against their ceiling and, when one is armed, active minutes against theirs. - Both new `limitKind` values reach the two whitelists outside core: the SDK's hand-copied union and the Web Shell's `getGoalState`, which rebuilds the record field by field and would otherwise drop them on the live path. Design: docs/design/2026-09-09-goal-turn-and-time-budget.md and its .zh-CN.md counterpart. * fix(goal): repair the assertions and mocks the budget change broke Six CLI assertions pinned the continuation prompt verbatim, and renaming the budget line's prefix and rewording the wind-down line moved that text out from under them. Two more CLI suites could not collect at all: they build their `@qwen-code/qwen-code-core` mock by enumerating exports, and `settingsSchema.ts` now reads two new caps at module scope. Neither was reachable from the verification steps this change shipped with, which ran only `runBudget.test.ts` and `config.test.ts` in the CLI package. `cd packages/cli && npx vitest run` reaches all eight. Also, while in these files: - `get_goal`'s `lastGoal` summary already reported `tokenBudget`, so a Goal stopped by a cadence ceiling could be inspected and show only the allowance that did not run out. It now reports all three, and the tool description says so. - Two validator doc comments said "-1 for no ceiling, or a positive integer"; the predicates accept `0` as the same opt-out, the way the token budget's sibling already documents it. - `settingsSchema.test.ts` now pins both new entries: integer, no default, bounded by their caps, no restart. * fix(config): reject signed zero for the Goal cadence settings, and say what the ceilings actually do The `excludedValues` guard added last round compared with `Object.is`, which tells `+0` from `-0`. `Number()` maps `-0`, `-0.0`, `-.0` and `-0e0` to `-0`, `Number.isInteger(-0)` is true and `-0 < -1` is false, so `/config model.goalMaxTurns=-0` walked past the guard, reported `Set ... = 0` because `String(-0)` is `"0"`, and `JSON.stringify` persisted the excluded value. Every later start in that scope then aborted on `must be > 0`, with `/config` unreachable to repair it -- the same lockout the guard was added to prevent. Compare numbers with `===` so signed zero matches, keeping identity for the string half of the field's type. The documentation of both ceilings overclaimed in three ways, each fixed wherever the claim is published -- the two schema descriptions and their generated mirror, the settings table, the user guide, both locales of the Web Shell placeholder, three places in the goal-draft skill, and the `propose_goal` objective example: - `-1` was described as running Goals with no ceiling. It only removes a ceiling a Goal has already spent, on the resume or edit that follows; a Goal still under its ceiling keeps it. - Neither ceiling is retrofitted onto a Goal already on the record, so the only way to bound one is to replace it. Nothing said so. - The minutes row read as a wall-clock cap. There is no timer: the ceiling is read between turns, so a turn already running is never interrupted and the window can be exceeded by that turn plus the hand-off. The design doc's "no behavioural change when off" bullet and its first acceptance criterion were also too strong, in both languages. Restoring an `active` Goal rebases `updatedAt` unconditionally, so the elapsed figure a restored Goal reports drops offline time for everyone, settings or not -- and drops with it any active time in an interrupted turn that no journal write had committed. Both are now stated rather than implied, with the reason the record cannot separate the two. Two promises the tests did not hold: - Three surfaces say user turns are still admitted at the ceiling, and the only case covering it admitted the turn one step below the limit. A case now reserves a user turn with the ceiling already spent and the hand-off in flight; adding a `spentBudget` gate to `beginTurn` reds it. - The active-time ceiling is the only one whose spent state can change between queueing a continuation and delivering it. A case now queues under the ceiling with no host and binds past it, asserting the hand-off; making `bindHost` flush directly reds it. * fix(goal): name what replacing a Goal costs, keep ceiling advice out of objective text Round-three review, four findings. Replacing a Goal is the only way to bound one already on the record, and the docs offered it without the cost. `/goal set` goes through `createGoal`: a new Goal at revision 1, turn, token and active-time meters at zero, and an evidence cursor at the replacement, so everything the old Goal recorded drops out of the citable catalog. Said so on every surface that offers the remedy -- the user guide, both settings rows and both schema descriptions, regenerated. The ceiling settings were pointed at from two opposite places: the goal-draft template slot said "outside the objective", while its exemplar, the user guide's Budget row and strong example, the `propose_goal` objective example and the Web Shell placeholder all wrote the pointer into objective text -- text re-sent every turn to a model that cannot act on it, advising a setting that by its own wording never applies to the Goal carrying it. One placement now: never in objective text. The rules-of-thumb bullet names the settings, the user guide's Budget row says to set them in settings, and the Web Shell pointer moves from the placeholder into the hint rendered under the same field. `SKILL.test.ts` pins both halves -- the settings are named in the skill body and absent from the template and the exemplar -- so they cannot drift apart. The strong example also gains the `On block:` clause the template asks for. The signed-zero guard is one `includes` call: SameValueZero already matches `-0` against a `[0]` exclusion, so the typeof-split ternary carried an arm no setting can reach. The `-0` assertion from last round still pins it. The settings table rows are trimmed to fit the widest existing row, so the table is not re-padded, and one shortening that would have misdescribed which values are rejected is reverted to the accurate list. * fix(goal): stop the ceiling docs promising more than the runtime does Round-four review, five findings, all documentation precision. Each one is fixed by narrowing a claim rather than qualifying it further, so the surface the next round can find fault with gets smaller. - Bounding a Goal already on the record: "replacing it with /goal set or clearing it, which starts a new Goal" said clearing starts a Goal. Clearing leaves none. Only the replacement starts a new Goal at revision 1; clearing is followed by starting again. Both settings rows and both schema descriptions now say so, matching the user guide. - "Time while the process is not running does not count" overclaimed. The only exclusion is the rebase a restart performs; a suspended process, such as a sleeping laptop, is charged. The claim now says exactly that. - The quantified overshoot, "that turn plus the hand-off turn", holds only when a turn is running as the window runs out. A window that runs out while no turn runs is not noticed until the next turn ends. The bound is dropped in favour of saying the Goal can run well past the window. - The Web Shell hint sent readers to settings for two keys that client's settings surface cannot reach. It now names `/config` in the chat, which runs in daemon sessions. - The second weak-to-strong exemplar still wrote a bare turn count into its Budget. It carries the advisory marker like the first, and the skill test now checks every exemplar row instead of one. --------- Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
67fe20a838
|
fix(autofix): charge regressions to the brake and gate test weakening (#10188)
* fix(autofix): charge regressions to the brake and gate test weakening
Two holes let an autofix round ship a new problem for free.
A round that pushed a fix and turned CI red counted as a SUCCESS: the
consecutive-failure brake counts "rounds that pushed nothing", so the red
it created reset the counter, came back as the next round's input, and was
paid for out of the round budget. A PR could alternate regress/repair
indefinitely while every brake read it as converging.
Every acted round now stamps what it pushed and whether the head it pushed
onto was fully green; the next round charges a regression only when the
live head is exactly that sha, the marker says green, the window key
matches, and the head is red now. The observing round records it in
whichever report it posts, and the brake stops resetting on a regressing
round. Attribution is conservative on every axis: a moved head, a re-arm,
a pending check at push time, a cancelled run, or the loop's own lanes all
drop the charge.
The second hole: relaxing an existing assertion was invisible to the whole
gate. Build/typecheck/lint never read assertions, the package tests run the
weakened file, and the bite check reads only the tests a round ADDS. The
SKILL already required content evidence for deleting or weakening a test;
nothing enforced it. The gate now measures deleted test files, net
assertion removal, and added skip/todo markers, and rejects the round
unless each file is recorded in test-weakening.json with a reason. It
judges that the claim exists, not that it is right — the reasons ride into
the round report for a maintainer to read against the diff.
Deletions get their own merge-freight discriminator: content equality reads
"absent on both sides" as identical, so a round deleting a test the PR
itself added looked like freight and escaped. The merge base separates the
two cases.
* fix(autofix): attribute test weakening to round commits, close marker escapes
* fix(autofix): close weakening-gate merge/typechange escapes, own-lane charge leaks (#10188)
* fix(autofix): close weakening-gate comment, attribute, and name-framing escapes (#10188)
* fix(autofix): close weakening-gate masking escapes and delete/restore false charge (#10188)
* fix(autofix): close weakening-gate lexer, marker-shape, and verdict escapes (#10188)
- WEAKEN_ASSERT_RE: left-boundary class (console.assert( never fails a
test) and a matcher requirement on added expect( lines (matcher-less
expect passes for anything); the del side stays bare so a
multi-line-formatted assertion removal still charges
- skip markers: concurrent/sequential/shuffle chains, tagged-template
each tails, computed accessors, and newline-split chains measured on
a joined view; sd counts comment-stripped del lines
- weaken_strip_comments gains string/template state (an in-string /*
can no longer poison the strip); weaken_strip_code gains
regex-literal and template-nesting state
- modify/delete merge resolutions that KEEP the file apply no freight
- the net-range deletion arm enumerates the explicit pre-round->tip
pair and is judged even when per-commit measurement is UNAVAILABLE
* fix(autofix): close weakening-gate member-assertion, marker-spelling, and lexer escapes (#10188)
- WEAKEN_ASSERT_RE/ADD_RE: the member arm requires the CALL (an
exported assert alias executes no assertion), and supertest's
throwing member form .expect( counts on every arm
- WEAKEN_SKIP_RE: backtick-quoted computed accessors, the ?.
optional-chaining spellings (it?.skip( and it.skip?.(), and the
reversed chain with an execution modifier behind the marker
- weaken_strip_code: regex-vs-division is keyed on the last
significant TOKEN (an operator including /, a regex-head keyword
preserved across whitespace, or a control-head close paren tracked
on a paren-kind stack), not the last output character
- weaken_strip_code/weaken_strip_comments: a ${} hole closes on the
brace matching its own ${ (per-hole brace depth), not the first }
inside the hole (an object literal or arrow body)
- the skip-marker net gains the whole-blob backstop twin of the
assertion arm, so a genuine addition cannot net against an
in-comment marker deletion or hide behind a block-comment span
opened above the hunk
- twelve witnessed fixtures (each guard flips its own test red under
mutation); fixtureWrite emits single-quoted shell words so
backtick/${ fixture content survives
* fix(autofix): close weakening-gate merge coverage, freight phantom, and lexer escapes (#10188)
* fix(autofix): close weakening-gate cross-commit, merge-discard, and skip-surface escapes (#10188)
* fix(autofix): close weakening-gate matcher-credit, each-skip, and merge-recount escapes (#10188)
* fix(autofix): close weakening-gate tail-drop, skip-netting, fast-forward, and JSX escapes (#10188)
* fix(autofix): stop the weakening gate charging honest test edits (#10188)
Three of the four measured defects were false rejects the gate published
as machine-measured fact, and the fourth was test surface it never saw.
The matcher-tail census counted every member call in a stripped blob, so
deleting a spy and its restore read as three removed assertion lines;
scope it to continuation-shaped lines, the only shape that arm exists to
see. A fast-forwarded main recorded a recount base only for files it
added, so main's own delta was charged to a round that synced and then
renamed a symbol; record bases for modified files too and read the ledger
instead of set membership. The body-call skip arm charged vitest's
condition-valued dynamic skip, which is the runtime spelling of the
.skipIf environment guard the doctrine already exempts. And `*_tests.rs`
matched no pathspec arm, leaving two tracked Rust test files invisible.
Each fix carries a witness that a mutation probe confirms goes red
without it; the pinned guards for all four arms stay green.
* fix(autofix): measure test weakening with the parser, not a text census (#10188)
Replace the awk lexer, the four extended regexes, the matcher-tail census,
the per-commit accumulators, the verdict-time recount and the netting skip
with one instrument: count-test-surface.mjs parses the whole file with the
TypeScript compiler and reports its declared test surface (statement-level
assertion chains, test/describe registrations with their enabled state,
bare early returns ahead of assertions). Each file's round delta is
tip - pre-round - main's contribution, where main's contribution across a
merge is measured on git's own auto-merge of main's side (git merge-file
--ours) and across a fast-forwarded main commit on the commit itself, so
the same weakening measures the same before, during or after a merge, and
main's delta neither charges nor shields.
Narrow the pathspec to test-NAMED files (a README under integration-tests/
is not a test deletion), read the enumeration producers' status instead of
swallowing it, re-apply the reason bar when rendering, and state the
declared non-goals in the gate header: reachability, condition-valued
guards and by-reference options are runtime facts the runner-backed
instruments judge.
Bind the af-155 regression classifier to the commit its rollup describes
(headRefOid read with the rollup; a moved head classifies none), exclude
the loop's own dispatch-pending StatusContext by its exact context value,
classify verdict-less checks (EXPECTED, STALE, STARTUP_FAILURE) as pending,
stamp pre=none after a salvage-merged push, drop the unread CHECK_STATE
from the failure report's env, and render the regression disclosure on
every report shape.
Drop the hand-rolled CommonMark fence model from workflow-size.test.js: the
existing anchor/pointer/contents pins already prove navigation, and a text
model of a renderer is the class of instrument this change removes.
* chore: record the merged qwen-autofix.yml size
* fix(autofix): stage the surface counter with the pre-merge absence guard (#10188)
The stage step copies count-test-surface.mjs from the trusted base, but
the script is new in this PR: pre-merge the base lacks it, and a bare cp
under the step's -e kills every PR-event round whose workflow resolves
from the PR's own ref — the deferral the reviewer kept re-reporting.
Give the cp the heartbeat guard: rm -rf first so a planted leftover
cannot survive a failed copy and be executed as trusted (af-149), then
|| true so absence skips; the gate already fails open on a missing
counter (WEAKEN_MEASURED=false). Pin the pair and their order beside
the heartbeat pins.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(autofix): close the weakening gate's fail-open paths and make green an allowlist
Answers rounds 21-26 by retiring mechanisms rather than adding entrances.
Head-state classifier (af-155): GREEN is now an allowlist, never the
fall-through. A charge-enabling verdict requires every counted check to
hold a state the classifier recognises as passing, so CANCELLED, NEUTRAL,
SKIPPED and any state GitHub adds later read as pending instead of minting
the premise. (R22-3)
Test-surface counter: a run that exits 0 without printing a readable
verdict is a measurement FAILURE, not a verdict. weaken_measure validates
the shape before returning it, so the file takes the existing fail-closed
arm ("test surface could not be measured") instead of being silently
uncharged while the round still reports itself measured. (R26-2)
Merge attribution: git merge-file's failure test is its exit status, not
its output size. --ours resolves conflicts and exits 0, so a legitimately
empty result (main emptying a test file) was never conflated with failure
-- the line is replaced anyway so the intent no longer has to be
re-derived from a size test, and pinned by a fixture that makes main's
side zero bytes across a real merge commit. (R21-4)
Report-post fallback: gated on the af-155 state the round AUTHORED, not on
its outcome. A no-op round's observation about the PRIOR round was
unrecoverable once its report failed to post, because the marker it read
is superseded by the next push and the workflow's handoff comment excludes
exactly the fixed/noop outcomes. (R22-1, R26-3)
Instrument staging keeps the pre-merge absence guard it was given here and
moves to its own step after npm ci, so the parser is a .cjs copy of the
typescript the TRUSTED lockfile installed; both digests travel in
expression context and are verified before the counter executes. A
verbatim absence probe runs the step against a script-less tree with
leftovers planted in both shapes.
Boundaries stated where they can be read: --ours is the attribution model
and its error deliberately lands on the charging side; the base axis is a
documented residual with the same bound as the flake carve-out; the SKILL
no longer promises assertion counting for the Python and Rust shapes,
which are watched for deletion alone.
508/508 in scripts/tests; five mutants (green default, verdict validation,
emptiness-as-failure, outcome-gated fallback, no-op event overwriting the
baseline) all killed.
* test(autofix): execute the paths the weakening gate's own guards protect
A reverse audit of the previous commit found two guards that no test
reached, both load-bearing in the fail-open direction, plus one hazard in
the counter's CLI dispatch.
git merge-file refuses a file carrying a NUL byte (exit 255, no output),
and the branch's own side has to stand in for the auto-merge then. Nothing
executed that arm: making the substitution unreachable left the whole suite
green. Without it the refusal's empty output reads as main having emptied
the file, which credits the round with the entire baseline surface and
masks a removal of up to that size. New fixture `binary-merge` -- main
appends a comment, the round drops an assertion, and the NUL makes every
side of the merge binary -- charges `net 1 assertion(s) removed` and fails
when the substitution is removed. The round rewrites the file rather than
filtering it, because a line filter reads a NUL-bearing file as binary and
would empty it instead.
The staging step's present-counter branch -- the one that runs in
production once this lands -- was pinned only by text. Swapping `mkdir -p`
and `cp` inside it left the suite green while killing every round, because
the step is unconditional and runs under `-e`. The probe now runs the step
body the way GitHub runs a `run:` block (`bash --noprofile --norc -e -o
pipefail`) in both shapes: the pre-merge tree that lacks the script, and a
tree that has it with a parser planted where npm ci puts one, asserting the
staged bytes and both digests.
The counter's CLI dispatch ran at module scope, so importing it executed it
against the importer's argv -- a positional the importing program owns
lands in the unknown-mode arm and takes that process down with exit 2. It
now runs only when the file is the program, compared as REAL paths: the
gate runs it from RUNNER_TEMP, which on macOS is reached through
/var -> /private/var, and a literal comparison silently disables the CLI
and reports every file unmeasurable. Both halves are pinned, including a
run through a symlink.
Also: the head-state classifier keeps the status axis it had, so a check
still in flight is pending even when it carries a conclusion from an
earlier attempt; and the dialect table is exercised for every extension the
pathspec selects, not only `.ts` -- deleting the JS entries was invisible
to 52 tests while every tracked `*.test.js` file is measured by the gate.
509/509 in scripts/tests. Mutants killed: the merge-file substitution made
unreachable, the staging order swapped, the main-module guard compared
literally, the JS dialects deleted, plus the five from the previous commit.
* fix(autofix): follow git's trivial-merge rule when merge-file refuses
A reverse audit of the previous commit found that its new binary-merge
fixture pinned THAT a substitution happens, not WHICH side it takes -- and
the side it took was wrong half the time.
When `git merge-file` refuses (a NUL byte makes the file binary, exit 255,
no output), the previous commit substituted the branch's side
unconditionally. That is right only when main's side is unchanged from the
base. When MAIN is the side that moved, it reads main's own weakening as
the round's and rejects a round whose only act was `git merge origin/main`
-- the exact property the neighbouring main-empties fixture already guards,
re-broken by a second trigger into the same substitution.
The fallback now follows git's own trivial-merge rule: a side that did not
move from the base contributes nothing, so the other side IS the result,
and only a genuine two-sided change resolves for the branch the way
`--ours` would have. Three fixtures pin the three shapes -- the round
weakens (charged), main weakens (accepted), both move (charged for the
round's own removal only) -- and both fixed-side choices now fail.
The head-state classifier's status axis becomes an allowlist too. The
denylist it had enumerated four in-flight states and missed REQUESTED, a
real CheckStatusState member that four other sites in this same workflow
already list; the axis is now `status == "COMPLETED"`, with an absent field
reading as finished because a commit status carries no status field at all.
Both axes are allowlists for the same reason a denylist forgets the member
it was not told about -- which is how CANCELLED entered the green arm in
the first place.
The classifier's rationale moves to qwen-autofix.md#af-155, leaving the
workflow 1024 bytes below the hard gate.
509/509 in scripts/tests. Mutants killed: always take the branch's side,
always take main's side, the four-member status denylist restored, the
status axis dropped entirely.
* fix(autofix): bound main's contribution by what the merge actually took
A third reverse audit found a false ACCEPT in the attribution model, in
both lanes: the auto-merge is computed from the two sides and never
consults the merge commit's own tree, so a merge that DISCARDED main's side
still books main's delta as a contribution.
A round can therefore merge main with `-s ours` and delete an assertion in
the same breath: the credit for main's discarded weakening absorbs the
round's own removal exactly, and the round ships uncharged. The previous
commit widened the reach of this by extending the trivial-merge rule to the
binary lane, where the same shape had been rejected before.
Main's contribution is now bounded by what the tip took. When the merge
result is the branch's side byte for byte, main contributed nothing to that
file, whatever an auto-merge would have produced, and nothing is
subtracted. A resolution that took main's version, or produced something
new, still goes through the model unchanged -- including `merge-conflict-
drop`, whose resolution commit differs from both sides.
Two fixtures pin the shape in both lanes: `strategy-ours-hole` and
`strategy-ours-hole-binary`, each a round that merges `-s ours` and removes
an assertion. Deleting the bound leaves both accepted.
509/509 in scripts/tests.
* fix(autofix): clamp main's credit by what the merge actually landed
The previous commit's bound was byte-exact: it fired only when the merge
result equalled the branch's side to the byte. A fourth reverse audit
showed one added comment line inside the merge reinstates the whole hole,
and the blast radius is main's ENTIRE modelled delta -- a round can strip
every assertion from a pre-existing test file and ship it uncharged.
Main's contribution is now the part the MODEL and the LANDED blob agree
on, and only where they agree in sign. Equal, and the clamp is the
identity: the ordinary merge measures as it always did. They diverge
exactly when a resolution took neither side whole, and then the clamp cuts
both ways -- a resolution that discarded main's side credits nothing, and
a resolution that weakened the file itself is not main's contribution
either.
`weaken_auto_blob` emits three lines now, always: the modelled auto-merge,
an optional override for the side to compare against, and the blob the
merge commit actually holds. Presence is deliberately NOT clamped: the
baseline is what the round had available to weaken, and letting the merge
result define it would let a round discard a test main added and answer
for nothing -- the one thing the deletion signal exists to catch.
What the clamp cannot see is identity. Main removing one assertion while
the resolution puts it back and drops a different one nets to zero, the
same way an assertion moved within a file always has; that boundary is now
stated in the header beside the model.
Fixtures: `ours-hole-plus-comment` is the evasion the byte-exact bound
missed, and `binary-resolution-takes-main` pins the binary fallback's
branch-side arm -- which the clamp had quietly un-covered -- at its
declared over-charging direction.
509/509 in scripts/tests. Mutants killed: the clamp returning the model,
the clamp returning the landed delta, the landed blob never emitted, the
binary fallback's else arm taking main's side. One survivor recorded: that
arm taking the merge base, which the clamp makes equivalent in every shape
a fixture can reach.
* fix(autofix): clamp only main's removals, never its additions
The clamp the previous commit added was sign-symmetric, and its two signs
have opposite consequences. Capping a REMOVAL cuts the shield, which is
what it was written for. Capping an ADDITION cuts the charge -- so a round
could discard whatever main added to a test file mid-round and pay
nothing. A fifth reverse audit demonstrated it through the real gate in
three spellings, none of them needing a merge strategy flag: an ordinary
merge resolution that drops the assertion main just landed, a
delete/modify resolution that drops the test main appended, and the same
resolution keeping that test registered `.skip`.
The commit that added the clamp already left PRESENCE unclamped for
exactly this reason, and said so; the reasoning was never carried into the
deltas. It is now: main's additions raise the baseline whatever the merge
kept, and only its removals are credited as far as they landed. It also
contradicted a promise the gate header makes -- the same weakening
arriving by fast-forward was charged while the one arriving through a
merge was not.
The event skip goes back to testing the model alone. Its second half was
added to let a landed-only divergence through, but the clamp returns zero
for every such event, so the only thing processing it could still do was
latch `baselinePresent` off a side main never held: a file the round
authored itself, touched while resolving a merge and dropped afterwards,
was charged as a deleted pre-existing test.
Six fixtures, all through the real gate: the three discard spellings
(charged), the round's own file edited in a merge and dropped (accepted),
main removing two where the resolution keeps one and the round then
removes another (charged for its own), and main disabling a test the
resolution keeps enabled before the round disables it (charged).
509/509 in scripts/tests. The four mutants that survived the previous
commit are now killed: both clamp branches, and `clampBag` returning
either bag unclamped. Presence clamped by the landed blob, and the skip
processing every event, are killed too.
* fix(autofix): measure main's contribution where main made it
A sixth reverse audit found two more defects, and both trace to the same
mechanism: main's contribution was read off a three-way SPLICE of the two
sides (`git merge-file --ours`), which mixes the round's own edits into
what is supposed to be main's delta.
The splice can invent a surface neither side ever had. Witness: the round
un-skips a suite, main adds a test inside that suite, and the resolution
takes main's file verbatim -- so the tip is byte-identical to main's and
nothing was weakened. The splice combined the round's un-skip with main's
addition into an ENABLED registration that existed in no tree, and the
round was charged for disabling it.
The splice also decided PRESENCE, through an endpoint that is the branch's
side in two arms. A path both sides added during the round, resolved to
main's file and then deleted by the round, read as "the baseline never
held it" and shipped uncharged -- while the gate's own
`weaken_baseline_holds`, the definition its fail-closed arm uses, said the
opposite.
So the splice is gone. Main's contribution is main's OWN delta, measured
on main's own side against the merge base, and presence follows main's own
side directly. That makes the two arms one rule: a modify/delete resolved
for main was already measured from the merge base. `git merge-file` leaves
the gate entirely, and with it the binary-refusal fallback, its
trivial-merge rule and the over-charge they carried -- a resolution that
takes main's version now charges the round for nothing, where the model
used to bill it for main's removal too.
What the merge actually DID with main's contribution still clamps it, as
before: removals credited only as far as they landed, additions never
clamped.
509/509 in scripts/tests, with the gate section 60 lines shorter. Four new
fixtures: the splice artifact (accepted), the add/add path dropped after
the merge (charged), a resolution that removes more than main did (charged
for the difference), and one that moves opposite to main (charged for the
round's own removal only). Mutants killed: main's delta measured against
the branch side, presence forced either way, removals credited from the
landed blob, the landed guard dropped, additions clamped down.
* fix(autofix): decide the baseline by what main LANDED, not by what it holds
A seventh reverse audit found three defects in the new model. Two are
false rejections with high reachability, one is a false accept that a git
tie-break decides.
Presence flipped true whenever main held the file at a merge, even when
main's side was byte-identical to the merge base -- main landed nothing
there. So a test file an EARLIER round removed, or renamed, is charged
`test file deleted` again in every later round that merges main, until
main also loses the file. The gate's own header promises a rename costs
one ack entry naming the old path; it was costing one per round.
Presence now moves only when main's side and the merge base DISAGREE about
the file existing. Main adding it during the round puts it in the
baseline, and the round answers for dropping it afterwards. Main deleting
it takes it out only when the merge adopted that deletion. Main merely
still holding a file it has always held says nothing.
The "not an event at all" early return is gone with it. It dropped main's
deletion from the running total while the NEXT merge's base already
reflected that deletion, so main's re-add was credited from zero a second
time: add, delete-with-the-round-keeping-its-copy, re-add was charged
three assertions where one was removed. Every merge event is recorded now,
and the clamp handles the resolution that kept the round's copy -- its
landed delta is zero, so main's modelled deletion credits nothing.
A criss-cross history has more than one equally valid merge base and git
picks one without promising which. Since main's delta is measured against
that base, the pick decided the verdict -- and one of them credits main
with the ROUND's own removal, waiving a real weakening. The file is now
refused rather than measured: `test surface could not be measured`, which
one ack entry answers.
509/509 in scripts/tests. Three fixtures for the presence rule and the
event chain; both presence mutants (latching main's hold unconditionally,
and clearing on main's deletion whatever landed) are killed. The stale
auto-merge prose in the counter's header, its clamp comment, an orphaned
doc comment in the gate and two leftovers in the suite are corrected.
* fix(autofix): chain main's events, and let the merge decide what it adopted
An eighth reverse audit found that the previous commit's headline fix does
not work and its fixture is vacuous, plus two false accepts it introduced.
The removed early return was load-bearing. Its case -- main deleted the
file, the merge kept the round's own copy -- is now recorded as an event
whose `landed` blob IS the round's copy, so a copy weaker than the merge
base credits MAIN with the round's own removal. The round guts a test file,
main happens to delete it, the resolution keeps the gutted copy, and the
round ships uncharged. The early return is back.
What that removal was meant to fix is fixed properly: main's events CHAIN.
Each event now measures main's side against main's side at the PREVIOUS
event, not against a fresh merge base that already reflects it, so the
events telescope to main's own net for the round. Add, delete-with-the-
round-keeping-its-copy, re-add was charged three assertions where one was
removed -- and it still was after the previous commit, because recording
the deletion with a landed delta of zero is behaviourally identical to
dropping it. The fixture missed it by re-adding byte-identical content, so
the third merge was not an event at all; it re-adds a comment line now.
Presence follows what main CONTRIBUTED, not what it holds. `mainHolds`
alone exempted a whole path for the round whenever the pre-round ref
lacked it -- including when main GREW it and the round restored main's
file and gutted it. The rule is now: main holding the file and having
moved it puts it in the baseline, whatever the resolution then did; main
deleting it takes it out only when the merge adopted that deletion.
The criss-cross refusal was charging honest rounds. More than one merge
base only matters when the candidates DISAGREE about the file, which is
now the test; when they hold the same blob the pick cannot change the
verdict and the file is measured. Two fixtures pin both sides of that.
516/516 in scripts/tests. Six mutants killed, each the exact regression it
names: the early return removed, the chain removed, presence from
`mainHolds` alone, presence cleared without checking the base, the refusal
made unconditional, and the refusal dropped. The stale presence comment
the previous commit missed, and `weaken_baseline_holds`'s claim to be the
same definition the counter reports, are corrected.
* fix(autofix): drop the event whenever main holds no side and the file stays
A ninth reverse audit found that the reinstated early return was too
narrow, that presence read byte movement where it meant surface movement,
and that the criss-cross guard compared the wrong identity.
The early return required the merge base to hold the file. One merge
later it does not: after main deletes a file the round kept, the NEXT
merge sees no main side AND no base, the event is recorded, and the
round's own copy becomes the landed blob that sets the clamp -- crediting
main with exactly the round's removal, one merge after the guard that
exists to prevent it. The base is not part of the test any more: main
holding no side while the file stays in the round's hands is the same case
however the history reached it.
Presence asked whether main's side moved BYTES. Main appending a comment
to a file an earlier round deleted contributes no coverage, and reading it
as a contribution re-charged that deletion in every round main happened to
touch the file -- and made the verdict depend on how many times the round
merged main. It asks about the measured SURFACE now: the totals and both
registration multisets.
The criss-cross guard compared `git rev-parse <base>:<path>`, which
returns a TREE oid for a directory while the measurement treats a tree as
no blob at all. Two bases that agree for the measurement read as a
disagreement, and an honest round was charged unmeasurable on a tie-break
that could not have changed the verdict. It compares blob-or-absent
identity now, the same question `weaken_blob` asks.
One finding is answered with a stated precondition rather than code. The
model reads main's side as MAIN's, which holds while origin/main does not
contain the round's own commits -- true here, where pull requests land
squashed and main's first-parent history carries no merge of a PR branch.
A guard for it was written, found unreachable from every history these
fixtures can build, and removed in favour of saying so in the header.
516/516 in scripts/tests. Three fixtures, and with them the two mutants
the audit found surviving are killed: the caller's skip of a dropped event
(previously indistinguishable, because no fixture had a prior main event
for the file) and the early return's base condition. The clamp's
`landedHolds` condition is now redundant for manifests the gate produces,
and says so.
* fix(autofix): read movement from the bytes where the surface is unmeasurable
A tenth reverse audit found that the previous commit's surface test
disabled the deletion arm for every non-JavaScript test file -- 45 Rust
and 18 Python paths in this repository.
`count()` reads no surface from those shapes, so two present blobs always
compare equal and "main moved it" could never become true. Once the
pre-round ref lacked such a path, nothing main did during the round put it
back in the baseline, and the deletion arm -- the only arm those shapes
have, and one the gate header promises never fails open -- was waived.
Movement now falls back to the bytes when neither side is a dialect the
instrument reads, because there "the surface did not move" is unmeasurable
rather than false.
The surface test also compared main's DISABLED registrations, which reach
no signal: only the tip's do. Comparing them raised the baseline for a
change the gate can never charge -- main adding an `it.skip` re-charged a
deletion an earlier round had already answered for, which is the exact
failure the surface test was written to remove. It compares the chargeable
quantities now: the assertion totals and the enabled-registration
multiset, of which `enabled` is a derived count.
The criss-cross comparison goes through `weaken_blob`, the reader the
measurement itself uses, instead of a second identity built from
`rev-parse`. A tree, a gitlink and a missing path are "no blob" to all of
them alike, and a mode-only difference is the same blob, so the guard
refuses exactly the disagreements that could change a verdict.
Two fixtures for the deletion arm: a Python file main grows while the
round keeps its earlier deletion, and a file main adds whose measured
surface is indistinguishable from an absent one. Both are charged; both
fail if the fallback or the presence rule's existence test is removed.
The `main-absorbs-round-commit` fixture is deleted. The audit showed it
measured nothing -- the file is byte-identical on both sides of its merge,
so the event filter drops it before attribution runs -- and its comment
claimed a mechanism that was not the one making it pass. The precondition
it stood for is stated in the header instead, with its justification
corrected: a squash-merge of the round's own work into main mid-round is
the spelling that breaks the model, not a merge, which is self-correcting
because the merge base carries the same commit.
516/516 in scripts/tests.
* fix(autofix): decide the byte fallback by what was measured, not by the extension
An eleventh reverse audit found that the previous commit's fallback keyed
on the file's EXTENSION rather than on whether the instrument read
anything. A `.test.ts` that registers nothing measures exactly like a
`.py` -- and this repository has one, a type-level fixture under the CUA
driver -- so its deletion could still be waived after an earlier round
removed it. The test is the measured surface now: no registration of
either kind and no assertion, on both sides.
The audit also showed the previous commit's narrowing of the surface
comparison changed real verdicts with no coverage, in either direction.
Main moving only DISABLED registrations is no coverage and reaches no
signal, so it must not put a file an earlier round deleted back into the
baseline; re-adding that comparison now fails a fixture, as does dropping
any of the three fields that remain.
The criss-cross guard's presence arm was equally uncovered: both shipped
criss-cross fixtures disagree about the file's CONTENT, and the variable
that tells "no blob" from "the first candidate" was free to be removed. A
fixture whose candidate bases disagree about whether the file exists at
all pins it.
Three fixtures, three mutants killed: the fallback keyed on the extension,
the surface comparison widened back to disabled registrations, and the
criss-cross presence guard removed.
The header's three claims are corrected. It said movement falls back to
the bytes for "the Python and Rust shapes" (it is any file that measures
to nothing, in any dialect), that presence follows main having moved the
surface (main ADDING the path raises it with no movement at all), and that
comparing merge bases through `weaken_blob` buys agreement on trees,
gitlinks and mode-only changes (the identity it replaced already had all
three; what it buys is one reader instead of two that can drift apart).
The byte fallback's cost -- main touching such a file re-charges a
deletion an earlier round answered for -- is stated rather than left to be
discovered.
516/516 in scripts/tests.
* fix(autofix): let a skips-only file reach the bytes, and pin every field
A twelfth reverse audit proved the previous commit introduces no new
false accept -- structurally, and over forty thousand generated histories
-- and found one inconsistency and a coverage claim that was false.
`isEmptySurface` counted a disabled registration as content while
`sameSurface` cannot see one. A file that is NOTHING BUT disabled
registrations therefore took the comparison branch, where the comparator
is blind to the only thing in the file, so no rewrite of it could ever
read as main contributing. This repository has one such file. It reaches
the bytes now, like every other file the comparison cannot read, while
main moving disabled registrations on a file that has other content is
still not coverage arriving.
Four fixtures, four mutants killed, and each of the comparison's three
fields is load-bearing on its own for the first time: main planting a
guard (the live total falls, the declared total stands), main deleting an
assertion already behind one (the declared total falls, the live total
cannot), and main renaming a test (both totals stand, the multiset moves).
Those three survived every test in the file before.
The previous commit's message claimed dropping any of the three fields
fails a fixture. It did not; only the re-added disabled comparison was
pinned. The claim is not repeated here, and the comparison's own header
now says what it leaves out and where those files go instead.
The criss-cross presence assertion also gains the path check its sibling
already had: exiting 1 does not discriminate, because every guard mutant
still exits 1 -- charging main's own deletion to the round, which is the
hazard the guard exists for.
516/516 in scripts/tests.
* fix(autofix): close the escape hatches the review found in the weakening gate
- count-test-surface: see through type-only wrappers (`as`, `<T>x`,
`satisfies`, `!`) in constant folding, options objects and call
chains; fold comparison/equality/logical operators of two constants;
treat template-literal returns as non-thenable
- count-test-surface: an `if (true)` wrapper or a catch whose try holds
an assertion no longer shelters a body skip; a registration callback
handed by name resolves to its module-scope binding; a collector
factory binding (it.skipIf / it.each / test.extend) registers nothing
- af-155: charge a regression only when the run's window IS the live
re-arm key, and never stamp pre=green when the push carries a merge
past the head prepare classified
- tests: exercise all five own-lane names in the charge classifier, and
gate the verbatim staging witness on the host having sha256sum
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(autofix): arm-aware guard conditions, lane-list dedup, branch-side clamp (#10188)
Holes the round-27/28 review kept re-asserting on top of
|
||
|
|
dfafbf240e
|
fix(ci): widen the replayed supersede-cede timeline margin to deflake Scripts (#11581)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
ac1edef974
|
fix(review): keep host-trusted state out of the container's writable surface (#9983)
* fix(review): keep host-trusted state out of the container's writable surface
Two instances of one root cause, both reachable on `main` today and neither
opened by the sandbox — before it, the reviewed repository's commands ran as
the host user with no filesystem restriction at all, so containment narrowed
this reach rather than creating it. What it did not do is close it, and a
`required` that claims containment should not leave host-trusted state in the
directory it hands the container read-write.
**Leases.** `cleanupReviewWorktreeLeases` matches a lease by session ids alone
and then force-removes whatever worktree and deletes whatever branch it names.
Those files lived in the review temp dir — the bind mount. Reviewed code that
edits one makes another session's cleanup destroy the wrong tree; one it
plants with a foreign session id is swept by nobody and wedges that PR on that
machine until someone deletes the file by hand. They now live one directory
over, in `.qwen/review-leases`, which nothing mounts. A lease left behind by
an older build is deleted when the same target is leased again.
**Gitfiles.** These cannot move — git requires `<tree>/.git` — so the check
moves instead. The existing identity gates cannot see the attack and never
could: the writer chooses both the gitfile's target and that target's
backpointer, so the round-trip agrees; `--show-toplevel` still names the tree;
no symlink is involved; and asking git for the common dir resolves THROUGH the
rewritten gitfile, so that answer comes from the same hand. Location is the
question that has an honest answer — a real admin entry lives under
`<repo>/.git/worktrees/`, outside the mount, and a planted one has to be
inside it. `restoreProbeTreeTracked` now refuses an entry that resolves inside
the review temp dir.
Reproduced end to end before fixing, and the reproduction is the reason the
gate is where it is: a rewritten gitfile pointing at a planted admin entry
whose own `commondir` names a planted repository, both under the mount,
passes all three existing gates — and `git checkout --force` then runs that
repository's `filter.evil.smudge` ON THE HOST. With the new gate the same
shape is refused while the entry it replaced still passes.
Three mutants, three reds: a gate that always admits, one that admits when the
entry cannot be resolved, and one that refuses when there is no mount at all —
the last would refuse every ordinary repository.
* fix(review): close the shapes the first cut of this gate left open
Six blockers from round 1, all verified before fixing.
**The predicate failed open at both boundaries of its own question.** An entry
whose realpath IS the mount root produced an empty relative path and was read
as outside — while the root is exactly as writable as anything under it — and
a child legitimately named `..evil-git` produced a relative path starting with
the characters an escape would. Both are places a planted entry can sit. The
hand-rolled arithmetic is gone; `isSubpath` already answers this correctly and
is already tested.
**Every gate sat behind `.git` being a file.** Reviewed code that replaces the
gitfile with a `.git` DIRECTORY of its own — `rm .git && git init .` inside
the mount — skipped the lot, and the restore then ran against the repository
it had just made. That shape is now refused before the branch, and only under
a mount: outside one, a `.git` directory is what every ordinary checkout has.
**The gate was on the second host-side write, not the first.** The probe
tree's own creation — `git worktree add` — resolves the repository through the
REVIEW worktree's gitfile, a second rewritable pointer in the same mount,
written after the build phase has already run the PR's code in the container.
`worktree add` checks files out, so it executes whatever filter that pointer
leads to, before anything inside the restore could fire. Both call sites now
ask the same question through one helper rather than two inline copies.
**The legacy-lease removal was a wedge.** `force` only swallows ENOENT, so a
DIRECTORY at the old lease path threw EISDIR out of acquisition — and
`mkdir .qwen/tmp/qwen-review-lease-pr-42.json` is one command inside the
mount, after which every review of that PR on that machine failed, with
nothing able to remove it: the rollback rethrows, the sweep skips the lease
shape, and `rm -f` cannot remove a directory.
**The move stranded the only automated orphan sweeper.** The review job's
`if: always()` heal step swept the old path only, so a cancelled or timed-out
run left a dead lease at the new one, on a runner whose workspace persists.
It now sweeps both, recursively.
Tests follow the same rule they were missing: the gate's production call sites
are driven, not just its predicate. Every previous fixture put its tree under a
bare `tmpdir()`, where `mountRootFor` answers null and the gate short-circuits
before its logic runs — which is how deleting the wiring shipped green. The new
fixtures sit under `.qwen/tmp` and plant a COHERENT admin entry, because an
incoherent one fails earlier for a reason that has nothing to do with the gate.
Seven mutants, seven reds — including passing the probe tree where the review
worktree belongs.
* fix(review): gate the third write, and stop breaking the lane that pins the sweep
**The workflow edit broke its own pinning test, and this PR's required check
with it.** `scripts/tests/review-worktree-cleanup-workflow.test.js` asserts the
sweep line literally, derived from `paths.ts` so a rename there fails the build
rather than silently no-op-ing the sweep on the runners. Adding `-r` broke the
substring. The test now pins BOTH locations — the new one where this job's own
runs leave leases, the old one where a persisted workspace can still hold one —
which is what the test existed to enforce and what the move should have updated
in the same breath. It went unnoticed because the commit was verified with
vitest alone; `npm run test:scripts` is a separate lane of the same required
job.
**The revert phase was a third ungated write, reached exactly when the others
refuse.** `git checkout base -- …` runs against the same mounted probe tree,
and its only guard was a symlink-only lstat walk that a rewritten gitfile
passes untouched. Worse, a restore refusal becomes `inconclusive` without
throwing and the mutation phase's catch continues on purpose "so the revert
probe below still runs" — so guarding the first two writes and not this one
left the route open precisely where the other two closed it.
**A claim in the gate's own rationale was false.** The comment called
`worktree add` "the FIRST host-side git write of this phase"; `discardWorktree`
above already runs `worktree remove --force` and `worktree unlock` with the
same cwd. They materialise nothing, so no filter runs — but that distinction
is the entire reason the gate can sit below them, and the looser sentence
invites the next maintainer to add a checkout above it.
Tests follow the same rule as the code. The ADMIT path had no coverage, so a
mutation mangling the parsed gitdir survived: every refusal fixture would still
refuse, because a mangled target fails to resolve and the location check fails
closed — while in production the same mangling refuses every healthy tree. It
is asserted now, relative spelling included, since git writes that pointer
relative when it can. And the revert gate is driven through the phase itself,
by a fake runner that rewrites `.git` from inside the probe tree — which is who
does it in production.
Nine mutants, nine reds.
* test(review): gate the new cases off the platform where containment cannot exist
The three tests added for this gate are deterministically red on Windows, and
the reason is this PR's own sibling: `mountRootFor` refuses every absolute
Windows path, because a drive letter is a colon and the `-v` grammar has one
separator. Containment is unavailable there by design, so the gates never
speak, `out.json` never carries the refusal, and the assertions fail for a
reason that has nothing to do with what they test. The Windows lane runs only
under `merge_group` and reports as skipped on the PR page, so green here was
no evidence — the first red would have landed in the queue and stalled the
batch.
Gated with the same named predicate the sibling suite uses, so the reason is
stated once rather than repeated three times.
Two more from the same review. The fake runner embedded the planted path in a
JS string literal by interpolation, which a Windows path's backslashes
self-mangle; it is generated through `JSON.stringify` now. And both new tests
had pasted the ~22-line fixture that `scaffoldModifiedPr` already provides,
leaving three copies to evolve in lock-step — the helper takes the worktree
location as a parameter instead, since the location is exactly what differs
and exactly why the gates fire.
The three gates were re-mutated after the refactor, one at a time: three
mutants, three reds.
* test(review): assert the property, not the sentence that explains it
Both gate tests asserted a refusal MESSAGE, and `worktreeCreateFailureDetail`
serializes that message identically whether the gate throws before the write
or after it. So the mutant that matters — moving `worktree add` above the
gate, which is exactly what the comment beside it warns a refactor might do —
checked files out through the planted pointer, ran its filters on the host,
and left both tests green.
Creation: the probe tree not existing is what says the spawn never ran.
Revert: the planted repository now carries a real `filter.evil.smudge`, and
the assertion is that its canary was never written. That is the property
itself rather than a proxy for it — with the gate removed the filter EXECUTES
on the host inside the suite, which is what the test now catches.
The fixture had to become coherent for this to mean anything: an empty planted
directory fails the checkout for reasons unrelated to the gate, so it copies
the real admin entry and points `commondir` at a planted common dir, the shape
the reproduction in the PR description uses.
* fix(review): ask git where the repository is, and gate the entrances that were left
**Stop re-implementing git's resolver.** The gate parsed `gitdir:` and resolved
it in Node while the write that followed resolved the same file in git, and the
two disagree in ways that are individually small and collectively unbounded:
JS `trim()` strips U+00A0 where git's `read_gitfile` trims only C-locale space,
so `gitdir: <NBSP><real entry>` resolved outside the mount here — admitted —
and to a planted entry inside it there; and `resolve()` is lexical where a
spawned git resolves relative targets against the tree's physical path after
chdir. Every divergence admits a pointer whose real referent only git sees, and
enumerating them has no last corner.
So the resolver that decides is now the resolver that acts: `rev-parse
--absolute-git-dir`, run in the tree, with hooks and fsmonitor inert. What
stays ours is the only judgment that was ever ours — WHERE the answer lives.
The fixtures had to become real repositories for this to mean anything, which
is an improvement on its own.
**Three more entrances, all probed to host execution.** Round 2 recorded that
per-call-site gating does not close the class, and I argued a shared layer
would be noise. That was wrong, and the review paid for the answer: base-tree's
rebuild had no defence at all; scratch-tree's screen matches
`filter.*.smudge|clean` and not `filter.*.process`, which also executes; and
`--resume` runs an index-refreshing `git status` through the same pointer, so a
planted `core.fsmonitor` fires inside the very command that collects the
ruling's evidence — the attack does not need the resume to succeed. All three
now ask the same question, and `status` carries an inert fsmonitor besides.
**Absence is the caller's question, not this one's.** The first cut refused a
tree that does not exist, which is the state `--resume` legitimately asks
about — 26 tests, measured. It returns null there now.
* fix(review): the entrances round 6 found, and two limits stated rather than papered over
Case, first: the verdict is a string comparison, and Node's JS `realpathSync`
hands back the spelling it was asked for whenever the lookup succeeded — so on
a case-insensitive filesystem (default macOS APFS, a host this pipeline
supports) a planted entry spelled with different case resolves INSIDE the
mount and compares OUTSIDE it. `realpathSync.native` asks the operating system
for the stored name, so both sides arrive spelled as the filesystem holds them.
Two more entrances. scratch-tree's REUSE path reaches a `checkout --force` of
its own through the scratch tree's pointer, and fetch-pr's FRESH path checks
out through the review worktree's. The reuse one is not a refusal, though: an
unusable leftover is exactly what the rebuild exists for, and refusing there
turned "rebuild over it" into "fail the command" — three tests said so.
And `--resume` refused to CRASH where this file's own contract, the option's
describe and the docs all promise refuse-to-FRESH. Throwing propagated out of
`runFetchPr`, so `cleanStale` — the thing that removes the planted tree — was
never reached. It returns a refusal reason now and the fresh path sweeps.
The two new refusal tests were red on Windows, for the third time in this PR
and for the same reason: `mountRootFor` refuses every absolute Windows path,
so containment is unavailable there by design and the gate never speaks. Both
gated off win32 like their siblings.
Two limits I could not close, stated where they are rather than left looking
proven. The `--resume` gate's wiring is pinned by no test: `fetch-pr.test.ts`
mocks `node:fs` without `existsSync`, so the tree never exists there and the
gate is inert — deleting it ships green. And the reuse-path gate is defence in
depth rather than a demonstrated check: with both trees pointed at one planted
common dir and a `filter.*.process` the existing screen misses, removing the
gate still executes nothing, because the reset declines that tree for its own
reasons first.
* fix(review): stop editing git's answer, and gate the pointer that is actually used
Two blockers, both inside the previous round's own fixes.
`.trim()` on `rev-parse`'s stdout re-opened the exact divergence the paragraph
above it closes. It strips U+00A0, U+FEFF and the U+2000 block, so an admin
entry whose directory NAME ends in one is resolved by git with the character
and judged here without it — and a twin of that name minus the character,
symlinked outside the mount, is where the judgment then lands. Only the
terminator `rev-parse` adds may be removed.
And the fresh-path gate checked the wrong pointer. `git()` sets no cwd, so
`worktree add` resolves the repository from `process.cwd()` — while the gate
asked about `wt`, which `cleanStale` has just removed, and a tree that does
not exist has no pointer to distrust. It was a no-op on every reachable path.
It asks about the launch directory now, which is the one the nested geometry
`mountRootFor` documents lets an outer review's containerized phase rewrite.
* fix(review): let the launch directory resolve the way git does
Last round's gate asked `untrustedGitfile` about `process.cwd()`, and that
function demands `<tree>/.git` — right for the trees this pipeline builds,
wrong for the directory a command was launched from. Any subdirectory of a
checkout has no `.git` of its own; git walks up. So in the nested geometry the
pipeline runs its own reviews in — cwd at `<review tree>/packages/cli` — the
gate answered "has no .git to resolve" and fetch-pr aborted before the
`worktree add` that would have succeeded. 77 tests in one suite, at the
reviewer's HEAD.
My checkout is not under `.qwen/tmp`, so `mountRootFor` returned null here and
the gate never fired: the suite stayed green while the dogfood lane broke.
That blind spot is now a test — a subdirectory of a mounted checkout, asserted
to resolve.
The launch directory gets its own question: ask git, from there, what
repository `worktree add` will find, and judge only where that answer lives.
No shape check, because the shape belongs to a tree and this is not one.
* fix(review): three failures of my own, and the block gate that stops the fourth
`realpathSync.native` is not always there. A suite that mocks
`node:fs.realpathSync` as a bare `vi.fn` gives it no `.native`, and reaching
through it threw a TypeError straight into the fail-closed catch — refusing
every worktree creation in any checkout that happens to sit under `.qwen/tmp`,
which is the geometry this pipeline reviews itself in. Falls back to the plain
call, and a test asks that shape directly, because the suite that carries the
mock never reaches this gate from an unmounted checkout.
The real-git fixtures ran without `isolateHostGitConfig`, which every other
real-git block in the same file installs. On a host with `commit.gpgsign=true`
and no usable key the fixture commit fails and the whole block goes red for
reasons unrelated to the gate — reproduced with a poisoned HOME.
And one more case was left ungated for the Windows lane. Rather than gate it
and wait for the fifth, the whole block is gated: every case in it builds a
layout under `.qwen/tmp`, and on Windows neither the question nor the fixture
exists.
Worth recording why these three reached the branch at all: my checkout is not
under `.qwen/tmp`, so `mountRootFor` answers null here and every one of these
gates is inert on my machine. Green locally proves nothing about the geometry
they are for.
* fix(review): gate the reads and the launch directories, not only the writes
The location gate this PR added was asked at the writes and nowhere else,
which is how the same class kept resurfacing: a class closed one call site
at a time re-opens at the next call site somebody adds. Four entrances stood
open behind it, and two more of the same shape were a census away.
`worktreeResidue` measured BEFORE the gates its two call sites carry, and a
measurement is not a safe read: `status` refreshes the index, an index
refresh runs the repository's clean filter, and every identity check the
probe carries is honest about WHICH repository answers and blind to where
that repository lives. A coherent plant writes both halves of the round
trip, HEAD comes from the copied admin entry so the sha pin matches, and no
symlink is involved. The gate now runs first, and a refusal is
unmeasured-with-reason — the channel every caller already reads as "not
clean".
`fetch-pr` asked at step 4, after `cleanStale`'s force-remove and after the
PR fetch, both of which discover their repository from `process.cwd()`. In
the nested geometry `mountRootFor` documents, that launch directory is
inside the outer review's read-write mount, and a fetch through a rewritten
pointer loads the planted repository's transport config. The gate moves to
the top of the command, before the lease is even read; the step-4 ask stays
as the narrower window around the checkout `worktree add` performs.
`captureLocalDiff` gets the same treatment for the local lane, before its
first `rev-parse`.
`comment-status` and `repo-context` read through the pointer without asking
at all. No execution witness is claimed for `log`/`ls-tree`/`show` — what is
at stake there is the answer: `changedSinceComment: false` sourced from a
planted repository is this pipeline certifying that the code behind a
blocker thread did not move, and `repo-context` exists precisely to take
identity files from the merge base rather than from the PR head.
And the containment predicate judged against the innermost `.qwen/tmp` only.
The MOUNT has to stay innermost — the first occurrence would pull
`<repo>/.git` and every sibling checkout into the container — but "which
directory may a container have been given read-write" and "where may
reviewed code already have written" are different questions, and in the
nested geometry they have different answers. The judgement now takes the
outermost layer. It costs the honest layouts nothing: `git worktree add` run
from inside a linked worktree still writes its admin entry under the main
repository's `.git/worktrees/`, outside every layer.
`mountRootFor` moves from `sandboxed-exec` into `worktree` (re-exported from
its old home) so the gates can default to it instead of taking it as an
argument every new call site has to remember to pass.
Tests: `host-execution.canary.test.ts` asks the property once for every
route, with a live plant as its oracle — a rewritten gitfile naming an admin
entry whose commondir names a planted repository carrying a filter git
executes, plus a canary the filter writes on the host. Its first case
asserts the canary IS written when the same command runs ungated, so a
fixture that stops being an attack fails there rather than certifying the
gates that walked around it. Six mutations, six distinct reds. The committed
fixtures that wrote a `gitdir: `-prefixed backpointer now write the bare
path git actually writes; the prefixed form failed the round-trip check
first and masked every route behind it.
* fix(review): gate the base tree's reuse fast path too
The census that found `repo-context` and `captureLocalDiff` found one more,
and this one returns before the gate rather than running beside it: the
base-tree reuse branch answers "already built at <sha>" on two facts that
both live inside the mount — a marker file in the base tree, and
`rev-parse HEAD` resolved through that tree's own `.git`. A planted
repository answers the right sha for a working tree holding anything at
all, and the A/B's BASE side is then the reviewed code's own; every "the
base behaves differently" verdict a shard reports would be its author's.
Not a refusal: an unusable leftover is what the rebuild exists for.
Falling through discards the tree — removing the plant with it — and
creates a fresh one through the review worktree's pointer, which the gate
before `worktree add` already checks. Same shape as `scratch-tree`'s reuse
path, for the same reason.
The canary suite gains the arm: with the plant in place the report must not
say "reusing it". Removing the gate reds it, and the mutant's own note names
the planted tree as an already-built base.
* fix(review): ask the launch-directory question where all of it goes through
Hoisting the gate inside `fetch-pr` fixed one command. The census that
followed found the same shape in `load-rules` (this review's own rules, read
with `show <base>:<path>`), `submit` (where a submission goes),
`match-remote` (which remote gets fetched), `run`, `revert-hunk` and
`cleanup` — none of which knows it is asking this question, and none of
which should have to.
They all share one thing: every wrapper in `lib/git` runs git with no `cwd`,
so each discovers its repository from `process.cwd()`. That is the choke
point, so the question moves there — four functions in one file instead of a
gate per command, which is the pattern that kept re-opening this class.
The throwing wrappers throw. `gitProbe` answers its own documented "could
not be run at all" value instead, because that is a probe's contract and
every `gitOpt` caller already handles it; the command does not run either
way, which is the part that matters. `releaseWorktree` is the case that
shows why this is the right split: through a poisoned launch directory its
`worktree remove` is refused, and the `rmSync` behind it still clears the
path — the remedy keeps working without git acting through the plant.
Memoized per directory; outside a review temp dir it costs no syscall at all
(`mountRootFor` returns null on a string scan). The cached admit is the same
TOCTOU residual the per-tree gates already document and do not close.
`captureLocalDiff`'s own gate goes: it now contributes nothing but a nicer
message, and a duplicate that has to be kept in step with the real one is a
liability. `fetch-pr` keeps its own, because that command changes state that
is not a git call — the lease read and write — before it makes one, and the
ordering test pins exactly that.
Seven mutations, seven distinct red sets, baseline green.
* refactor(review): let the gates default to the mount root everywhere
Every production call site now asks `untrustedGitfile(tree)` and
`untrustedRepositoryFrom(cwd)` rather than threading `mountRootFor` through
by hand. That is the point of moving it into this module: the argument stays
injectable for the fixtures that drive the judgement with their own root, but
a new call site cannot get it wrong by forgetting to pass one — and two
spellings of the same check in one file is the kind of thing a reader has to
stop and reconcile.
The test-only reset seam on the launch-directory verdict goes with it: the
verdict is keyed on the directory, and every fixture stands its plant up in a
fresh temp dir, so a cached answer can never be served for a different one.
* fix(review): gate revert-hunk's tree reads, and reflow a comment
The last tree-scoped command that asked nothing. `git apply` writes into the
tree without running a filter, so no execution is claimed here — what the
gate refuses is deciding a probe's outcome on answers that come out of the
plant. `--show-toplevel` still prints this tree when the gitfile has been
rewritten (that is the shape the location gate exists for), and `check-attr`
and `ls-files` then report the planted repository's EOL regime and index: the
two facts this function refuses a conversion on. A scratch tree is inside the
directory the sandbox hands the reviewed code read-write, so that writer is
the code under review.
A harness fact like every other refusal there — nothing is claimed about the
hunk, and the tree is untouched.
That leaves one deliberate exception, stated rather than left to be found:
`cleanup` still runs `worktree prune` and `branch -D` from the launch
directory ungated, because it is the remedy — refusing there would leave a
planted tree with nothing able to sweep it, and its `rmSync` clears the path
whether or not git can act through the pointer.
* fix(review): let a refusal say it refused, and re-ask where git resolves
Six review Criticals with one theme: a gate that answers correctly, then
reports through a channel its callers read as an answer.
`gitProbe` returned the launch-dir refusal as `{out: null, status: null}`
and every consumer used the status-blind `gitOpt`, so `releaseWorktree`
deleted the directory with `rmSync`, reported `freed: true`, and left
behind the registration and branch that wedge the next `worktree add`;
`load-rules` wrote an empty rules file into every agent brief while
printing "No review rules found". `status: null` already documented "the
command could not be run at all" — git answers a path absent at a ref
with 128 — so the fix keys on that and carries the refusal text beside
it, rather than adding a third state. `releaseWorktree` now reports the
release it could not make, and `load-rules` separates a source it could
not read from one that is absent.
The launch-dir verdict was memoized for the whole process, so a pointer
rewritten after the run's first git call was served that first clean
answer forever — and in the nested geometry the outer review's
containerized build holds the directory read-write for the length of a
command, which is a rewrite between calls, not between a check and its
use. Only the negative is cached now, and only outside a review temp dir
where it costs no syscall.
fetch-pr's step-4 refusal was thrown inside the try whose catch rolled
the fetched ref back with an ungated `branch -D`, so the gate's own
answer executed the plant's reference-transaction hooks on the host and
the run died as `Failed to create worktree at …`, indistinguishable from
an infrastructure failure. The ask is hoisted out, and the five remaining
spawns in fetch-pr and cleanup that resolved their repository from
`process.cwd()` go through lib/git's gated wrappers. Cleanup still
sweeps: the `rmSync` that clears the path is not a git call, and a
measured ungated prune was clearing the PLANT's registrations, not the
real one.
A lease moved out of the mounted directory with no fallback read, so an
older build's live lock was invisible to the gate and the next
acquisition deleted it before sweeping its worktree mid-run — #9205, the
incident the lease exists to prevent. The gate reads both locations for
one release, and acquisition refuses a foreign legacy lease instead of
removing it.
base-tree's reuse fast path policied the tree's pointer but not its
contents, and `rev-parse HEAD` does not move when working files change,
so the reviewed build could overwrite the base checkout and the A/B
compared the PR against a copy of itself — a regression it breaks then
reads as pre-existing on both sides. Tracked dirt falls through to the
rebuild; untracked build output still reuses, or the concurrent-shard
clobber the fast path prevents comes back. Measured: `npm run build`
here modifies no tracked file.
revert-hunk decided where `--tree`'s repository resolves but never asked
whether `--tree` IS that tree, so a symlink at the scratch path aimed
`git apply -R` at another legitimate tree while the report certified the
scratch path. The leaf is refused, and re-read immediately before the
write. The bounded ancestor walk `resetScratchTree` also does is not
added here: this command has no common dir to bound at, and an unbounded
walk refuses every tree under a linked `/tmp`.
Also one definition of the attack fixture instead of ten copies across
five suites, which repairs the one copy whose admin-entry backpointer
carried a `gitdir: ` prefix git never writes there.
* fix(review): ask a pointer where it resolves to, and stamp the run that built a tree
Four review Criticals with one theme: a gate that asked half the question,
and a marker that recorded what it vouched for but not who vouched.
A gitfile rewritten to `gitdir: <repo>/.git` names the repository's OWN
common dir, which resolves OUTSIDE the mount, so every gate that asked only
where the answer lives admitted it and then acted on the main repository
through it — measured, `status` refreshed the main index and `rev-parse
HEAD` answered the main head, so the reads a review treats as fact came
from a tree nobody verified. Both questions are now asked in one place, so
the tree gate, the launch-directory gate and the residue probe close
together instead of one entrance at a time, and the two `rev-parse` calls
share one sanitized spawn instead of duplicating its boilerplate.
scratch-tree's reuse route asked about the scratch pointer only, never
about the review worktree's — the one `headSha` and every comparison inside
the reset resolve through — so an unpinned run answered `available: true,
reused: true` with the main head and reset the verifier's tree to the
user's own commit. Suspicion there falls through to the rebuild, whose gate
refuses, rather than refusing on the spot.
`mountRootFor` matched no temp dir for a process standing AT `.qwen/tmp`,
because the marker ends in a separator: the launch-directory question went
unpoliced there and `lib/git` memoized that negative as trusted.
The base-tree reuse marker recorded a sha but not a run, and `cleanStale`
never releases `-base`, so an earlier round's tree was certified as this
run's A/B base after a whole containerized phase could drop untracked
executables into it — `dist/cli.js` is what a host-side A/B runs, and
`--untracked-files=no` cannot see it. Refusing any untracked file at all
would disable every legitimate reuse and bring back the concurrent-shard
clobber the fast path exists to prevent, so the marker carries the run
epoch the deadline stamps and the session ledger already key on: same-run
reuse survives, an earlier run's tree rebuilds.
And the lease move left `isReviewPlumbing` naming its directories by hand,
so a checkout holding a lease captured that churned lease JSON as the
user's untracked change and an interleaved local round could never again
report "no changes". The pattern is built from the `paths.ts` constants
now, which is what keeps the next move from re-opening it.
Two existing witnesses were repaired rather than left green for the wrong
reason: the canary's base-tree fixture and the different-sha marker case
both hand-wrote a one-line marker, which the epoch fence now turns away
before the gate each exists to exercise. Both stamp the epoch, and a probe
removing the reuse pointer gate reds the canary again.
* fix(review): fail closed where a gate could not be run, and stop editing git's answer
Seven review Criticals, all in code this pull request added: four ways a gate
reported a success it had not earned, one that threw where its contract says it
degrades, one lane-breaking test, and one regex that edited the very answer it
exists to pass through unedited.
`rev-parse` answered null for three different things — git ran and said this is
no repository, git timed out, and git died on some other fatal — and two of the
three gates read all three as "no objection". The launch-directory gate budgets
30s against the protected commands' 120s, and the residue probe's spawns carry
no budget at all, so a config sized to parse between the two left the gate
silent while the command it gates resolved through the pointer nobody had
judged: measured, `status` refreshed the planted index and ran its clean filter
on the host. The null is classified once, at the spawn that produces it, and
both gates refuse the class they cannot judge. A genuine not-a-repository still
passes through — that is the caller's own error path to own, and refusing it
would answer a question nobody asked. Pinning `LC_ALL` is what makes that
classification depend on git's behaviour rather than on the runner's locale.
The same strip that removed git's record terminator also removed a `\r` that
was the last byte of the path itself, which is precisely what the docstring
above it forbids: a plant named `evil\r` was judged at its twin `evil`,
symlinked outside the mount, and admitted while every gated command resolved
through the plant.
`gitProbe`'s launch-dir pre-check read `process.cwd()` outside its try, so a
review whose worktree was swept out from under it threw ENOENT through
`releaseWorktree`'s documented never-throws contract and aborted the sweep
before the branch delete and the lease release — leaving the stale lease that
refuses every later cleanup of that target.
`cleanup` collapsed a refusal into each of its two benign answers: `refExists`
said "no such branch", so the branch leg was skipped silently and the lease
released over a surviving branch, and `pruneWorktrees` said "nothing to prune",
so the symlink arm announced `Removed … link` over a registration that stayed.
Both report the refusal and hold the lease now. A genuine prune failure stays
swallowed, because it must not mask the error that got us here.
And the rewritten `toContain('common dir')` assertion depended on the
mount-relative gate answering first, which it never does on Windows — the same
lane this pull request has broken three times before. Split by the file's own
convention rather than branching one expectation on `process.platform`, so each
lane asserts a single unconditional sentence.
Every guard above has a witness that reds without it, including the admit arms:
a genuine not-a-repository keeps passing, and the residue probe's `status` is
shown never to run at all.
* fix(review): fail cleanup's branch probe on any non-answer, survive a deleted cwd
With --verify --quiet only exit 1 is genuine absence: a null status
(spawn failure, timeout kill) or a 128 fatal used to read as "no such
branch", so the delete was skipped, the lease released, and "Nothing
to clean" printed over a surviving branch (R19-1). Name the non-answer
and hold the lease instead.
Also capture the launch directory at runCleanup's entry and hand it to
the ancestor walks as an explicit stopAt: their default reads
process.cwd() outside any try, so a deleted cwd threw uv_cwd out of the
best-effort sweep before any degradation could run (the cleanup half of
R19-4). The holder-skip note now names the path the lease was found at,
which during the rollout can be the pre-move path (R20-1).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): walk --tree's ancestors for a planted symlink, not only its leaf
The redirect gate checked the leaf of --tree alone, at entry and again
before the mutating git apply. A symlink swapped in at an ancestor
between the review temp root and the leaf — after the create/sweep
checks ran — was invisible to every check in the chain, and git apply -R
then reverse-applied into whichever tree the link names while the report
certified the scratch path (R24-1). Walk the ancestors bounded at the
outermost review temp root, at both checks; a --tree outside any review
temp root keeps the leaf check so a linked /var refuses nothing.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): mirror new leases at the legacy path, bound the legacy read
The dual-location lease read was one-directional: a pre-move build stays
blind to leases written at the new path, so for the rollout window an old
fetch-pr passes its gate over a live lease and force-removes the new
session's worktree mid-run (R16-1). Mirror the lease at the legacy path
after the new-path wx write wins; an honored foreign lease surfacing
before the mirror backs the acquisition out instead of clobbering it.
The legacy read also re-granted gate authority to the one directory
reviewed code can write: a planted legacy lease naming a foreign session
was a permanent denial of service (R24-2). A legacy lease is now honored
only while its mtime predates LEGACY_LEASE_CUTOFF_MS (the release date of
the first build carrying the move — MUST be set before shipping).
Release and diagnostics see both locations (R20-1): clear removes the
legacy path too (recursive, so a directory at the lease name cannot
EISDIR-wedge), and the held-lease messages name the path the lease was
actually found at via the new readReviewWorktreeLeaseAt. The finalizer
sweeps the legacy directory only for leases content-identical to the
new-path twin — the mirror is readable from inside the mount, so copied
credentials prove nothing, and a plant must never drive a host-side
removal. readLease lstats before opening, so a planted FIFO hangs no
gate read (R24-4).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): fence base-tree reuse with a per-run secret kept outside the mount
The reuse fence keyed on inputs the sandbox's read-write mount exposes:
the run epoch was the plan's mtime and the plan lives inside the mount,
so reviewed code could forge the stamp and have the pipeline certify an
attacker-shaped tree as the A/B base (R15-4). The stamp's second line is
now a random per-run nonce persisted beside REVIEW_LEASE_DIR — created
atomically by the run's first host process, adopted by its shards,
unreadable and unwritable from inside the mount. The failed marker
carries the same fence: a planted sha-only marker no longer settles the
A/B lane into "already failed" with no build attempted (R22-1).
At marker write the untracked path set the build legitimately left is
recorded host-side, and reuse requires the current set to be a subset —
an untracked plant dropped after the stamp (dist/cli.js, what a
host-side A/B executes as the BASE side) falls through to the rebuild
that sweeps it (R25-1). And when a tree THIS run built fails a state
check — legitimate codegen dirt, a concurrent shard's snapshot update —
the command declines busy instead of discarding the live tree another
shard may be mid-A/B in; discard-and-rebuild is kept for cross-run
leftovers, forges, and plants (R26-1).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): fail the trust gates closed when the mount root is refused
mountRootFor's null was overloaded: "outside any temp dir" and "inside
one, but refused" were the same value, and every trust gate read a
refusal as "nothing to police" — measured end-to-end with a symlinked
.qwen/tmp, where a gated worktree add ran through the plant and its
smudge filter wrote its canary on the host (R26-3). The gates now ask
the containment question lexically (insideReviewTmpLexically, no
filesystem) beside mountRootFor, and where the spelling says inside and
the mount says null they refuse with a reason of their own — including
a launch directory that no longer exists at its spelling, the rename
attack's shape.
The launch-dir gate's memo made that inversion permanent: a null
produced while mountRootFor consulted the filesystem was cached TRUSTED
for the process lifetime, so one from inside the
outer mount switched the gate off and a planted filter.<x>.clean
executed as the review user (R26-2). Only the pure no-marker string
scan is memoized now; inside a review temp dir the question is re-asked
on every call, as documented.
Also: releaseWorktree's not-freed predicate keyed only on refusal, so a
probe's {status: null} (spawn failure, timeout kill, deleted cwd) read
as "no objection" and freed:true was certified over a surviving
registration and branch — the next add met "missing but already
registered" (R24-3); and its first statement read the cwd outside any
try, so a deleted cwd threw uv_cwd through the never-throws contract
(R19-4). comment-status evaluated the same trust gate twice per run and
could certify a trust state its own threads[] contradicted; the verdict
is now computed once and handed to makeGitProbe (R26-4). mountRootFor's
walk bound moves from the checkout's parent to the geometry-aware root
(a linked direct parent refused every macOS /tmp checkout, R19-3), and
its Windows refusal covers the colon-less UNC shape as a class
(R19-2).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): round-trip the admin entry's backpointer in the location gate
untrustedPointer admitted a gitfile rewritten to a SIBLING worktree's
legitimate admin entry: the entry is outside the mount and is not the
common dir, so both location questions passed while every host-side
command through it measured and mutated the sibling tree (R23-2). The
gate now carries the same backpointer round-trip the write paths always
have: the entry's gitdir file must resolve back to the tree it is asked
about, or the tree is refused. Only a definitive mismatch is this arm's
to name — an unreadable or dangling backpointer redirects nothing, is
refused by the downstream identity checks with their own reasons, and
the one writer who could corrupt a backpointer to duck the mismatch
cannot reach an entry outside the mount at all.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): count a lossy decode of git's answer as no answer (R8-2)
encoding: 'utf8' maps an undecodable byte in git's byte-exact rev-parse
output to U+FFFD, so a gitfile target holding an invalid UTF-8 byte was
judged at the U+FFFD spelling while git acted on the original bytes: a
plant under the raw-byte name inside the mount and a decoy under the
U+FFFD one, symlinked to the tree's real admin entry outside it, passed
every question the location gate asks — the round-trip included — while
the gated write resolved through the plant. No gate may judge a spelling
git never printed: a U+FFFD anywhere in the answer now counts as the
question not answered, which every caller already fails closed on.
The witness plants the full shape (raw-byte admin entry, U+FFFD decoy)
and is itself gated to Linux: NTFS is UTF-16 and APFS rejects invalid
UTF-8 names with EILSEQ, so neither the fixture nor the attack can
exist there.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): fail the prune arm when git could not be run at all (R23-8)
The symlink arm's pruneWorktrees() surfaced only the probe's refusal, so
the third shape {out: null, status: null, refusal: null} — a spawn
failure, the timeout kill, a deleted cwd — read as a successful prune:
the arm announced "Removed ... link", wrote no stderr line, and released
the lease over a registration git never swept, and the next worktree add
met "missing but already registered" with nobody told why. Same
shape-conflation class as R19-1 at a second site. Only genuine non-zero
exits stay swallowed.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(review): type the lease-read mocks with their parameters
tsc --build (unlike the vitest run) rejects the readReviewWorktreeLeaseAt
delegation calling a zero-argument vi.fn with two.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): close what the trust-artifact move's first pass missed
Four follow-ups from an adversarial audit of the fence:
The untracked record was git status's collapsed path set: dist/ and
node_modules/ recorded as one entry each, so a plant dropped INSIDE a
directory the build left — the exact executable a host-side A/B runs —
changed no membership and passed the subset fence. The record is now
file-level (ls-files --others, which never collapses), so an addition
inside a recorded directory IS a membership change.
The trust file was keyed by the plan's path and mtime, and the plan lives
inside the mount: a utimensat backdate to an earlier run's exact stamp
re-keyed this run to that run's file, adopting its nonce. The key now
takes max(mtimeMs, ctimeMs) — ctime cannot be set from userland, so the
identity cannot be rolled back.
The busy arm over-generalized: a rewritten pointer or moved HEAD on a
tree this run built has no benign cause, and declining left the plant
standing for the run. Those arms now fall through to discard-and-rebuild
— while tracked dirt AND untracked additions (an A/B's own snapshot
--update or cache output is the benign shape) decline busy instead.
And the discard itself could not rebuild a planted tree: worktree remove
refuses a tree whose gitfile no longer points back, so the removal
cleared the coherent plant's admin dir while the real registration
survived and the next add met "missing but already registered" — the
deferred refusal->rebuild wedge. When the remove fails, the reverse scan
now also runs, narrowed to the tree's own basename so a tampered sibling
entry is never deleted.
Also pins the sweep's per-field equality with a same-target doctored
twin (the worktreePath comparison had no witness) and gives
readReviewWorktreeLeaseAt a legacy found-at-path witness.
* fix(review): close the audit's second pass — witness gaps and one substitution route
From the adversarial audit of the takeover batch:
- releaseWorktree's symlink arm now has its own witness for the
status-null prune (the main arm had one; the sibling arm was pinned by
nothing — the mutation survived green).
- The launch-dir memo's comment now names the rename attack's actual
premise — Node's cachedCwd, not a kernel property, so a future
in-process chdir is the thing that re-opens the window — and the canary
pins the stale-spelling premise instead of assuming it.
- scratch-tree's --standalone shape asks the location question before
buildStandaloneTree: headSha was read through the review worktree's own
gitfile, and with no --fetched-sha pin a planted HEAD would put the
plant's content in the tree presented as the commit under review.
- revert-hunk's outermost-bound arithmetic gains a nested-geometry
witness (a swap at the OUTER layer), so a drift to lastIndexOf reds it.
* fix(review): anchor the scratch sweep at the captured root (R30-6)
The entry-captured repositoryRoot was handed to the ancestor walks but
not to scratchWorktreesOf: its resolve(worktree) and redirectedAncestor
default stop both read process.cwd() again, so a cwd deleted mid-run
threw uv_cwd out of the sweep — past the entry guard that exists to
catch exactly that. Both now anchor at the captured root. The witness is
the call count: a full cleanup reads process.cwd() exactly once.
* fix(review): the base-tree fence reads only host-side state
Rounds 27-30 found the nonce-in-marker design's hole: the markers carried
the run secret in cleartext inside the tree — a child of the read-write
mount — so reviewed code read it and forged a settling failed-marker (the
A/B lane suppressed as "infrastructure") or an ok marker over a tree it
shaped. The fence now reads the trust record alone, kept outside the
mount: per tree it carries the pinned base, the build's verdict, and the
untracked inventory per file with its size+ctime pair — an in-place
rewrite of a recorded file is as visible as an added one. Markers in the
tree are informational baseSha notes, excluded from every fence decision.
The trust file is one per plan path, rotated in place on identity drift
so no earlier generation survives to adopt; the run identity is the
worktree lease's mtime when one is held (host-side, mount-untouchable)
and the plan mtime with the ledger's tolerance otherwise. A mid-run
mergeBaseSha rewrite conflicts with the pin and declines rather than
rebuilding at a sha named after the fact. Same-run trees with a torn or
missing record decline busy instead of being discarded; pointer/HEAD
rewrites still discard-and-rebuild. The record step re-asks the pointer
question first, and the git helpers carry the inert-config pins and a
512 MiB buffer so a real tree's ignored listing cannot silently drop the
record.
* fix(review): the mount-null is three answers, and the gates split them
Where containment cannot exist — a drive-letter colon, a UNC share, a
colon-bearing POSIX checkout — the review never ran sandboxed, so the
mount is no trust boundary and the gates' docstrings promise silence.
The fail-closed arm read every null as a refusal and refused
unconditionally there (measured: 43 red tests under the declared Windows
model). mountRootFor now shares one implementation with a discriminated
nullKind (outside / unmountable / refused); the gates fail closed only on
a redirect or a root that vanished mid-check, and stay silent where
containment cannot exist.
The pointer gate's round-trip is now exact: the admin entry must own the
tree the question is about (untrustedRepositoryFrom compares against
git's own --show-toplevel, which a borrow cannot bring into agreement),
an owner that cannot be resolved fails closed, and a redirect planted
between the mount root and the owner is refused before anything resolves
through it — the round trip must not agree with a link. The mount root is
threaded through the questions rather than re-asked mid-gate, so a
mid-gate flip of the mount cannot split the answer. The flat-case walk
bound moves to .qwen: the checkout's own directory is the user's layout,
not the pipeline's.
releaseWorktree's not-freed predicate keys on the prune alone now: a
remove that answered 0 already cleared the registration, and keying on
the remove's null published freed:false over a release the prune had
completed. Witnesses pin each arm, including the symlink arm that had
none.
* fix(review): the legacy lease is never believed, the mirror never fatal
The mtime-bounded honored read is gone entirely: a pinned-to-the-past
cutoff froze the honored population at release day while remaining a
utimes-forgeable permanent DoS channel from inside the mount. A
legacy-path file is residue now — the mirror still replaces it (loudly,
naming the displaced session, when it parses as another session's lock)
so pre-move builds keep seeing the lock, and only the new path answers
the gate. The mirror write is best-effort (an obstruction in the mounted
directory is mount weather, warned and skipped, never a failed
acquisition) and replaces via tmp-file + renameSync, which never follows
a planted symlink and never opens a readerless FIFO.
The finalizer is driven solely by the trusted directory: one destructive
pass per target, the legacy twin deleted only when content-identical —
and never listed, so a poisoned mount cannot shape the sweep. The lease
directory re-roots lexically to the outermost enclosing repository in the
nested geometry, so a review launched from inside another review's
worktree keeps its host-trusted state outside every layer's mount.
fetch-pr's step 4 also asks the destination's ancestors: a symlink left
standing by a declined releaseWorktree would otherwise have mkdirSync and
worktree add create and check out the PR's code at the link's target —
outside the review temp dir, unsandboxed. And the review job's lease
sweep feeds both globs through remove_review_tree's repair ladder instead
of a bare rm -rf, so a mode-000 wedge is reported rather than silently
surviving. Tests that delete the process's own cwd or need a mount are
gated to hosts where those exist.
* fix(review): judge the launch directory where the kernel stands (R30-47)
The launch-dir gate judged Node's cached process.cwd() spelling while
every wrapper spawns with no cwd, inheriting the KERNEL cwd — and the two
diverge exactly once an ancestor is renamed. A leaf rename inside the
mount plus the spelling re-stood as a symlink to an honest sibling made
the cached spelling pass every arm while git ran in the renamed plant.
The gate now asks the kernel (/bin/pwd -P) where the process stands and
judges that spelling; a divergence is the rename, refused. Windows keeps
the cached-spelling judgement: no /bin/pwd there, and no containment to
protect either. The canary witness drives the full leaf-rename shape and
goes red with the kernel judgement disabled.
* fix(review): the second audit round's findings on the batch itself
- base-tree trust: key under the OUTERMOST repository in nested geometry
— the lease re-roots there (R27-6) and the trust file must follow, or
it lands inside the outer review's writable mount where the fence's
record is the outer code's to forge; runIdentityMs then reads the lease
where it actually lives.
- redirectedAncestor: keep walking past a nonexistent component — a leaf
ENOENT under a symlinked ancestor used to end the walk with the link
itself un-lstat'd, which defeated the R27-9 gate when the link sat
above an unresolvable leaf.
- createReviewWorktreeLease: a same-session refresh preserves the lease
mtime — the base-tree run identity keys on it, and a resumed run would
otherwise rotate the trust state and discard the standing base tree.
- untrackedInventory is a null-prototype map: a file literally named
__proto__ would otherwise land in the prototype setter, invisible to
the fence.
- mirror/atomicWrite tmp files are removed on a failed rename; the
standalone refusal no longer prescribes a remedy (--fetched-sha) that
the same gate would refuse.
* fix(review): the mirror's tmp cleanup must not throw either
The mirror is documented never fatal, but the rename-failure catch called
rmSync unguarded: an rm error there escaped createReviewWorktreeLease
after the new-path lease had already won, rolling back the acquisition —
the fatal outcome the contract exists to prevent, one failure deeper.
Same wrap in the trust store's atomicWrite, where a failing cleanup would
have masked the original write error.
* fix(review): import rmSync in the trust store, type the rmSync mock's path
esbuild-transpiled vitest runs don't typecheck, so the round-3 commit
built red in CI: atomicWrite's new catch used rmSync without importing
it, and the lease suite's rmSync mock typed the path PathOrFileDescriptor
where the real signature takes PathLike.
* refactor(review): split the base-tree reuse fence out of this PR
The fence landed here on 2026-09-08 to answer R29-1, and it has been the
cluster the review keeps returning to: 11 of the 18 open Criticals live in
`base-tree.ts` / `lib/base-tree-trust.ts` or in code those two pulled into
other files, against 22% of the diff. Round 33 said it plainly — a cluster
that keeps producing siblings is one root cause being fixed instance by
instance, and splitting it ends the loop faster than another round.
The dependency runs one way: the fence uses the location gates from
`lib/worktree.ts`, nothing in the gates uses the fence. So it lifts out
whole, and it comes back as its own PR stacked on this one.
What leaves:
- `lib/base-tree-trust.ts` and its suite — the host-side trust store.
- `base-tree.ts` and `base-tree.test.ts` return to their state before the
fence commit. The location gates this PR is about (`untrustedGitfile` at
the reuse fast path, at the worktree read and at the rebuild) predate the
fence and stay; `GIT_NEUTRALIZE`, `gitOutZ` and the 512 MB `maxBuffer`
were introduced for the fence's inventory listing and leave with it.
- The canary's base-tree case goes back to certifying on the in-tree marker
— which is what the command reads again — and the fence's own
record-step canary leaves.
- `createReviewWorktreeLease`'s mtime preservation: it exists only so the
fence's run identity survives a resumed run's re-acquisition, and it is
R33-1's subject. Its tmp-file cleanup on a failed mirror rename is
unrelated and stays.
`packages/cli` is green (120 files, 5997 tests), `tsc --noEmit` clean, and
the one deterministically-red test on Linux (R33-2) leaves with the fence
file it lived in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vBGpv2LrLQ8QNEYu2gsNn
* fix(review): the seven Criticals the split leaves behind
Every one is mutation-probed: the guard is removed or negated, the case
named beside it goes red, and the rest of the suite stays green.
`releaseWorktree` (R32-2). The `refusal !== null` disjunct that forced
`freed: false` was not gated on "neither arm cleared the registration",
while `couldNotRun` beside it was. Each probe re-asks `launchDirRefusal()`
from scratch, so a refusal on one arm negated a release the other had
completed — the same miskeying R31-1 and R28-11 record for the null-status
arm. A refusal is already inside `couldNotRun` for its own arm, so it now
only ever supplies the REASON for that verdict, never makes one.
`untrustedPointer`, question 3 (R32-10). The containment test that gates
the `redirectedAncestor` walk was bounded at the DEEPEST review temp root
while the location question two above is widened to the OUTERMOST. A
sibling of the enclosing review's worktree, relinked at this tree, sat
outside the walk entirely and the realpath round trip agreed with the link.
Both questions ask against `outermostReviewTmpRoot` now.
`untrustedPointer`, question 2 (R33-3). `resolvedCommonDir` dropped the
`notARepository` half of the taxonomy, so a timeout, a spawn failure or a
git too old for `--path-format` read as "no objection" — and question 2 is
the only one of the three that sees `gitdir: <repo>/.git`. It carries the
taxonomy now and refuses every no-answer that is not git's own.
`dropWorktreeRegistration` (R32-12). git appends a counter to a worktree id
whenever the basename is taken, so the reverse scan's `id === basename`
narrowing could not match `wt1` — the entry the tree actually owns — and
left the registration standing after `rmSync` took the directory. Matched
against git's own id grammar instead; the gitdir-names-this-path and
tree-is-gone narrowings still carry the "never a sibling's" half.
`fetch-pr` step 4 (R32-11). The destination guard walked `dirname(wt)` and
never `wt`, so a link AT the worktree path redirected the create and the
checkout. The walk starts at the leaf; `redirectedAncestor` lstats its
first component before any stop test, so the ancestors are unchanged.
`clearReviewWorktreeLease` (R32-4). The legacy delete is the one
mount-resident removal in the clear path and it ran unfenced, after the
trusted lease was already released — a planted mode-500 directory or a
Windows handle threw out of `runCleanup`, whose tests pin it as never
throwing. Loud instead, like the mirror write.
The mirror displacement warning (R32-3). It interpolated a session id read
out of the writable mount verbatim; a newline in it forges a second stderr
line and a raw ESC repaints the terminal. Through `inertPath` now, the
flattener the rest of the repo already uses, and bounded — the warning
names the displaced run, it does not carry its payload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vBGpv2LrLQ8QNEYu2gsNn
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: wenshao <nigolaschao777@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
585589ac67
|
fix(dev): install hooks during worktree bootstrap (#11496)
* fix(dev): install hooks during worktree bootstrap
* fix(dev): keep worktree hook setup out of the shared git config
Husky runs `git config core.hooksPath .husky/_` with no --worktree, so
from a linked worktree the value lands in the config every worktree of
the repository shares while `.husky/_` is created only in the checkout
being bootstrapped. Skip the Husky step and report it when the key is
unset and this checkout does not own the repository config, so a
bootstrap can no longer repoint hook resolution for roots that never
received the wrappers. A primary checkout still installs hooks, and an
already-configured `core.hooksPath` is untouched.
Also drop the caller's success exit, which `install()` made unreachable
when it started exiting on every successful path, and bring the
pnpm-worktree-bootstrap design doc in line with a hook step it still
recorded as deliberately skipped.
The new fixture runs the real script against a throwaway root whose
`.git` is a file or a directory and whose config comes from a real
`git init` repo, which makes both new branches reachable and pins the
fail-closed guard: the injected `GIT_CONFIG_*` constant holds one value
for the child's whole lifetime and cannot express the unset state that
asks husky to write.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtuo29vduf
* fix(dev): ask git which root owns the worktree hook config
`ownsRepositoryConfig()` inferred repository-config ownership from the
filesystem shape of `.git`, and the proxy is wrong at both ends. With no
`.git` at all, `statSync(..., { throwIfNoEntry: false })` returns `undefined`
and the predicate folded that absence into "owns the config", so a
repository-less checkout ran husky into its `.git can't be found` soft failure
(exit 0) and the fail-closed check then turned a successful dependency install
into exit 1 blaming Husky. A `.git` file is not only a linked worktree either:
`git clone --separate-git-dir` checkouts and submodules have one too and do own
their config, so hooks were declined where they would have been correctly
scoped.
Ask git instead: `rev-parse --git-dir` differs from `--git-common-dir` only in
a linked worktree, and a failed `rev-parse` names the no-repository state, so
the skip notice stops asserting "linked worktree" and the bootstrap does not
gain a hard git dependency.
Also bind the fail-closed check to an artefact husky's own write produced, not
only to the config value. husky 9.1.7 exits 0 on every soft-failure path
(`index.js:16` git command not found, `index.js:17` refused `git config` write)
before the `mkdirSync(_())` on line 19, and a linked worktree inherits
`core.hooksPath` from the config it shares, so re-reading that value compared
it against itself and passed exactly when husky had created nothing.
Restore the registry-fallback case's hermeticity: `PATH` holds only the stub
directory again, which now also pins that the retry needs no ambient git.
Rebuild the ownership fixture from real git layouts, because `rev-parse`
resolves nothing for a `mkdirSync`'d `.git` or a hand-written `gitdir:` file,
and give the stub husky a failing mode so the exit code husky returns is pinned
rather than the install result's.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtuys2rjuu
* fix(dev): surface hooks-path read failures in worktree bootstrap
- getHooksPath() no longer collapses a refused git config read (exit 128/2/3) into "unset": only an absent key (exit 1) or a missing git binary keeps the skip path; anything else fails the bootstrap with the read error instead of a green, hook-less worktree.
- The linked-worktree skip notice now names the recovery path: re-run this script once the primary checkout has hooks installed.
- Tests cover a git stub exiting 128, the real-world unset HUSKY state, and pin the recovery sentence in the skip notice.
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
11e629b3a1
|
perf(export): split the transcript renderer's embedded CSS into a versioned asset (#11485)
* perf(export): split the transcript renderer's embedded CSS into a versioned asset The export renderer carried the web-shell component stylesheet as a ~2.3 MB string literal, so every reader parsed and compiled 4.1 MB of JS (56% of it dead CSS) before a transcript could render. Lift that literal out at export build time into a version-pinned, SRI-protected export-transcript-document.css served from unpkg and loaded via a nonce-bearing <link>, dropping the renderer JS to ~1.83 MB. The transform is an esbuild onLoad plugin in the web-templates export build that strips the injected CSS constant from web-shell's dist/transcript.js; web-shell source and runtime behavior are untouched. The document's fail-closed load-error path is extended to the stylesheet so a missing CSS asset fails the same way as a missing renderer. * fix(export): match the transcript CSS entry on Windows paths too esbuild hands plugin callbacks the platform-native absolute path, so the extract-transcript-css `onLoad` filter never matched on Windows: the callback did not run, `extractedTranscriptCss.css` stayed undefined, and the mandatory extraction guard below aborted the build. That build is not platform-gated — `scripts/prepare.js` runs it from `prepare`, so `npm ci` itself would fail on every Windows contributor and on the windows-latest legs of test_windows and desktop-release. Widen the separator to `[\\/]`, keeping the `transcript\.js$` tail so the barred `web-shell/dist/index.js` package root still does not match. The filter moves to transcript-css-entry.mjs because build.mjs is a top-level-await script with no harness — the same reason scripts/sdk-node-exporter-stub.js exists — so scripts/tests/transcript-css-entry-filter.test.js can pin both separators. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtufhms2u4 * fix(export): latch a stylesheet failure ahead of the parser block The `<link id="transcript-stylesheet">` sits in `<head>` while the `window` error listener that catches its failure is registered by an inline script in `<body>`. Chromium parser-blocks that script on the pending stylesheet, so when the CSS failure settles first the error event is dispatched with no listener to receive it: nothing marks the render as failed, both renderComplete guards in document-main.tsx pass, React mounts the transcript without any of the component CSS, and the requestAnimationFrame stamps `data-render-complete="true"`. The reviewer measured this fail-open above roughly 2.1 MB of document HTML (272 of the 1,000 permitted blocks) for a 404, an SRI rejection, a truncated body and a destroyed socket alike, and fail-closed for a *late* failure — so size, not failure kind, decides it. Latch the failure in `<head>` before the `<link>` is parsed and act on the latch from the existing body IIFE. The head script only records: `showLoadError()` writes `document.body.dataset` and `#app`, neither of which exists while the parser is still in `<head>`. It carries `nonce="__EXPORT_NONCE__"` because the document CSP allows no inline script, which is safe — `formatters/html.ts:53` replaces every occurrence. The listener is capture-phase because resource error events do not bubble. Not the `link.sheet === null` variant: the reviewer measured `sheet` non-null for a 404, a truncated body and a destroyed socket, so it only detects SRI rejection. scripts/tests/export-transcript-document-template.test.js pins the position, the nonce, the capture phase and the record-only shape; all five cases go red against the unpatched template. The behavioural witness (real Chromium, large document, instant CSS abort) belongs to the playwright transcript gate, which is out of budget on this host. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtufhms2u4 * fix(scripts): name the missing export renderer assets, pin the CSS gate The bundle copy became all-or-nothing over two artifacts but its `else` warning still named only the renderer, so the one new way to reach that branch — a tree built before the split, then `npm run bundle`d without rebuilding web-templates, which has the JS and no CSS — told the operator to go looking for a `export-transcript-document.js` that was sitting right there, and silently discarded it. List the paths that are actually absent, matching the sibling Web Shell warning twenty lines above. Stays warn-and-skip: prepare-package.js is the release gate. Also pin that release gate. Every fixture that reached `preparePackage` staged `dist/export-transcript-document.css` unconditionally, so deleting the new required-path entry left the whole test:scripts lane green; a release built with `npm ci --ignore-scripts` would then publish documents whose stylesheet 404s on unpkg for that version. `verifyBundleArtifacts` reports through console.error + process.exit(1) rather than a throw, so the new case stubs exit instead of copying the audio-capture sibling's `toThrow` idiom. Both cases were flip-checked: restoring the old warning text, and deleting the CSS line from prepare-package.js, each turn their case red. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtufhms2u4 * fix(export): pin the stylesheet-failure id contract and sync the design docs The <head> latch, the body listener and the <link> each spell 'transcript-stylesheet' independently and nothing compared them, so renaming either listener's id left the whole suite green while the latch recorded nothing - reinstating the fail-open the latch was added to close. Derive the id from the <link> and assert both listeners compare against it. Verified red under both mutations: latch id -> 'transcript-renderer' (1 failed | 5 passed), and the mirror with the body listener's id wrong and the latch intact (same). Both design docs still specified the two shapes the previous round replaced: the forward-slash-only onLoad filter that never matches on Windows, and the body-listener-only fail-closed extension. Section 1 now quotes the shipped TRANSCRIPT_CSS_ENTRY_FILTER and names transcript-css-entry.mjs, section 2 describes the <head> latch (position, nonce, capture phase, record-only), section 3 names the module-level render guard, and "Files affected" lists the three omitted files. EN and zh-CN are updated in the same commit. Also correct the shape-guard comment in build.mjs: the document nonces every <style> created through document.createElement, so the CSP would not block an un-stripped duplicate, and a 367-byte regrowth stays inside both byte budgets. That throw is the only guard on the duplicate-injection path. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtuo29vduf * fix(export): close out transcript CSS review comments * fix(export): tighten transcript CSS closeout Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
f6540d1994
|
fix(web-shell): make the workspace provider guard self-diagnosing and reload on root retry (#11421)
* fix(web-shell): make the workspace provider guard self-diagnosing and reload on root retry
The strict useDaemonWorkspace guard fails closed with a message that
cannot distinguish an absent provider from a duplicated module copy
(two DaemonWorkspaceContext instances in one page, seen in dev when the
module graph is refreshed under a live page), so the root boundary
showed a dead-end fallback that only a manual reload could clear.
The provider now registers a per-module-copy marker on first render,
and the guard's error states which of the three cases it hit: no
provider rendered, a provider rendered from a different module copy
(with both copy ids), or a provider from this copy rendered and the
consumer is outside its subtree. The standalone root boundary's retry
now reloads the page, since re-mounting the same broken module graph
would throw again.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(web-shell): restore spies in afterEach so a failing retry test cannot leak the console.error mock
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(web-shell): guard the root-fallback reload behind token survivability and sharpen copy diagnostics
Review round on
|
||
|
|
ad92d2bf8f
|
feat(autofix): run one bounded self-review before an address-review commit (A/B) (#11516)
* feat(autofix): run one bounded self-review before an address-review commit (A/B) Measured on the takeover fleet (40 PRs, 79 acted rounds, 1369 inline findings blamed at their review heads): after a round pushes, 73% of the next review's new Criticals and 93% of its Suggestions sit on that round's own delta, while Critical density is the same on bot and human deltas (~2 per review, no decay across rounds). So a fresh adversarial pass over the delta before the push has the right scope, and ONE bounded pass, never a loop, is the right shape. Armed by the QWEN_AUTOFIX_SELF_REVIEW repo variable (off|ab|on; ab splits on PR parity), resolved once in prepare and never on a github-hosted runner. An armed round gets a 180m agent budget under a per-arm step cap (190/130), skips the same-run repair chain so the 345-minute job bound still holds, and skips the pass below 150 changed lines or 75 minutes left. The skill runs the review inside the session's own sandbox and records self-review.json after its commit; the gate validates the record, binds it to the pushed commit's tree id and publishes one token string that the round report renders as an autofix-self-review marker. Advisory only: nothing rejects. Design record: qwen-autofix.md#af-156. * ci(autofix): keep the workflow under its size allowance The self-review arm added 4,978 bytes to qwen-autofix.yml, past the 4,096-byte ratchet, with 3,113 of them comment prose that af-156 already carries. Compress those comments to pointers; the mechanism is unchanged. |
||
|
|
ecae037176
|
fix(review): reply carried findings into their thread, resolve fixed ones (#9940)
* fix(review): reply carried findings into their thread, resolve fixed ones (#9906) A carried finding re-posted under its original id used to open a NEW review thread every round — the Create Review API opens a thread per comment, only the replies endpoint joins an existing one — and a finding ruled `fixed` retired from the ledger while its thread stayed open forever. submit now reads the PR's review threads once before posting and matches them by the carried id that already leads every claim line: - a still-standing carried finding is posted as a reply in its original thread instead of a new inline comment; a fresh inline comment stays the fallback for resolved/gone/foreign originals and for (fix-induced) re-reports - each `fixed` ruling carried in the new state.fixedFindings channel replies `R<id> fixed by <what>` into every live own thread under the id and resolves it - a payload that re-reports an id it also rules fixed is refused (inline and bodyCriticals channels), and duplicate rulings dedupe by id The skill docs' "GitHub stacks same-line comments in the original thread" premise is corrected to match. Ledger marker format and counts unchanged. * fix(review): stamp ledger ids onto fresh findings so their threads stay reachable (#9906) The thread lifecycle matched threads by the ledger id leading the root comment, but fresh findings posted id-less roots — ids were minted only into the ledger marker at compose time — so a finding ruled fixed the round after it was reported could never resolve its own thread, and a `still stands` carry opened a second one. Stamp each freshly drafted comment with the id the marker records for it (the claim-line shape Step 6 writes on carries), so a thread is reachable from the round it is born; id-less roots posted before the stamp degrade to the pre-fix behaviour, and the module header says so. Also close the review round's findings: the attribution-off readback leg now strips leading render-nothing residue exactly like presubmit's (the two ends disagreed on one posted shape); reply routing prefers a `(fix-induced)` root over an older unmarked original under the same id; the fixed-vs-re-post contradiction gate scans every id-carrying channel (cannot-tell Criticals, duplicate drops, Critical deferrals, and floor-rerouted comments before their removal); the failed fixed-ruling warning states the true retry guarantee; SKILL.md discloses the 240-char `by` cap and instructs naming a moved location in the claim text. * fix(review): normalize ledger-id stamping so stamped ids survive their own readback (#9940) * fix(review): scan every rendered channel for fixed-vs-re-post contradictions (#9940) * fix(review): close the contradiction gate's remaining channels and pin its guards (#9940) * fix(review): close the constructed-channel escapes and pin the lifecycle read (#9940) Address round-5 review findings on the thread lifecycle: - fetchReviewThreads dedupes by thread id — a stale/echoed cursor re-fetches the same page to the cap, and the resolve leg is not idempotent, so duplicates multiplied a ruling's reply and resolve - the contradiction gate now scans the channels compose CONSTRUCTS: the floor-rerouted deferral entries' path/body and the Budget-gap disclosure lines — both render verbatim and ride no payload state field - the gate's by-scan leg is dropped: every id it could catch is a member of fixedFindings, singly ruled fixed in the same payload, so it could only refuse self-consistent payloads (its own post-dedup-index citation and the FIXED_BY_MAX token fabrication go with it) - among several marked threads the NEWEST leads — the standing claim under an id is the latest re-report's - a draft whose body opens a code fence takes no stamp (text before the backticks would break the fence the gate validated); the skip is disclosed - the thread-lifecycle planning line is phrased as a plan ("to resolve"), not an accomplished fact * fix(review): police the fixed-vs-re-post gate on ledger state, not prose (#9940) Over six review rounds the contradiction gate in `submit` grew a whole-token id scan (`LEDGER_ID_SCAN`) over every free-text channel the body renders — cannot-tell lines, duplicate-drop notes, deferral titles and paths, downgrade reasons, rerouted comment bodies, transcript-derived budget-gap lines, even `modelId` — and round 6 (R6-1) found it refusing whole payloads over text the body never renders and over lines no re-compose can redraft. Every one of those channels is prose: a retired id mentioned there is a cross-reference, not a re-report — the ledger never carries it, the next round rules on nothing under it, and the thread it names is legitimately closed. The gate now refuses exactly what would make the ledger marker and the resolved thread state disagree: a drafted comment whose claim line carries a fixed id and a body Critical leading with one — the two channels the ledger builder carries, read through its own readbacks (`carriedFindingOf` and the new shared `bodyCriticalClaim`) — plus a ruling naming an id this same pass mints. It reads both channels from the payload as the model authored it, captured before the Aone anchor degrade and the floor removal, so a degraded or rerouted re-post still counts and every refusal cites an authored index by construction (R6-3: the reduced-position and fabricated `state.bodyCriticals[k]` citations are gone). `LEDGER_ID_SCAN`, `floorEnforcedEntries` and `budgetGapDisclosures` are deleted; the save-artifact `Omit` names the live-only stamp inputs instead. Also: the open-Criticals re-check now records a `fixed by this diff` ruling in `fixedFindings` whenever the thread root leads with a ledger id — a blocker that left the ledger through `cannot tell` or `superseded` still has its thread, and the posting pass is its only closure (R6-2); the VOLUME line and `postedInline`'s contract say the count covers comments whether they open a thread or reply into one (R1-4); the fixed-ruling reply's `by` takes the strips every posted body takes; the carried-reply stderr line is phrased as a plan; the `by` cap cuts by code point; posting.md states the reply disposition as GitHub-only and re-describes the same-SHA re-run under the reply mechanism. Tests: the prose-channel refusal cells become one `it.each` negative control (ten channels post and resolve), the budget-gap cell is flipped into R6-1's witness, two Aone cells pin authored-index citations past the anchor degrade, and a floor-reroute cell pins the authored index on the GitHub path. * fix(review): read the contradiction gate through the ledger's own projections (#9940) R7-1: the fixed-vs-re-post gate read the payload through weaker projections than buildLedger consumes, so a payload re-reporting an id while ruling it fixed could post through two demonstrated entrances. Stacked severity markers (Aone route): the gate's comment leg stripped ONE marker while the relocate leg iterates to a fixpoint, so a stacked-marker carry re-posting a fixed id passed the gate while the relocated entry carried the id standing. The shared readback strip (markerStrippedBody) now iterates the whole marker run, aligning the gate, the thread matcher, the stamp, the ledger builder and the relocate leg — which drops its now-redundant second strip. This also closes the sibling entrance where the stamp re-minted a stacked carry into a double-id line. Split forged footer span (GitHub route): the bodyCriticals leg read raw authored entries while buildLedger reads the collapsed entries compose's ingestion produces — a forged footer span split across a hard break hid its id from the gate while the builder rejoined it and carried it standing. The leg now reads through tryIngestBodyCriticals, the same acceptance table buildLedger consumes (index-preserving, so refusals still cite the authored index). Four new cells refuse both entrances; a flip cell pins that the hard-break entry alone posts with the ledger carrying its id. Mutation probes confirm each guard is witnessed. * fix(review): shape contradiction refusals as exit-3; read threads through forced colour (#9940) * fix(review): read the stamp's fence guard through residue; share the marker separator with the readback (#9940) * fix(review): stamp drafts whose fence opens past the marker's line (#9940) The round-8 fence guard read through leading residue before testing, and the residue class swallows newlines — so the skip also swallowed drafts whose fence opens on line 2 or later, shapes the stamp provably cannot flip. A bare-marker draft with a balanced fence on line 2 passed every gate, reached stampCarriedId, and went out un-stamped: its thread root carried no id, the marked readback leg read the fence opener, and every later carried re-post matched nothing, posted inline, and opened a new thread — the multiplication this PR exists to kill. Skip the stamp only when the fence shares the marker's projected first line: refuse only if no bare newline was consumed outside HTML comments (newlines inside comments stay render-invisible, so a comment-led same-line fence keeps the skip). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(review): gate deferred-Critical re-posts and share the separator readback (#9940) The fixed-vs-re-post gate never scanned the deferral channel, but a deferred Critical does not stay deferred: the split relocates it into the body Criticals, and the relocation prefix strips the carried id from position 0, so the ledger carries the claim renumbered. One pass could reply `fixed` into a thread while the same review re-asserted the claim as an open blocker under a minted id. The gate now reads Critical deferral titles the way the closure mint already reads them, refusing the contradiction at its authored index; Suggestion deferrals keep the prose cross-reference carve-out. The readback's separator step also drifted from the post-time strip: a multi-line comment between the separator colon and the carried id truncated the claim line to `<!--` while the attribution-off post exposed the id, so the draft re-minted a fresh id and the original thread was orphaned. markerStrippedBody now shares MARKER_SEPARATOR_RE with stripSeverityPrefix, so both marker-strip fixpoints agree on the stacked-marker shape and the post-colon residue shape alike. The cross-reference cell that pinned the Critical-deferral post-through becomes a refusal cell with a posting flip half. * Merge remote-tracking branch 'origin/main' into fix/review-thread-lifecycle * fix(review): CR/HTML-block stamp skips and full-width-colon id readback (#9940) * fix(review): unify carried-id readback, quote-aware stamp skip (#9940) * fix(review): address rounds 14-17 review findings on the thread lifecycle Six code fixes, each with its pinning test, plus the test hardening the round-14 probe findings asked for: - inline-counts: make the residue token's comment alternative admit exactly one parse, so MARKER_SEPARATOR_RE stays linear — the lazy form gave a residue run 2^N decompositions and stripSeverityPrefix hung on residue-led colon-less drafts (~30 comments = minutes) on every GitHub submit and attribution-off post (R14-1) - submit/compose-review: surface the floor reroute's deferral entries from compose and scan their Critical records in the contradiction gate — a rerouted blocker whose carried id sat below the claim line escaped the fixed-vs-re-post refusal and one pass both resolved the thread and carried the claim forward (R14-2) - submit/compose-review: read deferral titles through the head-slot tokeniser's own id read (readClaimHead(...).id) in the gate AND the closure mint — the anchored read over .stripped kept a source tag ([probe]) at position 0 and read no id there (R15-1) - compose-review: bound fixedFindings ids by LEDGER_MAX_ID and the round range like isLedgerFinding, and refuse a `by` that renders as nothing — an unbounded id rode disclosures verbatim, an invisible `by` posted `R<id> fixed by` with no account of what fixed it (R14-3, R14-4) - presubmit: gate the footer recognition disjunct on top-level comments at both sites — the lifecycle's own fixed-ruling reply carries the footer and inherits its thread's commit_id, so ungated it bucketed into overlap and the drop rule discarded a genuinely new finding at the location (R16-1) - thread-lifecycle: strip same-line render-nothing residue after the blockquote unquote in stampCarriedId's skip — a quoted opener led by an HTML comment (`> <!-- x -->` + fence) was stamped and the insertion flipped the quoted structure (R17-2) - submit.test: reset the spies and exitCode per expectRefusal CALL so the loop-converted gate tests keep isolating their vectors (R17-1), and pin the round-14 probe holes: the marked-tier age-independence, the interleaved real-gh CLICOLOR shape, the rootCreatedAt fetch mapping, the floor-alignment posted marker, the attribution-on reply footer, the stampedFresh no-thread-read short-circuit, and the diverted-finding-rides-the-marker property (R14-5..R14-11) * fix(review): close the second residue-token ambiguity in the marker separator `\s` and `\p{Cf}` both match U+FEFF — the only codepoint in both, verified by enumerating the whole Unicode range — so as two alternatives of INVISIBLE_RESIDUE a FEFF run had 2^N decompositions and the failed `[::]` made the engine explore them all. Round 14 closed this shape for the comment token; the character half stayed open, and the bounded-time cell it added exercises comment runs only, so the suite stayed green: N=24 measured 318ms and doubles per character. The two character alternatives become ONE class, so a character matches the token exactly one way. The accepted codepoint set is unchanged (a probe compares the separator, the leading strip and the global sweep across the residue shapes the tests pin, byte-identical), and FEFF stays residue in all three consumers. The new cell feeds a 64-character FEFF run to stripSeverityPrefix under the same bound as its round-14 sibling, and re-checks that leading FEFF still classifies, still separates a carried id, and keeps the two marker-strip fixpoints agreeing. Mutation-checked: splitting the class back into alternatives takes the cell to 150s (red). (#9940 review R18-1) * fix(review): align the carried-id readers and close the by-gate's exit gaps Round 20/21 findings on the thread lifecycle, each with its pinning cell (all six mutation-checked: reverting the fix reddens its cell): - presubmit: extractCarriedIds reads the marked leg through ledgerClaimLine (forged footer spans, comment-marker lines and leading residue stripped) and the id through readClaimHead(...).id — the same projection buildLedger, the contradiction gate and the thread matcher apply. A span-led or source-tag-led re-post ([probe] R1-2: …) read id-less here while the ledger kept carrying it, so the standing re-post was dedup-dropped every round (R21-3 and its sibling) - submit: the contradiction gate's deferral and floor-reroute legs scan EVERY entry, not only Criticals — the closure mint reads an id from every entry of the same two channels, and a deferred Suggestion is still a finding channel the body publishes as standing; a Critical-only gate let one pass resolve R2-3 as fixed while deferring it (R21-2). The cross-reference table's deferral-title row moves to a refusal cell; an id-less deferral title still posts - compose-review: the `by` gate projects through rendersAsNothingAtExit (submit strips the reply through the attribution-off chain, so a bare marker rendered as prose here and as nothing there), and the gate runs again on the capped value — a visible tail past FIXED_BY_MAX sliced to invisible-only; that cut is the tool's, so it degrades to a by-less ruling instead of refusing - compose-review.test: pin the closure mint's source-tag-led re-post read on the deferral channel (R20-1) * fix(review): drop the inline-Suggestions clause the diversion empties The opener's "Suggestions are inline." is keyed to the count compose takes from the PRE-diversion posting set, and the thread lifecycle's diversion runs after the compose and after the consistency gate — so a round whose only inline findings are carried re-posts into live own threads (the steady state of a long-lived review) posted the clause over an empty comments array while the JSON honestly reported inlineComments: 0. submit reconciles the clause after the diversion: when the pre-diversion set had inline Suggestions and the post-diversion set has none, the clause is stripped from the body in both languages, with the space that joined it to its neighbour. The event is untouched — it is keyed to the confirmed count, carried findings included, and a diverted finding still stands. The clause text is named once (INLINE_SUGGESTIONS_CLAUSE) so the writing end and the stripping end cannot drift. Pinned by a lifecycle cell (one carried Suggestion, comments [] + one reply, clause absent, event COMMENT) and a control on the fresh-beside-carried cell; removing the reconciliation reddens the new cell. (#9940 review R23-1) * fix(review): count the marked set for the clause guard, let carry-replies carry the re-post exemption Round-24 findings, both mutation-checked (reverting each fix reddens its cell): - submit: the round-23 reconciliation counted `finalComments` and `reviewComments`, whose visible severity markers the attribution-off rewrite had already stripped, so under attribution off both counts were zero and the guard never fired — the clause posted over an empty comments array on exactly those runs. The guard now counts the MARKED post-floor posting set (`payload.comments`, a 1:1 source of `finalComments`, so the diverted indices address both); attribution on is unchanged. Pinned by an attribution-off twin of the round-23 cell and a keep-the-clause control (R23-1, fix-induced) - presubmit: own-account replies carrying a posting signal reach the classifier as re-post EXEMPTION carriers only. The thread lifecycle answers a carry inside its root thread, which makes the root replied-to (`resolved`, never a target) while the round-16 exclusion kept the reply — the only comment still carrying the id — out of the set; with another own root overlapping the location the carried finding had no carrier and the drop rule discarded its re-post as a duplicate every round. A reply adds a `repost` entry keyed on the wanted ids it carries and nothing else: not the overlap bucket, not the ambiguity count, not the id-less fallback; same account/SHA/id gates as the root match, both platform paths, fixed-ruling replies inert. Pinned by the three-comment steady-state cell (both posted shapes) and a negative cell (foreign account, stale SHA, fixed reply, unwanted id all carry nothing); the round-16 pin stays green (R24-1) - posting.md: the report-shape note no longer claims every repost entry is also an overlap, and the overlap bullet names the carry-reply as the carrier * fix(review): let carry-replies carry across commits, refuse non-canonical ruling ids, share the fence-opener rule Round-25 findings, each mutation-checked (reverting the fix reddens its cell): - presubmit: the round-24 reply-carrier leg was gated on the current SHA, but a reply inherits its ROOT's commit id — after any new commit the carry-reply failed the gate forever while its root bucketed `stale`, and a still-standing carried finding was dedup-dropped whenever a current-SHA own comment overlapped its location. The SHA gate is dropped from that leg only; the account gate and the wanted-id filter remain (the wanted-id match is the evidence the current round still carries the finding), fixed-ruling replies stay inert. The stale-SHA expectation moves out of the negative cell into its own positive cell (R24-1, fix-induced) - compose-review: ingestFixedFindings also requires the canonical spelling `R[1-9]\d*-[1-9]\d*` — the shape tolerated leading zeros (`R01-2`) that no minted entry or posted root carries, and every downstream join is raw-string equality, so such a ruling passed every gate, retired nothing, and slipped past the two-way refusal beside a standing `R1-2` re-post. Tightened in the ingest gate, not the shared token; the mint numbers from 1, so no real id is refused - review-footer/thread-lifecycle: the fence-opener rule is one exported predicate (`fenceOpener`) that both `scanLines` and the stamp skip apply — a backtick run whose info string carries a backtick opens no fence (CommonMark), so the delimiter-only skip over-stamped nothing and posted an id-less root behind a disclosure naming a fence that did not exist; tilde fences and plain-info backtick fences keep their skip. Under the mutation `scanLines`' own pin reddens with the new cell, which is the point of sharing it * fix(review): skip the stamp on every line-leading construct, keep a fresh claim's (fix-induced) prose out of the marking Round-26 findings, each arm mutation-checked (reverting it reddens its cell): - thread-lifecycle: the stamp's first-line skip guarded only fences and type-1/6 HTML blocks, so a blockquote, ATX heading, list item, thematic break or type-3/4/5 raw-HTML opener on the marker's projected first line was stamped and demoted to paragraph text by the attribution-off post — silently, in a structure the gate never validated. The skip now covers those too (the blockquote tested BEFORE the unquote — the quote itself is the construct; the others through a stamp-local OTHER_LEADER_RE, HTML_BLOCK_OPEN_RE left to the line model), and one line-2 arm: a block that cannot interrupt a paragraph (indented code, an ordered list not starting at 1) directly under the marker, with no blank line between, was a block of its own behind the empty attribution-off first line and became continuation text of the `R<n>-<k>:` paragraph the stamp writes above it. Fences, bullets and `1.` on line 2 keep stamping; a blank line in between keeps stamping. The stderr disclosure and its comments name the widened set (R26-1) - thread-lifecycle/inline-counts: a FRESH claim starting with the `(fix-induced)` prose token read as prose on the draft (no id for a marking to hang on) but as a genuine marking once spliced behind the minted id — permanently, so a later still-standing carry paired with the mislabelled root ahead of the true original. The stamp now lets the readback arbitrate: while carriedFindingOf reads the stamped body as marked, the first head-slot occurrence on the claim line is removed (tags cannot contain it; residue ahead of it stops the read). Genuine carries return verbatim before this. The token is stated once (FIX_INDUCED_TOKEN_RE) and the anchored readback is derived from it — source and flags identical to the former literal (R26-2) * fix(review): make the carried-id stamp, both readback legs and the fixed-ruling gates decide once, and close the round-27 join and construct holes Five reverse-audit rounds over the thread-lifecycle stamp and its readback, plus the round-27 review findings (R26-3, R27-1). Every fix carries a cell, and every new arm was mutation-checked (removing it reds a cell). Readback / stamp (lib/inline-counts.ts, lib/thread-lifecycle.ts): - Ledger ids are canonical at every join: `readClaimHead` returns `canonicalLedgerId`, `planThreadActions` joins carried and fixed ids canonically, the contradiction gate compares canonical spellings, and a non-canonical `fixed` id is refused naming the canonical one (R26-3). - One line model for the stamp and both projections: `severityOf` reads no marker off an indented code line; `separatorStrip` folds a soft break and its continuation indentation (a lazy continuation), keeps an indented code block only behind a block boundary (`blockBoundaryIn`: a blank line or an HTML-comment line up to three columns in), finds the separator colon with comments masked and never on a code line, and leaves a format character, NBSP or comment leading the content line where it stands. - Indentation counts columns (`indentColumns`, tab to the next multiple of four) everywhere the strip, the readback and the line model test it. - The stamp skips the constructs that cannot interrupt the id paragraph or would re-shape it (setext underline, empty list item, ordered list not at 1, link reference definition incl. next-line destination and escaped labels, type-7 lone tag with quoted attributes) and re-attaches the break run for the ones that open a block (blank line, table header with or without a leading `|`, block-level tag, a marker line that is itself an HTML block); indentation alone never refuses (R27-1). - `(fix-induced)` on a fresh claim is arbitrated on both projections (a footer span split across a soft break reaches the head slot only on the attribution-off exit), and a model-carried id reads `fixInduced` from either leg; the strip loop is one pass per token run. - Linear time: `residueLineBreaks` is a merge walk, `stripSeverityPrefix` tests emptiness once at the fixpoint, the last marker is found with comments masked, `LONE_TAG_RE` / `TABLE_DELIMITER_ROW_RE` / the HTML opener are unambiguous. Line model (lib/review-footer.ts): - Type-1 and type-6 HTML block start conditions end at the tag name (`<div class="x">foo` opens a block); a type-1 block whose closing tag sits on the opener line ends there; `<pre-x>` is a type-7 tag; four columns is code before it is an opener or a fence; a single-line footer-span strip keeps the line's indentation. Compose / submit / presubmit: - `by` gates read both projections, unclosed comments are detected by a forward scan (`<!-->`/`<!--->` are empty comments), the cap's retreats run only on a cut, and raw tag openers go inert after the gates; `fixedRulingLine` is the last belt before the reply write. - The body without the inline-Suggestions clause is rendered ONCE beside the body (a same-length placeholder run replaces the clause, so every budget decision is shared) and `submit` posts it when the thread diversion drains every inline Suggestion — no text search over model prose. - Downgrade reasons are quoted like every other model line, capped per entry (400) and as a list (2000 code points, disclosed in `remediation`). - `bodyCriticalClaim` reads no id off an indented code line; presubmit's `matchedIds` reports every spelling the findings file wrote. Docs: posting.md notes the downgrade-reason cap. * test(review): loosen the linear-time stamp cell for loaded CI runners The 150 ms bound tripped at 194-534 ms on the self-hosted runner under load 50 while the quadratic forms it guards against take seconds; the cell now takes the best of two runs against a 2 s bound. * fix(review): let the parser arbitrate the id stamp, and close the round-28 readback, entry-channel, reply-carrier and census holes Round 28 of the review (R27-1 still standing, R28-1..R28-8), each fix pinned by a cell and mutation-checked. Stamp (lib/thread-lifecycle.ts, lib/review-footer.ts): the hand-listed CommonMark constructs are gone. `stampCarriedId` builds its candidates — the canonical one-line shape, then the shape keeping the separator's line-break run — and accepts the first whose top-level block skeleton (markdown-it, block-only, render-nothing HTML blocks dropped) is the same before and after the insertion on BOTH projections; otherwise the documented id-less degradation. Closes R27-1 (A) a `>` inside a quoted attribute and (B) the incomplete-tag over-skip, and every construct class by construction. Readback (lib/inline-counts.ts): the block-boundary state resets on a paragraph line; the separator colon is content on or after a code block the residue opened, with the marker line's HTML-block-ness reaching that search; `markerStrippedBody` keeps residue between iterations so the post and readback fixpoints agree (R28-1, R28-6). `stripFooterSpans` routes a bare-CR body to the line-aware path (R28-7). Entry channel (compose-review.ts): `bodyCriticalClaim` and the ledger's body leg read through one head — the one-line channel has no indented code — `collapseEntry` trims one-line entries, and a code-led comment the floor reroutes records a `(quoted code)` title no reader takes an id from (R28-2). `escapeTagOpeners` pairs backtick runs the CommonMark way, so an unpaired run is literal and the tags inside it go inert (R28-5). The stop re-rule's grant keys are canonical ledger ids (R28-8). Presubmit: the own-reply re-post carrier joins by id at any location — GitHub reports `line: null` on an unmapped anchor (R28-3). Fixed-ruling reply: carries `<!-- qwen-review-fixed-ruling -->`, never the posted comment-marker shape; .github/workflows/qwen-autofix.yml skips review-bot comments carrying it in the inline census and the three digest legs, with a scripts/tests case executing the extracted jq (R28-4). * fix(review): seed the post-colon strip from the colon's line, join the repost exemption on the id, define the fixed-ruling filter once - separatorStrip seeds the post-colon walk from the colon's own line: the marker line's HTML-block state applies only while the colon sits on it; a colon on a later line is a paragraph line (or a comment-led HTML block that ends on that line), so an indented claim under it is a lazy continuation the carry must read, not code (round 29, R29-2). The walk itself (scanResidue, behind codeBlockStartIn and markerLineOpensHtmlBlock) now runs over PHYSICAL lines with HTML-block state — a comment-led line opens a block that runs to the first line containing `-->`, and a comment that opens later on a line hides no break — where the comment- span view misread `<!-- a --><!-- b\n-->:` and `<!-- a -->:<!-- b\nc -->` colon lines as one HTML-block line and kept the claim under them as code. The walk also tracks the code block it opens: codeIndentedAfter (the marker-line guard behind severityOf) now asks whether the marker's OWN line is code — inside a block that runs on to it, not after a code line it ends nor as a lazy continuation under a format-character line — and bareClaimLine reads no claim once any line of its lead is code, so the bare and marked readback legs agree; blank lines are spaces and tabs only, an indented NBSP line is code; and "same line" in the no-colon strip is physical — a comment glued to the marker that spans a break goes with the run instead of leading the attribution-off post as an HTML block (fuzz audit vs markdown-it, 940k bodies). - posting.md: the overlap drop exempts a finding whose id appears in matchedIds of ANY repost entry — a root leg matches at the finding's (path, line), a carry-reply leg on the id alone at the reply's own location, which may be unmapped or a line the finding moved off; pinned in SKILL.test.ts and presubmit.test.ts (R29-1, R29-3). - qwen-autofix.yml: FIXED_RULING_FILTER is one workflow-level env value, spelled with a single backslash and handed to all four jq sites as --arg frf; the scan step's shell copy spelled [^\\n], which as an --arg value compiled to "neither backslash nor n" and let a ruling note whose `by` clause contains an `n` count as feedback (R28-4 residue). * fix(review): close four Criticals the Critical-only pass found, and harden the channels around them A full Critical-only pass over the PR's new surface (nobody had completed one — four consecutive review runs timed out) found four Criticals, each reproduced before it was fixed and pinned by a cell that fails without the fix. Two reverse-audit rounds then broke the first cut of three of these fixes; what lands here is the re-worked version. - submit: the contradiction gate read the drafted body while the thread diversion read the attribution-off post, so a payload that re-posted an id it also ruled fixed slipped the refusal and then replied "still stands" AND "fixed by" into one thread and resolved it. The gate now reads the union of both projections — a superset of what the diversion acts on, which is the safe direction; the diversion reads the drafted body, the projection the ledger builder and stampCarriedId also read. - compose-review: downgrade reasons were escaped per reason but render as one paragraph, and backtick runs pair across the `; ` join, so one reason could free the next one's `<details>` and fold every later paragraph away. Escaped once over the join, and the 2000-point total is charged against the string that actually posts. A reason too big for the budget no longer evicts the shorter ones after it. - compose-review: model-written blockers reached the body with raw HTML openers intact — one ordinary sentence naming a tag without backticks hid every later blocker, the disclosures and the footer. Escaped, along with the cannot-tell list, the duplicate-drop list and the "Not reviewed:" disclosure paragraphs: a list item bounds `<details>` at its `</li>` but not a RAWTEXT element (`<textarea>`, `<style>`, `<script>`), which swallowed the footer from all three. - pr-context: this PR moved a still-standing carry from a new root to a reply, but "Blockers to re-check" tested root bodies only, so a Critical carried into a Suggestion-rooted thread left the mandatory section for a 240-char snippet under "do NOT re-report". Promotion now reads the whole thread (from any author — widening it can only add a thread), the standing claim is this account's own newest re-post, and the section renders the root in full ALWAYS with the re-post beside it, bounded so one long thread cannot starve later blockers. A `fixed` ruling note is neither: its `by` clause routinely carries blocker prose. Hardening the same audits asked for: planThreadActions refuses to match on an empty login, rules each thread once, never resolves a thread it is replying a carry into, and discloses a ruling that resolved nothing; fetchReviewThreads names itself when the read comes back empty; the fail-closed identity guard reads replies too, since the lifecycle carries Criticals there (renamed accordingly); a markdown link opening a claim is no longer read as an axis tag; and SKILL.md/posting.md drop three statements the code does not implement. * fix(review): close the three Criticals of review round 30, and the holes the reverse audit found in the first cut - escapeTagOpeners paired backtick runs across blank lines, inventing a code span CommonMark never forms and leaving a tag opener inside it live. Reachable through the `Not reviewed:` disclosures, the one escaped channel whose entries are verbatim model prose that nothing folds to a single line. Pairing is now block-scoped — and a FENCED block is a block, not a span: its blank lines do not end it, its `<` is already inert, and the first cut escaped there, rendering a literal `<` to the reader for no safety gain. Fences are masked in one line-wise pass; a lone CR ends a line too; both lookups are cursor-based, so the scan stays linear (measured 14-22 ms on 100k-character adversarial inputs). - isRuling matched FIXED_RULING_MARKER as a substring, so a genuine Critical that merely QUOTES the marker — a review of the file that defines it — was demoted out of the mandatory "Blockers to re-check" section, and the two halves of this diff disagreed on one input (the workflow census already matched an anchored shape). Both now use one shape, exported as FIXED_RULING_SHAPE_RE and anchored at BOTH ends of the note's line: `^` alone is only worth anything while the severity marker leads the body, and under `attribution: false` the post strips it, so a Critical whose claim line opened by quoting a ruling still demoted itself. A real note ends at the marker; a quotation carries on past it. The census filter gets the same end anchor, and a test pins the two spellings against one corpus. - stripSeverityPrefix's `!kept` exemption was over-broad: a marker, a blank line and an indented marker kept the second one, so submit's renders-as-nothing refusal was disarmed and the post carried a comment whose only content is a bare machine marker rendered as code. The original test still runs for an unkept remainder; a kept one is now tested against a narrower projection — markers, the separator grammar they trail (either colon) and whitespace, never comments — so a quoted ` <!-- qwen-review -->` line still reads as the code audit 6 kept. Every fix is pinned by the acceptance criterion its finding named, plus the arms the reverse audit reported unpinned: the fence mask and its interior runs, the blank-line rule, the CR line ending, both anchors of the ruling shape and its `by` clause, the separator and format-character halves of the marker projection, and MARKERS_RE's global flag. * fix(review): escape tag openers against the renderer GitHub runs, and anchor a ruling note to the whole comment The escape that keeps a model-written entry from posting a live HTML element was derived from the CommonMark prose. GitHub does not render with the prose: it renders with cmark-gfm and then hands the raw passthrough to an HTML parser, and all three disagree about which constructs FORM, how far each one HIDES, and which backtick runs PAIR. Every rule here is now cmark-gfm's own, measured against it. A/B over 40000 fuzz lines, each escaped and embedded in a realistic body, rendered by cmark-gfm and parsed by parse5: before after leaked a raw element 847 0 live <details>/<summary>/<select> 31 0 second blocker or footer folded away 29 0 0 across 160000 lines on four independent seeds. `<details>` is the canary because GFM's tagfilter — which GitHub runs — turns `<script>`, `<style>`, `<textarea>`, `<iframe>` and their siblings into visible literal text. They cannot swallow anything; `<details>`, `<summary>` and `<select>` can, and did. - escapeTagOpeners derived CommonMark BLOCK structure by hand so it could decide which backtick runs may pair. Every round patched one more block start into that model and the next round found another: blank lines, then fences, then indented code, then list items, ATX headings, a fence interrupting a paragraph. The model is deleted. Every channel that reaches the escape folds to a single line first — `ingestEntryList` for the entry channels, the `\s+` normalisation for downgrade reasons, `collapseEntry` for the `Not reviewed:` disclosures (it folded only `\n`; it now folds `\r` and `\r\n` too), `scriptLintGate`'s own push, and compose's refusal of a line break in a ruling note's `by` — so there is no block structure to model and none is modelled. `escapeScopes`, `BLOCK_PARSER_NO_HTML` and `HTML_BLOCK_START_RE` are gone. - Per line the escape is ONE left-to-right pass, which IS the leftmost-first rule: code spans, autolinks, raw HTML, GFM autolink literals and link destinations all compete in it, and the construct that starts earlier consumes the others. Each rule is cmark-gfm's, not an approximation, because BOTH directions of a wrong answer leak: believing in a construct the renderer does not form steals a backtick from the delimiter pool and shifts every later pairing. * A CDATA section is `"<![CDATA[" ([^\]] | "]" [^\]] | "]]" [^>])* "]]>"`, not "is there a `]]>` later" — an array index inside the fixture puts a third `]` in front of the terminator and the section does not form. What it HIDES ends at the first `>`, because the raw span reaches an HTML parser that closes a bogus comment there; the rest of the span is RAW TEXT, where no backtick is a delimiter. * A comment needs its `-->` and refuses a text that ends with `-`, and the HTML parser closes it at the first `-->` or `--!>` substring, with none of cmark's arithmetic. Only a failed COMMENT poisons the `<!` family; poisoning on a failed declaration refused a comment cmark still formed. * A declaration is `<!` plus UPPERCASE letters plus WHITESPACE, so `<!ENTITY<details>` and `<!entity <details>` form nothing and the tag behind them is live. Autolinks are tried first, as cmark does: `<!--@t>` is a mailto autolink and no comment forms. * A GFM autolink literal covers `ftp://` and every case of the three schemes, is refused without cmark's domain (first character alphanumeric, no `_` in either of the last two labels) and behind an unmatched `[`, and its boundary is read against the line's CONTENT — `>www.z.test/p` links because the block phase strips the block-quote marker first. It runs past a backtick and past a backslash, so the code span this pass believed in never formed. * Code spans are cmark's scanner, not the spec's: it carries a memo a successful scan overwrites, so the spans it forms are a strict SUBSET of the spec's, and it refuses to open one on a run longer than 80. - A `<!` at the start of a line's CONTENT opens an HTML BLOCK (types 2, 4 and 5), which is passed through raw — the inline phase never runs inside one, so the code span and the backslash escape this pass reads to decide an opener is inert do not exist there. That `<` is escaped and the rest of the line rescanned. Reachable from a body Critical under `review.attribution: false`, which is a top-level line, and from the two list channels. - The scan is linear: every terminator is located once per line into a sorted array read through a monotone cursor. 800000 backtick runs went from 7.6 s to 0.18 s; the unterminated-opener shapes went from four minutes to 23 ms. - rendersAsNothing listed the entity references that render nothing, and the list named ten of the thirty-three HTML5 names and twelve of the numeric spellings that do — so `**[Critical]** 	` passed the gate and posted a blocker with no visible content, counting toward the verdict and re-promoting as unanswerable. The list is replaced by a decision: decode the reference and ask whether what it decodes to is invisible. The old arm also matched case-INSENSITIVELY, and named references are case-sensitive — `&NBSP;` is five characters a reader sees. - The same projection's `<!` arms ran to `]]>` and to the next `-->`, took the four openers as four independent global passes in the wrong order, read a declaration as `<!` plus any letter, and deleted `<script>` and `<style>` spans that GitHub renders as visible text. Every one of those threw the whole compose away over a body that renders perfectly well — a security Critical that pastes its payload is exactly the shape that hit it. The family is now ONE left-to-right scan, which is also what makes it linear: an alternation with no end-of-input arm retries at every later `<`, and 720 kB of unterminated `<![CDATA[` took nineteen seconds in a projection that runs twice per entry. - floorEnforcedReroute decided its `(quoted code)` guard on the first PHYSICAL line while `collapseToLine` builds the title from the first non-trim-empty one. A body led by an NBSP, a BOM, an ideographic space or a form feed got no prefix, and the NEXT line's ledger id led the title — an id the claim-line read cannot see, which the fixed-vs-re-post contradiction gate then refuses the whole post over. - Both ruling-shape matchers decided "is this a ruling note" from a first LINE. A comment that quotes a whole ruling and then states its own finding underneath — the natural way to say "this was ruled fixed last round and is not" — was classified as a note and dropped from the census wholesale, the live Critical with it, at all four jq sites and in pr-context's blocker section. Both now anchor over the WHOLE body and admit only the canonical attribution footer after the marker. The two engines are kept in agreement by construction: no `\s` (Oniguruma matches U+0085 and JS does not; JS matches U+FEFF and Oniguruma does not), no `.` where the dialects disagree on line breaks, and no two whitespace runs that can split a run of spaces two ways — that last one is quadratic, and real jq aborts the census with `retry-limit-in-match` rather than returning false. - The writer and the readers of a ruling note now agree on one set of line breaks. `isFooterSafeModelId` admitted U+2028 and U+2029, and `reviewFooter` took its fallback version unchecked, so a modelId or a `CLI_VERSION` carrying either built a CANONICAL note that neither the shape matcher nor the census filter recognises — the note is re-posted every round and the loop never converges. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
005fc97b33
|
fix(desktop): unblock macOS and Linux desktop packaging (#11518)
* fix(build): stop bundling the OpenTUI library for the other libc The OpenTUI asset copier ships the native render library from every installed `@opentui/core-*` platform package. On Linux that is two packages, not one: the glibc and musl builds differ only by the `libc` field, so a glibc host installs both and the bundle carries both ELF libraries. That breaks AppImage packaging. linuxdeploy walks every ELF file in the AppDir to deploy its dependencies, and the musl library needs `libc.so`, which no glibc system resolves; `ldd` exits non-zero and linuxdeploy aborts with nothing but `failed to run linuxdeploy` reaching the log. Desktop release 0.2.3-preview.0 failed this way on Linux, because the desktop app copies this dist tree into its bundled runtime verbatim. Skip the Linux package built for the libc this host does not use. The remaining platforms are untouched: their libraries are not ELF, so the Linux bundler ignores them and the completeness gate keeps working. It also drops roughly 27 MB from every Linux bundle. * fix(desktop): sign every Mach-O binary in the bundled runtime The macOS signing step walks a list: the ripgrep vendor binaries and the Node.js runtime. That list covered everything the bundled runtime carried when it was written, but the runtime is a copy of the CLI's dist tree, so its native payload changes whenever a CLI dependency does — with nothing in this package changing. A renderer library arrived that way and shipped unsigned. Release 0.2.3-preview.0 built for twenty minutes on both macOS targets and then lost the archive to the notary service: "The binary is not signed with a valid Developer ID certificate." Enumerate the Mach-O files under the staged runtime and sign all of them, keeping the JIT entitlement on the Node.js binary alone. Verify the signatures in the same step so a gap fails the build in seconds instead of surfacing from the notary service after the build. |
||
|
|
89da433888
|
feat: delegate a subagent turn to an external agent over ACP (Claude Code first) (#11003)
* feat: delegate a subagent turn to an external agent over ACP
A subagent definition can now declare an `executor` block naming an external
agent process. The turn runs there over ACP and is re-published as the same
AgentEventEmitter events the in-process path emits, so the JSONL transcript
writer, SubAgentTracker's nested-permission bridge, virtual subagent sessions
and the Web Shell subagent panel all work unchanged. Claude Code is the first
agent wired up.
Delegating rather than exposing a foreign agent as a peer session backend keeps
the parent Qwen session authoritative: no re-keying of workspace identity from
cwd to (cwd, backend), and no change to the bridge's one-channel-per-runtime
invariants. Agent definition files already mirror Claude Code's
`.claude/agents/*.md` schema verbatim, so this extends that compatibility from
the definition layer to the execution layer.
Core side: a `SubagentExecutor` interface narrowed to the members production
callers actually use, which `AgentHeadless` now implements so drift fails at
compile time; `createAgentHeadless` returns the interface and dispatches on
`config.executor`; `AgentCore.buildChatSystemPrompt` is extracted as
`renderSubagentSystemPrompt` (a verbatim move, the private method delegates) so
an executor that never builds an AgentCore produces a byte-identical prompt;
`dispose?()` is composed into the returned dispose so the external process
cannot outlive its subagent; and the host factory is injected through
`Config.setExternalAgentExecutor`, following the existing
`setSessionWorkflowEnabledProvider` inversion, so core gains no ACP dependency.
The types a host needs are exposed on a narrow `./subagentRuntime` subpath
rather than the package root. Because the key (`subagentRuntime`) does not mirror
its source stem (`subagent-runtime.ts`), the wildcard cannot resolve it, so every
resolution map that fronts core's exports carries an explicit entry: the cli
`tsconfig` paths and vitest alias, `integration-tests/tsconfig.json` paths, and
the skill-review-harness loader's named map. A cross-package contract test pins
the export entry, the barrel's re-exports and the cli resolutions; the repo's own
`integration-tsconfig-core-paths-sync` and `text-capture-core-loader-sync` guards
pin the other two, so a future named export that forgets a mirror fails CI rather
than silently falling through to a stale `dist`.
Everything that could silently substitute a different agent, or grant authority
nobody approved, fails loud instead. Three bypass paths are closed. Frontmatter
rejects a malformed `executor` block outright rather than dropping it — dropping
it leaves `config.executor` undefined, so no downstream check engages and the
turn runs in-process under Qwen's model with nothing on stdout, stderr or the
debug log — and the block is re-validated at the consumption point, because
session-level subagents are injected as plain objects and bypass frontmatter
parsing entirely, and at serialization, so a save/reload round trip cannot drop
the backend. An executor-bearing file whose frontmatter YAML does not parse is
rejected too, but only such a file: the validation reads the original document
node and refuses when `parseDocument` reports an error AND the file claims a
top-level `executor` — by AST key, by the sanitized frontmatter value, or by a
raw-text probe anchored to column 0 with optional quotes, so a quoted
`"executor":` is caught (the lenient parser keeps the quotes in the key and a
repaired `parseDocument` nests it, so both miss it) while an indented `executor:`
under `mcpServers:` or inside a `description: |` block scalar is not (it declares
nothing and must not hard-refuse an unrelated definition). The error must also be
able to reach the executor. The guard refuses when `parseDocument` — the real
YAML parser — lost a text-claimed executor key (`!hasExecutor`): YAML errors are
not line-local, so a compact-mapping error on an earlier line drops the whole
remainder, and the lenient `parseSimple` fallback must not be trusted to rebuild
it — it is a line-based heuristic that mangles block scalars and sequences,
turning `command: |` plus an indented `npx` into `command: '|'` and so spawning
an executable literally named `|` that the file never declared. It also refuses
when `parseDocument` kept the node but an error sits at or after the executor's
line, where a repair could rewrite it into a different command/args. Only an
error strictly before the executor line that `parseDocument` survived
(`hasExecutor` still true) leaves the subtree byte-faithful, so a valid
external-agent definition — e.g. one carrying a duplicate `name:` from a bad
merge — keeps loading instead of vanishing from `/agents` over an unrelated
quirk. And because `parseDocument` tolerates an unresolved YAML alias
(`command: *undef`) with an *empty* `document.errors` while `toJS()` throws when
it resolves the node, the `toJS()` read is wrapped so that case is refused as an
invalid executor block rather than escaping parseSubagentContent as a raw parse
error the errors-based guard cannot see. For a real
executor the lenient parser would otherwise repair a malformed document into a
node that dispatches a different command/args than the file declares, or drops
the key and runs in-process. Every other definition keeps loading through the shared
parser's lenient `parseSimple` fallback — a description containing a colon, a
tab-indented field, or a duplicate key from a bad merge must not silently vanish
from `/agents`. The refusal message carries `invalid executor block` so
`warnInvalidSubagentFile` surfaces it on stderr instead of the debug log.
`convertToRuntimeConfig` rejects executor definitions,
which is the path team and background spawning use. Workflow `agent()` rejects
them before spawning, because token budgets, schema output and workflow tool
restrictions cannot be enforced across the process boundary — that covers bounded
and unbounded workflows alike. Cold resume is denied for a transcript carrying
`executor: 'acp'` or a legacy `external-acp:*` model label; the label is
deny-only and never selects an executor. With no factory registered, `create()`
throws before warming providers or resolving a model, and an ordinary Error from
an external factory propagates unwrapped instead of being relabelled as an
AgentHeadless construction failure. This deliberately diverges from the lenient
drop used for mcpServers and hooks: losing those degrades a capability, losing
this one substitutes a different agent.
The permission mode is the host's effective, already-clamped approval policy —
not the definition's raw request and never the external agent's own config. The
Agent tool resolves the definition's `approvalMode` against the parent session's
mode and folder trust and stamps the result onto the runtime context, and the
manager reads that resolved value back when it dispatches, so a definition
cannot escalate the peer past the parent session's limit. The executor requires
the resolved mode to be present in `session.modes.availableModes` and applies it
with `session/set_mode` before the first prompt, because the agent's own
`defaultMode` was measured to let a Write execute with no permission request
emitted at all. When the agent offers no option matching the user's outcome the
executor denies rather than falling back to the first offered option — approving
"proceed once" against `[allow_always, reject_once]` must not answer
`allow_always`. `optionKindForOutcome` is exhaustive over the enum with a `never`
check and a runtime default that denies, so an unmapped outcome cannot become a
grant. A host-policy denial (headless, permission avoidance, a display-only
TOOL_WAITING_APPROVAL listener, or a routed interactive question) rejects the
TOOL by selecting a `reject_once` option rather than answering ACP `cancelled`,
because `cancelled` tells a conforming peer the whole turn is over and would
abort the delegation on its first sensitive tool; it falls back to `cancelled`
only when the peer offered no reject option, and the stream-json responder path
still answers normally. An explicit user rejection of a single tool — the
dialog's Cancel/Esc, which `selectPermissionOption` maps to undefined — likewise
selects the peer's `reject_once` option instead of answering `cancelled`, so
denying one action scopes to that tool and the turn continues, matching the
button's "suggest changes" promise and the in-process sibling (which records
"User did not allow tool call" and lets the model proceed) rather than abandoning
the remaining work and billing a fresh prompt to re-delegate.
Child-process handling: the child is spawned detached with `sanitizeChildEnv`,
because `executor.command` comes from a project-level file, so a repository the
user merely cloned picks the executable and must not receive the daemon bearer
token. The handshake races a 10s deadline against both the `error` and `exit`
events, since a command that spawns and stays silent would otherwise hang
`create()` forever. Turn wall time defaults to 10 minutes when
`max_time_minutes` is omitted, matching the workflow subagent default, and an
invalid value throws before spawn. The child is owned by acp-bridge's
ProcessRegistry with `ownsProcessTree`, so disposal escalates SIGTERM to
SIGKILL and a SIGTERM-resistant descendant cannot survive; `dispose()` suppresses
only the two shapes where the foreign root is already gone — the signal it was
sent, and a root that exits on its own mid-snapshot (a Linux race where the
prompt rejects on stdout EOF before the process `exit` event, invalidating the
initial tree snapshot) — via the exported, unit-tested
`isExpectedExternalAgentCleanupExit`. The second shape covers the peer's own exit
status for ANY code/signal (`exited uncleanly during shutdown`), which acp-bridge
raises only after it has driven every owned process group to empty, so it reports
a foreign agent that exited — already reflected in `terminateMode` — not a cleanup
failure; letting `dispose()` reject on it would replace the turn's declared
terminal state (a user-CANCELLED turn would surface to the parent as `failed`).
Every genuine cleanup-PROOF failure (a truncated snapshot, an absent or
non-group-leader root, a failed snapshot/signal/inspect, or an exceeded exit
deadline — i.e. a descendant that may have survived) still propagates. A
post-handshake exit handler drains parked approvals;
its ERROR emit is guarded by `rawListeners`, because AgentEventType.ERROR is
Node's 'error' event and the background, resume and workflow emitters attach no
listener, so an unguarded emit would turn a child crash into an uncaught
exception. stderr goes through acp-bridge's `createStderrForwarder` for chunk
buffering and credential redaction. `methodNotFound` returns a real
`RequestError`, the only form the SDK preserves as -32601 rather than
repackaging as -32603, and the SDK's `extMethod` hook answers the same way
instead of hanging the agent.
Token usage for an external turn is unknown rather than free: the adapter
exposes only a context-window gauge, and feeding a level into accumulating
statistics would inflate totals past the window. The Agent tool therefore
suppresses execution stats for external subagents, skips live and completion
stat refresh, reports no execution summary, and appends "[External executor
token usage and cost are unavailable.]" to the result. That notice is a suffix
appended after the model-visible text and its empty-text fallbacks on both the
foreground and background completion paths, never baked into the text itself —
baking it in would make a non-GOAL background run that produced no text publish
the notice in place of the real failure reason (`Agent terminated with mode:
TIMEOUT`), because the `finalText || <reason>` fallback would see a non-empty
string. Transcript metadata records `executor: 'acp'` and no `persistedCliFlags`,
so the provenance is inspectable and the resume denial above has something to key
on. Stop reasons are mapped narrowly — end_turn to GOAL, max_turn_requests to
MAX_TURNS, cancelled to CANCELLED, and refusal, max_tokens and anything unknown
to ERROR — so a truncated external turn cannot be reported as completed, and a
peer that *refused* the task is not relabelled as a user cancel (CANCELLED
renders "cancelled by the user", a false statement that also hides the refusal
from telemetry). A wall-time expiry is classified by the timer's own
`ExternalAgentTimeoutError` identity rather than re-reading `Date.now()` in the
catch, because a second realtime clock can miss the monotonic deadline by under a
millisecond and misreport a turn that merely reached its budget as ERROR instead
of TIMEOUT. The continuation loop also re-checks that budget before dispatching
each round: `remaining` clamps to 0 once the wall time is spent, and without a
guard `connection.prompt(...)` is evaluated first — a new, billed model turn
really reaches the peer — one tick before `wait`'s 0ms timer rejects with
`ExternalAgentTimeoutError`, so the catch disposes (SIGTERM/SIGKILL) a peer that
just started work and the message the round already drained and recorded as
delivered is never processed. The loop now sets `TIMEOUT` and breaks before that
dispatch when `remaining <= 0`, mirroring the in-process sibling, which checks
the budget at the top of every round; it sets `terminateMode` directly rather
than throwing, because the catch classifies wall-time expiry by error identity.
And a continuation `execute()` turn — a resident external agent
re-invoked per incoming user message — emits the task as a user-side
`EXTERNAL_MESSAGE`, matching the in-process sibling, so the JSONL transcript does
not lose every message after the first. When a turn ends with a tool call still
open (cancel sets `cancelled` before the peer's terminal `tool_call_update` can
arrive, and timeout or peer crash leave it mid-flight), `runTurn`'s finally
flushes every entry left in the tool map as a failed `TOOL_RESULT` +
`TOOL_RESPONSES_FINALIZED` before `FINISH`, so the inline frame and Web Shell row
stop showing a tool executing forever, the JSONL `functionCall` is paired with a
`tool_result`, and the `FINISH` totals sum — the in-process sibling closes the
same gap via `onAllToolCallsComplete`. The session-update entry point drops any
update once the turn is over for ANY reason — its guard now also checks
`disposed` and the integrity-abort signal, not just `cancelled` — because on the
timeout and crash paths the catch awaits `dispose()` before the finally clears
`executing` and `cancelled` stays false, so a peer still writing would otherwise
be appended to `finalText`, emitted as `STREAM_TEXT`/`ROUND_TEXT`, and counted
into the `FINISH` totals for up to `EXIT_DEADLINE_MS` after termination, handing
a partial result to the parent verbatim on the non-rethrowing TIMEOUT path.
Safe mode also refuses an external executor at the dispatch gate, not only by
filtering discovery: `loadSubagent` resolves a `subagent_type` from disk
regardless of safe mode, and a cloned repo can ship `.qwen/agents/<builtin>.md`
carrying an `executor` that resolves before the built-in of the same name, so
safe mode now throws rather than spawn a definition-supplied binary.
The design doc now describes only the shipped product and these fail-loud
rules; the rejected peer-backend alternative is recorded as rejected.
Verification: core and cli `tsc` 0 errors, ESLint and Prettier clean, `git diff
--check` clean, cli executor tests 48/48, core subagent/agent/runtime suites
1483/1483, cross-package contracts and the two core-export sync guards 17/17.
Mutation
proofs, each run in both directions: removing the frontmatter executor guard
fails 8 tests, removing the workflow `agent()` rejection fails 6, replacing the
deny fallback with the first offered option fails 3, returning a plain Error
instead of `RequestError` fails the -32601 wire assertion, narrowing
`isExpectedExternalAgentCleanupExit` back to the signal-only form fails the
cleanup-exit classification test (which now also pins tolerating the peer's own
non-zero exit code), answering a host-policy denial with `cancelled`
instead of a `reject_once` selection fails 5, deriving the peer mode from the raw
definition instead of the host-resolved policy fails its dispatch test, reading
the sanitized frontmatter value instead of the original YAML node fails the
null-args rejection, making the `document.errors` refusal unconditional (instead
of executor-scoped) fails the non-executor lenient-load test, un-anchoring the
executor-claim text probe (dropping the column-0 / optional-quote form) fails
both the quoted-key refusal and the nested-key lenient-load tests, disabling the
turn-end `flushOpenTools` fails the open-tool flush test, deleting the
`loadCliConfig` executor registration fails the registration test, narrowing the
session-update guard back to `cancelled`-only lets a post-timeout `LATE` chunk
into `finalText` (failing the post-terminal-drop test), and disabling the
safe-mode dispatch refusal lets an external executor run in a trusted safe-mode
folder (failing the safe-mode refusal test), routing a user's Cancel back to ACP
`cancelled` (dropping the `reject_once` fallback at the respond site) fails the
user-cancel denial test, and reverting the executor-error guard to fire on any
`document.errors` entry instead of scoping to errors that reach the executor line
fails the unrelated-error-before-executor load test while the quoted-key and
unterminated-quote refusals stay green, removing the continuation budget guard
lets a 0-`remaining` round dispatch a second prompt (failing the
no-dispatch-after-budget test, which then sees two prompts and GOAL instead of
one and TIMEOUT), and reverting `astLostExecutor` to also require
`frontmatter.executor === undefined` lets a `parseDocument`-dropped executor load
through the `parseSimple` fallback as `command: '|'` (failing the dropped-node
refusal test), and re-throwing the raw error from the `toJS()` wrapper instead of
the graceful `SubagentError` fails the unresolved-alias refusal test (the
rejection becomes the raw "Unresolved alias" YAML error, not /invalid executor
block/). Mapping
`refusal` back to CANCELLED fails the continuation test, and dropping the
continuation `EXTERNAL_MESSAGE` emit fails the transcript test. The wall-time
test was the flake the timeout-classification fix removes; it now passes 5/5
deterministic runs. The real-subprocess suite is gated
`describe.skipIf(process.platform === 'win32')` like the repo's other
real-process suites, because Windows tree-kill reports a numeric exit code that
`dispose()` surfaces as unclean — the `test_windows` lane is merge-queue-only, so
an ungated suite there would eject the entry. The executor tests
drive a real ACP child process speaking the wire protocol, across
init/session/mode/prompt hangs and exits, max_tokens and unknown stop reasons,
duplicate permission callIds and optionIds, ask-user denial, descendant tree
kill, env sanitization and unsupported-extension responses. A separate
end-to-end run drove the real `@agentclientprotocol/claude-agent-acp` adapter and
a real model through this executor: `session/set_mode` `default` landed before
the prompt despite conflicting factory inputs, one permission request was emitted
and answered, a `Cancel` produced no file while a `ProceedOnce` selected the
adapter's `allow-once` (not its broader `allow-always`) and wrote exactly the
requested content, and both turns reported zero tokens — which is why the Agent
tool suppresses the summary rather than presenting zeros as free.
Not done: the real-adapter run above is a direct-factory harness, not the full
settings-loader → CLI → Web Shell path, and it does not exercise a browser
approval dialog, a second permission request in one turn, or headless auto-denial
against the real adapter; only macOS has been exercised, so the Windows `.cmd`
spawn path is unimplemented and untested (R3-1, left open — `cross-spawn`, the
repo's safe Windows launcher, is not a `packages/cli` dependency, and `shell:
true` around a project-supplied command is an injection surface, so this needs a
dependency decision and a Windows-testable design rather than a guess); the
optional mid-turn external-message waiter is unimplemented (R3-6, left open —
queued messages are still drained between turns, but an external subagent cannot
park for a not-yet-arrived one); and the Web Shell approval dialog still uses the
`info` confirmation variant, so it renders no file diff and has no screenshot
here. `ROUND_START`/`ROUND_END`/`USAGE_METADATA` are still not emitted, but the
one Critical consumer — the workflow stall watchdog, which arms only on those
events — can no longer be reached by an external agent at all, because workflow
`agent()` rejects external-executor definitions before spawn; `USAGE_METADATA`
also has no honest source (the adapter exposes a context-window gauge, not
per-turn token deltas). The round-4 through round-7 fixes in this commit were
audited to convergence by mutation proof and direct full-diff review; the earlier
rounds' agent-driven directionless audit did not converge because those agents
died on infrastructure failures.
* fix(subagent): close the round-10 review Criticals on the external executor
Five Criticals from the round-10 review of the external-agent executor and its
definition loader. Each is mutation-verified (reverting the fix turns its test
red) and the surrounding guarantees are pinned.
R10-1 — the executor-frontmatter parser derived both the claim and the value
from a column-0 regex plus the lenient `parseSimple` fallback instead of the
real YAML AST, which got the verdict wrong in both directions. Under-detection:
a TAB-indented top-level `executor:` is invalid YAML the AST drops, and a
column-0 probe missed it, so the definition loaded executor-less and ran
in-process under a Qwen model with nothing on any channel. Invented dispatch:
`parseSimple` hoists an `executor:` line sitting inside a `description: |` block
scalar into a top-level key, so a file declaring no executor loaded as external
and dispatched a command that exists only as prose. Both legs now come from the
AST: the claim probe is indentation-tolerant (`^[ \t]*`), `claimsExecutor` no
longer reads `frontmatter['executor']`, and `executorRaw` is `undefined` (never
the `parseSimple` value) when `parseDocument` has no top-level executor node.
The accepted cost — an `executor:` token nested under another key in an
otherwise-malformed file is now refused — is a visible, user-fixable
over-refusal, which beats an invisible substitution or an invented dispatch.
R10-2 — the load-time executor refusals were file *skips*, so `loadSubagent`'s
session>project>user>extension>builtin fall-through substituted a
lower-precedence in-process definition (or a case-insensitive builtin) of the
same name and the delegated turn ran under a Qwen model — the exact substitution
this feature prevents, with only a discovery-time `console.warn`. Executor-block
refusals are now recorded by declared name during the level scan (the
`parseSubagentContent` re-wrap preserves the `SubagentError` and its
`subagentName`), and the by-name fall-through path throws the recorded refusal
before descending or reaching `getBuiltinAgent`. Scoped to executor refusals (not
parse failures generally) so an arbitrary malformed repo file cannot disable an
unrelated builtin, and scoped to the no-level fall-through path (the explicit
-level path does not fall through, so it cannot substitute) to keep the
management callers' contract unchanged; `isNameAvailable` treats a refusal as
"name taken".
R10-3 — `isExpectedExternalAgentCleanupExit` swallowed acp-bridge's
`exited before its initial process-tree snapshot completed`, which
`mergeAsynchronousSnapshot` records and returns BEFORE `collectOwnership`, so a
detached (`setsid`) descendant was never enumerated, signalled or reaped — the
tree was not proven gone, yet `dispose()` resolved silently and the parent saw a
clean teardown. That shape is now `isUnprovenExternalAgentTreeExit`: `dispose()`
reports it (`debugLogger.warn` + an `AgentEventType.ERROR`) and resolves rather
than rethrowing (it is awaited between terminal-state classification and return,
so rethrowing would convert a classified TIMEOUT/CANCELLED into a thrown ERROR).
`isExpectedExternalAgentCleanupExit` is narrowed to the peer's own unclean exit
(raised only once the tree is proven gone); every other cleanup-proof failure
still propagates.
R10-4 — `runTurn` destructively drained the external-input queue and recorded the
messages as delivered before the round-top budget check, so a wall-time expiry
(or a bottom-of-loop abort) discarded user messages already emitted as delivered
— the transcript certified delivery of a message never sent, and the registry had
already spliced it out. The drain is now gated on the budget and the abort signal
first, mirroring the in-process sibling (`agent-core` checks the budget before
`drainExternalInputs`): when the budget is spent or the signal aborted, the loop
breaks before draining, so the message stays queued for a future turn.
R10-5 — an absent `max_time_minutes` silently meant "10 minutes then terminate"
for an external agent, while meaning "no cap" everywhere else, so the same
definition behaved differently based only on whether it declared an executor, and
a TIMEOUT turn (handed to the parent as the answer with no truncation marker) was
reachable by default. The cited rationale was void: `DEFAULT_WORKFLOW_SUBAGENT_
MAX_TIME_MINUTES` is read only on the workflow path, which hard-rejects external
executors upstream, so it never reaches this executor. An absent value is now
passed through as `undefined` (no timer), matching `agent-core`; a caller that
wants a cap supplies `max_time_minutes` (validated positive, finite, ≤ the Node
timer range).
Verification: core and cli `tsc` 0 errors (after a full `npm run build` to refresh
the workspace `dist` the merge had left stale); ESLint and Prettier clean; `git
diff --check` clean; cli executor suite 50/50; core subagent/agent/runtime
suites 2510 passed (6 skipped); cross-package contracts and the two core-export
sync guards 17/17. Mutation proofs, each run in both directions: reinstating
`?? 10` fails the no-default-cap test; removing the pre-drain budget/abort gate
fails the queue-preserved test (the message is drained, recorded delivered, and
lost); making `dispose()` swallow the snapshot-race error fails the
unproven-tree-report test; reverting the claim probe to column-0 anchoring fails
the nested-token refusal test; restoring the `parseSimple` fallback for
`executorRaw` fails the block-scalar-prose test (it loads `command:'npx'` from
prose); and no-oping the recorded-refusal throw fails the by-name dispatch test
(it resolves the builtin instead of rejecting). Reverse-audit converged after one
fix (scoping the R10-2 throw to the fall-through path so the explicit-level
management callers keep their null contract), then clean passes over the
production and test diffs.
* fix(subagent): close the round-11 review Criticals on the external executor
Six Criticals from the round-11 review: five on the executor and its definition
loader, one a regression the round-10 R10-2 fix itself introduced. Each is
mutation-verified (reverting the fix turns its test red).
R11-1 — a file that CLAIMS an executor but fails an EARLIER validation (a
missing description, a bad approvalMode) was skipped with nothing recorded, so
loadSubagent's fall-through silently substituted the builtin of the same name.
The executor claim and the trusted declared name are now computed at function
scope, right after the frontmatter is parsed (BEFORE any validation can throw),
so the catch converts ANY load failure of an executor-claiming file into a named
executor refusal the fall-through throws. (A file whose own name is unparseable
stays undefined-keyed and falls through to the generic wrap — it cannot be
matched by name anyway.)
R11-2 — the wall-time budget was computed per-turn (timeoutMs - elapsed this
turn), so a resident agent re-invoked per incoming message (resetStats:false,
which preserves durationMs) got a FRESH max_time_minutes cap each continuation
and could overrun the whole-delegation budget indefinitely. Both remaining
computations now subtract the accumulated durationMs, matching the in-process
sibling's preserveStats base: max_time_minutes caps the whole delegation. Both
production continuation callers (background-agent-resume, agent) pass
resetStats:false, so the cumulative cap reaches the real path.
R11-3 — the host approval mode was used AS the peer's mode id, but the
vocabularies differ (host/qwen: auto-edit/auto/yolo; Claude: acceptEdits/
bypassPermissions), so a host auto-edit could select a peer mode that does not
exist (or, worse, a qwen peer's broader auto). resolvePermissionMode now maps the
host policy to a peer-vocabulary-independent TOKEN (default/plan/acceptEdits/
bypass) and connect() picks the first id the peer actually advertises via a
canonical-first alias table, refusing (naming the policy) when none is
advertised. The mapping never WIDENS: acceptEdits prefers the peer's narrowest
edit-only alias.
R11-4 — the executor refusal was keyed by the lenient parseSimple name, which
strips only double quotes, so a single-quoted `name: 'Explore'` was recorded
under "'explore'" and missed the 'explore' dispatch lookup, falling through to
the builtin. The refusal is now keyed by the real YAML AST's name (parseDocument
strips both quote styles), with the lenient value only as a fallback when the
AST read throws.
R11-5 — the approval confirmation's prompt rendered the OPTION LABELS (which the
dialog already shows as buttons) and dropped the action's arguments, so the user
approved "Write" without seeing the `rm -rf ./build` it would run. The prompt is
now describeExternalAction(toolCall): the title plus the rawInput, bounded to
300 chars and stripped of C0/C1/DEL control chars (foreign-process data, never
rendered as markup).
R10-2 (fix-induced) — extension agents load via loadSubagentFromDir, which skips
+ warns on a refusal, so the R10-2 level-scan recording never ran for them and
the extension leg of the fall-through read an empty map. loadSubagentFromDir now
takes an optional refusal collector; the runtime loadExtension records each
extension's executor refusals onto extension.agentExecutorRefusals, and
listSubagentsAtLevel('extension') merges them into the 'extension' refusal
bucket before findSubagentByNameAtLevel returns, so the by-name fall-through
refuses them too. (The install/update consent path also calls
loadSubagentFromDir, but only for display — it does not feed the dispatch
fall-through, so it needs no collector.)
Verification: core and cli tsc 0 errors; ESLint and Prettier clean; cli executor
suite 57/57; core subagent-manager suite 199/199; core subagent/agent/runtime
regression net 2513 passed (6 skipped). Mutation proofs: dropping the cumulative
durationMs subtraction fails R11-2 (turn 2 ends GOAL, not TIMEOUT); removing the
peer-canonical acceptEdits alias fails R11-3 (the auto-edit dispatch finds no
advertised mode); reverting the approval prompt to the title fails R11-5 (no
`rm -rf ./build`); neutralizing the catch conversion fails R11-1 (loadSubagent
resolves the builtin); keying by the lenient name fails R11-4 (single-quoted
name resolves the builtin); and no-oping the extension-refusal merge fails the
R10-2 extension leg. Reverse-audit converged after one fix (the R11-2 test burned
the whole budget in turn 1, tripping the R10-4 pre-drain gate's GOAL-to-TIMEOUT
reclassification; redesigned so turn 1 ends GOAL and turn 2 hits the cumulative
cap), then clean passes over the production and test diffs.
* fix(subagent): close the round-12 review Criticals on the external executor
Six Criticals from the round-12 review — three on the executor, one a
regression the R11-5 fix itself introduced, and two on the definition loader's
refusal bookkeeping. Each is mutation-verified (reverting the fix turns its
test red).
R12-1 — execute() re-rendered and re-sent the entire system-prompt bundle on
every continuation turn, although the live ACP session has held it since turn 1
(NewSessionRequest has no system-prompt channel, so the prompt content block is
the bundle's only channel and the FIRST turn must still send it). A resident
external agent re-invoked per incoming message re-sent the definition's
systemPrompt + the appended rules + the whole memory hierarchy (~16.5 KB) every
turn, billed by the peer and pushing its context toward compaction, while the
transcript recorded only the task. The bundle is now gated on the first turn:
`const continuation = this.started` is hoisted above the render and
continuations send only the task.
R12-2 — the turn-entry `emitInputs` recorded the caller's messages as delivered
BEFORE the round-top wall-time guard, so an over-budget continuation wrote
EXTERNAL_MESSAGE user records for a prompt that was never dispatched (the guard
breaks TIMEOUT first). The entry emit moved to the loop's commit point — after
the budget guard, before the prompt — keyed on the ENTRY round (`entryRound`,
not `this.round === 1`, which is broken under resetStats:false because the round
counter is preserved across continuations). Emitting before the dispatch keeps
user-before-assistant order in the JSONL transcript.
R11-5 (fix-induced) — the R11-5 confirmation carried the peer's arguments but
rendered them as MARKDOWN (the `confirmationDetails` omitted
`renderPromptAsPlainText`), so a glob `**` was eaten and a `[label](url)` could
mis-render, misrepresenting the action being approved. The info variant now sets
`renderPromptAsPlainText: true` (the bound and control-strip stay — the flag is
not a substitute for them). Consolidating the local control-char stripper into
the centralized `stripAnsiAndControl` was considered but requires a new
cross-package core export; the flag is what neutralizes the rendering, and the
local stripper already kills the ESC byte so no escape sequence survives.
R12-3 — `resolveResumeTarget` reaches the `loadSubagent` that R10-2/R11 made
throwing, unguarded, so background-agent discovery swallowed the throw into a
debug-only per-sidecar warning and the recovered row VANISHED from /tasks
instead of listing with a `resumeBlockedReason`. The `loadSubagent` call is now
wrapped and the throw converted into the existing `unavailableReason` shape
(both call sites already fold it into `resumeBlockedReason`). The
executor-provenance early return stays ahead of the call, so the four
cold-external-resume provenance legs still refuse before any load.
R12-4 — `executorRefusals` was only rewritten when a level scan reached the end
of its `try`, so a scan that could not read the directory left the previous
scan's refusals in place and `loadSubagent` kept throwing for a file that no
longer exists. Both non-scan paths now reset the level's map: the `readdir`
catch and the `project == home` early return.
R12-5 — the indentation-tolerant raw-text probe made `claimsExecutor` true for
frontmatter that only MENTIONS `executor:` as prose inside a `description: |`/`>`
block scalar, so `astLostExecutor` hard-refused an in-process definition that
carried an unrelated tolerated YAML quirk (duplicate key, tab indent), and keyed
the refusal by its name — blocking every lower-precedence definition and builtin
of that name. The probe match is now excluded when its offset lies inside a
block scalar (walked via the real AST's BLOCK_LITERAL/BLOCK_FOLDED scalar
ranges, fail-closed if the walk throws). A genuine top-level — even
TAB-misindented or nested — `executor:` key is not inside a block scalar, so the
R10-1/R9-2/R7-1 refusals all still hold.
Verification: core and cli tsc 0 errors; ESLint and Prettier clean; cli executor
suite 61/61; core subagent-manager 202/202 and background-agent-resume 56/56;
core subagent/agent/runtime regression net 2517 passed (6 skipped). Mutation
proofs: re-sending the bundle on continuation fails R12-1 (turn 2 carries 2
blocks); moving the entry emit back above the guard fails R12-2 (`delivered`
becomes `['second']`); dropping `renderPromptAsPlainText` fails the R11-5 case;
rethrowing in `resolveResumeTarget` fails R12-3 (the row vanishes); deleting the
catch reset fails R12-4 (stale refusal persists); dropping the block-scalar
exclusion fails both R12-5 variants (they reject). Reverse-audit converged to
clean passes over the production and test diffs.
* fix(subagent): resolve the two carried Criticals via maintainer decision
Closes the two Criticals standing since round 3, per the maintainer's decision
on the round-12 review: both are resolved by an explicit, surfaced limitation
rather than an untestable/billing-risky implementation. Each is
mutation-verified (reverting the fix turns its test red).
R3-1/R3-22 (Windows spawn) — decision: POSIX-only for this release. On Windows
an npm-installed adapter resolves to a `.cmd` launcher that libuv's PATH search
never finds (bare name + `.exe` only) and Node >= 18.20.2 refuses to spawn
without a shell; the process-tree reaping is likewise POSIX-specific. The
executor previously failed at dispatch with a misleading `spawn <cmd> ENOENT`
(reporting an installed adapter as missing). A new
`assertExternalAgentSpawnPlatformSupported` guard now fails closed at the top of
`create()` with a clear, actionable "POSIX-only in this release" error, before
any spawn. A cross-spawn-style PATHEXT resolution + quoted `cmd.exe` arm is a
tracked follow-up (it changes the security-critical detached-spawn path and is
untestable without a Windows lane).
R3-6/R3-51 (mid-turn waiter) — decision: explicitly decline + surface the
limitation. ACP v1 has no mid-turn injection primitive (no `session/steer`), so
the executor deliberately does NOT implement `setExternalMessageWaiter` /
`setExternalMessageWaitPredicate` (documented on the class); input arriving
mid-prompt is delivered at the next turn boundary via the provider. To stop the
background loop presenting a queued steer as if delivered mid-turn, the
delegation result for an external executor now appends
`EXTERNAL_MID_TURN_INPUT_NOTICE` (alongside the existing usage notice) stating
queued input is delivered between turns, not mid-turn. True mid-turn steering
(cancel + re-prompt) re-bills the in-flight turn — a protocol/billing decision,
tracked as a follow-up.
Verification: cli + core tsc 0 errors; ESLint + Prettier clean; cli executor
suite 63/63 (incl. a win32-stubbed `create()` rejection that never reaches
spawn); core agent suite 288/288; core subagent/agent/runtime regression net
2518 passed (6 skipped). Mutation proofs: removing the win32 throw fails both
R3-1 tests; dropping the mid-turn notice fails the R3-6 result assertion.
Reverse-audit converged to clean passes over the production and test diffs.
---------
Co-authored-by: probe <probe@local>
|
||
|
|
0039f184f8
|
fix(ci): space PR creation from release to avoid secondary rate limit (#11444)
* fix(ci): space PR creation from release to avoid secondary rate limit * test(ci): pin sleep-before-create gap in all three release workflows |
||
|
|
e09c461a92
|
fix(cli): restore OpenTUI banner, footer, and loading states; fix the OSS mirror flavor (#11437)
Some checks failed
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Update ECS Runner Qwen / Resolve version (push) Has been cancelled
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-1 (push) Has been cancelled
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-2 (push) Has been cancelled
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-3 (push) Has been cancelled
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-4 (push) Has been cancelled
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-5 (push) Has been cancelled
Update ECS Runner Qwen / Report a stale fleet (push) Has been cancelled
* fix(cli): restore banner, footer, and loading states in the OpenTUI shell The batched OpenTUI migration restructured the original single-file backend into modules and dropped three user-visible pieces of the chrome along the way: the startup banner, the bottom status line, and the responding spinner. The OpenTUI renderer therefore booted into a screen that looked nothing like the ink one it was meant to replace. Rebuild all three as their own modules and mount them in the app shell, following the ink layout: banner at the top, loading indicator above the composer, status line at the bottom. Two defects carried over from the original implementation are fixed rather than copied. The context-usage label was missing its percent sign, and the approval-mode colour check compared against a label that never matched, so the auto-edit mode fell through to the default colour. The context-file count is intentionally not restored: the shell no longer tracks it and the current ink footer dropped it too. * fix(cli): tick the OpenTUI spinner at the rate ink uses The restored spinner ran at 120ms, carried over from the pre-batch implementation, while the ink responding spinner it mirrors runs the same frame set at 80ms and the existing OpenTUI compaction spinner does too. The same animation therefore looked visibly slower under one renderer. Name the interval and set it to the shared rate. The frame set itself is unchanged. * fix(release): sync the OSS mirror by what the tag shipped, not by a variable The workflow that mirrors a release to Aliyun OSS decided whether to expect the bun/OpenTUI preview archives from a repository variable. A repository variable describes the default branch, while the checkout and the downloaded assets come from the tag being synced, and the workflow can be re-dispatched for any tag. Re-syncing a tag cut before the preview flavor existed therefore asked that tag's own verify script for archives the release never shipped: v0.22.3 fails because its script does not know the flag at all, and v0.23.0 fails on a missing checksum entry. Derive the flag once from the archives actually downloaded and let all three consumers read it, so the mirror follows the release instead of the branch. Two related gaps in the same gate are closed here. The test covering it asserted only the variable name, so inverting the comparison and silently changing the flavor's default polarity stayed green; it now pins the operator. And the variable appeared in no documentation, so an operator had no way to learn that leaving it unset is what keeps the flavor off. * fix(cli): source the OpenTUI chrome from main's shared helpers The restored banner, footer, and responding indicator had grown private copies of mappings main already owns, so the two renderers could drift. Route the approval-mode label through formatApprovalModeName, extract the context-usage format and version-label rules into ink-free helpers both trees consume, and take the spinner frames and loading phrases from the shared constants and cycler. Also truncate the footer rows and the indicator phrase to the terminal width so a long branch, path, or phrase cannot wrap the chrome mid-turn, and read typed Config accessors in the banner instead of structural casts and a manual package.json read. |
||
|
|
a5bc6c5497
|
fix(ci): isolate serve route E2E from fork pressure (#11389) (#11391)
* fix(ci): isolate serve route E2E from fork pressure (#11389) The Linux docker E2E leg has gone red four times since #11306 merged the three Linux shards into one job with three Vitest forks (issues #11331, #11343, #11384, #11389). In run 34221129472 the cli/qwen-serve-routes.test.ts suite — a long-lived `qwen serve` daemon answering local HTTP requests — took 297.8s and timed out on all three attempts against 85.5s in the preceding green run, while the same commit's sandbox:none leg passed. The suite has this exact signature on record already: AcpSessionBridge initialize timeouts and 504s on the sandbox:docker leg under shared-host load. Run the latency-sensitive suite alone in a single fork after the three-fork batch on both Linux legs, leaving every other suite's parallelism unchanged. The sandbox:none bounded retry now covers the batch and the isolated suite as one attempt; the docker leg still runs each command exactly once. Workflow and retry witnesses updated, with bash-executed coverage that a failure in either half retries once and that two failed halves keep the step red. * test(ci): pin isolated E2E suite order and filter targets (#11389) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): complete exclude tree pins and tolerate benign script formatting (#11389) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
70cf363395
|
feat(web-shell): Improve split-view session navigation (#11250)
* feat(web-shell): Improve split-view session navigation * fix(ci): Run Web Shell browser checks on hosted runners * fix(web-shell): Keep split navigation outside approval shortcuts * fix(web-shell): Address split-view review regressions * fix(web-shell): Stabilize split approval reports * fix(web-shell): Preserve history anchors during slow rendering --------- Co-authored-by: 易良 <1204183885@qq.com> |
||
|
|
b9f5d15a5b
|
fix(ci): isolate subprocess-heavy E2E from fork pressure (#11388)
* fix(ci): isolate serve route E2E from fork pressure * refactor(ci): move E2E runner out of workflow YAML * refactor(ci): reuse E2E runner arguments * fix(ci): serialize SDK E2E subprocess suites |
||
|
|
d87ccc6579
|
fix(ci): reap release integration containers (#11330) | ||
|
|
078b924989
|
refactor(ci): extract release workflow scripts (#11165)
* refactor(ci): extract release workflow scripts
* test(ci): pin release-note labeling to the extracted step script
release.yml no longer inlines the auto-labeling body: the publish step now
just dispatches to run-release-step.sh, which owns the git rev-list
enumeration and the classify-release-notes.mjs call. The wiring test kept
asserting those four strings against the step's run block, so it failed on
the dispatcher path alone.
Read run-release-step.sh and assert the labeling body there, and assert the
dispatch from release.yml. The workflow-level guarantees (step name,
continue-on-error, GITHUB_TOKEN env, issues/pull-requests write permissions)
and the .github/release.yml exclusion entry are unchanged.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtpmlcodmz
* test(ci): repoint release assertions at the extracted step script
The extraction moved the GitHub Release notes, standalone archive, and
release-failure autofix label logic out of .github/workflows/release.yml
into .github/scripts/run-release-step.sh, but three consumer suites still
pinned the moved text in the YAML, so `npm run test:scripts` was red and
the release workflow's fail-closed `quality` gate would refuse to publish.
Read the step script in ai-release-notes-workflow, install-script, and
qwen-autofix-workflow and assert the moved strings there with their new
lowercase locals (`notes_args`/`notes_file`), keeping on release.yml only
what genuinely stayed in it. Restore the "Safe to auto-apply approval"
rationale above both AUTOFIX_APPROVED_LABEL writes in the notify-failure
arm; it is the only recorded justification for the workflow granting
itself autonomous autofix approval.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtq0vr74ni
* test(ci): derive the ECS wipe subject set from the runner, not the wipe
Selecting checkout jobs by "has a Restore workspace ownership step" made
the subject set a function of the property under test, so a job added to
the ecs-qwen-hk4-host pool with an actions/checkout and no wipe was
filtered out before any assertion ran. Key the exemption on the pool
marker instead, and pin that every pool-routed job carries the wipe
whether or not it checks out.
The hosted label cannot be the discriminator: every pool-routed runs-on
expression names 'ubuntu-latest' as its fallback branch.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtq0vr74ni
* test(ci): re-scope the release-creation token pin to its own step
The workflow-wide toContain was satisfied by the second CI_BOT_PAT
occurrence on "Trigger ECS runner qwen update", so flipping the token on
"Create GitHub Release and Tag" to github.token left every lane green.
Restore the step scoping and the deleted rationale comment in release.yml.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2
* fix(ci): fail the release step runner closed on pipeline legs
run-release-step.sh set bare `set -e` while both sibling runners set
pipefail. In notify-failure a connection-level gh failure inside
`gh issue list ... | jq -c ...` leaves jq exiting 0 on empty input, so the
empty result took the create-a-new-issue branch and filed a duplicate
autofix/approved release-failure issue past all three reuse guards.
Pin the flag next to the existing sibling pin and add a behavioural row for
the notify-failure arm with an unreachable gh.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2
* test(ci): relocate the release timeout value-format contract
This PR condensed the nine-line release.yml lane comment to "values are
minutes" and relocated the format half nowhere: grep for the deleted claims
(expression error / free text / truthy / leading number) found no copy under
.github/, scripts/tests/ or docs/. Put the contract beside the assertions that
already pin the three tunable lanes' expressions, since release.yml is
size-pinned twice.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2
* docs(ci): scope the workflow_sha claim to the extracted helpers
The design doc said the trusted checkout "prevents an operator-selected
release ref from supplying code that receives release credentials". Two paths
at HEAD contradict that reading: resolve-version executes the ref's
scripts/get-release-version.js under the job token (run-release-step.sh:84),
and push-release-branch commits with core.hooksPath .husky (:116, :130) in a
step whose env carries CI_BOT_PAT (release.yml:659). Both predate this
extraction, so name them as residual paths rather than as prevented ones.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2
* fix(ci): format the release-version test to satisfy the Prettier gate
`Lint & Static` failed at the `Run Prettier` step: a continuation line in
the `assert-unreleased` format-gate test carried four extra spaces.
* fix(ci): exclude nested .git dirs from the docker build context
The extraction added a `.release-workflow/` checkout to `integration_docker`
between the sandbox build's reset and the build itself, and `.dockerignore`
listed `.git` only in its root-anchored form. A depth-1 sparse checkout of
the pinned ref is 1.1M of worktree over 51M of git objects, so every
uncached sandbox image baked those objects into a discarded builder layer on
the shared pool this job is routed to. `node_modules` and `dist` already
carry both forms.
* fix(release): close four gaps the script extraction left behind
R1-15: the push-time guard returned exit 2 for a malformed version, and
run-release-step.sh treats 2 as a transient probe failure — so a refusal
that can never succeed was retried three times over ~45s and logged as a
connectivity problem. Malformed versions now exit 4, which the retry loop
breaks on immediately and which is not the exit-3 version_refusal marker.
R1-16: the extraction moved the guard behind `.release-workflow/` so the
decision comes from the workflow-pinned SHA, but left a working
`--assert-unreleased` dispatch in the ref-supplied get-release-version.js.
A future step using that spelling would load the selected ref's guard.
Removed it, and re-pointed its five tests at the real entry point.
R1-13: assert-release-version.mjs ships alone under release.yml's
sparse-checkout set, so it cannot import release-helpers.js — the
duplicated isExpectedMissingGitHubRelease is deliberate. Documented that
at both sites and pinned the two bodies identical, so widening only the
guard's copy (which would read a rate-limited probe as "release absent")
goes red.
R1-6: restored the notify-failure arm's rationale, matching the condensed
comments the other five arms kept: the exact-title re-check that stops a
v0.18.1 failure reusing a v0.18.10 issue, the bot-author preference, and
the re-query that keeps autofix off a maintainer-owned issue.
* fix(release): re-pin the trusted runner before every step that runs it
R1-10: `Build Bundle and Prepare Package`, `Build Standalone Archives` and
`Verify Standalone Archives` executed `.release-workflow/.../run-release-step.sh`
from a tree that selected-ref npm code had already run over — `build-package`
for the first two, and `npm publish`'s own lifecycle scripts for the third,
inside the job that holds `id-token: 'write'`. At the merge base these were
inline `run:` bodies sourced from the workflow file, immune to anything
written into the working tree, so the exposure is introduced by the
extraction. Each now gets the reset-and-re-checkout pair the design doc
already promises for credential-bearing steps.
The guarding test hand-listed six protected step names, so it could not fail
for the three it omitted. It now sweeps every step whose `run` mentions
`.release-workflow/` (17 today) and separately pins that each re-checkout is
preceded by a reset.
R1-5: nothing tied the publish allowlist to the guard's `PUBLISHED_PACKAGES`
except a comment pointing at publish steps this PR deleted from release.yml.
A channel added to the loop but not the array would ship unprobed, so a retry
of a partial release reports "unreleased" and force-pushes over the tip the
shipped package anchors to. The two sets are now pinned against each other,
and the stale comment points at the real pin.
The three inserted step pairs grow release.yml by 294 bytes (well inside the
4096-byte allowance); the ratchet baseline and the PR description move with
it rather than letting the recorded number drift.
* test(release): pin the flags, arms and modes the extraction left unwitnessed
R1-19: the arm's only executable probe stubbed npm with a script that
recorded `$PWD` and discarded every argument, so deleting `--tag=${NPM_TAG}`
stayed green while `npm publish` would fall back to the `latest` dist-tag —
handing the 21:00 UTC nightly to every end-user install and to the ECS fleet
updater. The stub now records the argv and each publish is checked for
`--access public` and the dist-tag.
R2-3: `resolve-version` was the one arm no test executed or text-pinned;
flipping `--type=preview` to `--type=stable` survived all 2233 scripts tests,
while at runtime the stable path ignores `preview_version_override` and would
publish an operator's manual preview as a stable release. Two execution tests
now cover the preview mapping and the malformed-version rejection; both were
run against the real arm before pushing.
R1-18: release.yml invokes these scripts by bare path, so the executable bit
is load-bearing, but every test invokes them as `bash <script>`, which
ignores the mode — and the workflow triggers only on schedule/dispatch, so no
pull-request lane runs the bare-path form. A mode-normalizing commit would
keep the suite green and kill `set-flags` with exit 126. The recorded git
mode is now pinned for all three scripts.
* test(release): pin the trusted prefix, the fork gate and the real version formats
R1-3: every arm pin elsewhere is a substring match that a bare
`.github/scripts/run-release-step.sh` invocation would satisfy just as well,
so nothing pinned the mechanism the extraction exists for. All 17 step
invocations are now required to carry the `.release-workflow/` prefix and
forbidden from naming the release-ref copy.
R1-9: `PUBLISH_AUDIO_CAPTURE` replaced a deleted step-level
`if: github.repository == 'QwenLM/qwen-code'` gate and, unlike its
`PUBLISH_EXTERNAL_CONTEXT_MEM0` sibling, had neither its gate expression nor
its true branch pinned — a fork running this workflow would publish
@qwen-code/audio-capture. Both halves are pinned now.
R1-14: the format gate is the guard's only genuinely new behaviour and its
accept path was exercised solely with '1.2.3', while the two scheduled
releases produce nightly and preview strings. Both formats are now asserted,
so tightening the pattern fails here rather than at 21:00 UTC.
* fix(release): keep failure reporting alive when the trusted runner is not
R1-11: `notify_failure` gained a checkout of `github.workflow_sha` as its
first step, and its whole body now lives in a shell script. At base the job
had no checkout at all and the notifier was inline, so the job whose only
purpose is reporting every other job's failure acquired two ways to fail
silently: a degraded git backend takes out the checkout, and a syntax error
or dropped exec bit in run-release-step.sh takes out every arm including
notify-failure. In both cases `Create Issue on Failure` carried the default
`if: success()` and was skipped — no issue, no autofix label, no dispatch,
and the scheduled autofix fallback scans issues, so nothing recovered it.
Rather than moving 105 lines of notifier back into the workflow — which the
orchestration guard in this PR forbids at 12 run-lines per step — the primary
path keeps the extraction and stops being load-bearing: it runs under
`always()`, absorbs its own failure, and a five-line inline fallback files a
plain issue whenever it did not succeed. The fallback depends on neither the
checkout nor the script, does no reuse, labelling or dispatch, and accepts a
duplicate issue as the cost of never going silent.
The workflow's line ceiling moves 800 -> 830 for those lines, with the reason
recorded at the assertion; the per-step run cap that keeps logic out of the
YAML is unchanged and still holds. Ratchet baseline and the PR description
move with the file.
* style(release): apply the pinned Prettier to the fallback notifier
Prettier 3.6.1 normalizes the quoting on the new `if:` and drops a
trailing space; the ratchet baseline follows the two-byte change.
* docs(release): name exit 4 where the guard's exit codes are described
Adding the malformed-version code left two descriptions behind: the retry
loop had no statement of the contract at all, and the test comment beside it
still said exit 0 and exit 3 were the only decisive outcomes.
* fix(release): avoid duplicate fallback failure issues
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
|
||
|
|
3ecfaffdf8
|
perf(dev): add pnpm worktree bootstrap foundation (#10449)
* perf(dev): add pnpm worktree bootstrap foundation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(dev): harden worktree bootstrap process handling Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * perf(dev): prefer cache-only worktree installs Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(dev): preserve cached install cancellation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(build): harden pnpm worktree bootstrap Validate pnpm lock updates in releases and exercise real installs and builds across supported hosts. Preserve npm release compatibility and keep dependency-only setup from rewriting npm-layout notices. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: record merge attribution Record the required attribution for the Stage 1 merge without rewriting published history. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(dev): keep worktree bootstrap clean Use a bootstrap-scoped notice guard because nested npm lifecycle commands replace npm_lifecycle_event. This preserves explicit notice generation while preventing dependency setup from rewriting tracked output. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci: track pnpm smoke workflow size Register the new workflow in the repository size ratchet as required by the main CI gate. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): drop stale importers from the new pnpm lockfile The lockfile was generated before the WebShell cutover (#9811) removed @qwen-code/webui from web-shell and the tailwind tooling plus @qwen-code/webui from vscode-ide-companion, so frozen-lockfile installs fail on all three smoke platforms with ERR_PNPM_OUTDATED_LOCKFILE. Drop the five stale importer entries so the lockfile matches the current package.json manifests; verified with pnpm 11.24.0 install --frozen-lockfile --lockfile-only. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): refresh pnpm lockfile after merging main The merge of origin/main added remend@^1.3.1 to packages/cli/package.json without updating pnpm-lock.yaml, breaking the pnpm Worktree Smoke workflow frozen-lockfile install. Regenerated with pnpm install. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtid3rse9h * fix(scripts): resolve the path variable case-insensitively on Windows A spread of process.env is an ordinary object, and native Windows shells expose the path variable as `Path`, so `env.PATH` was undefined there and findOnPath never located corepack — the offline-first Corepack bootstrap silently degraded to npx on exactly the hosts it exists for. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(scripts): pin the worktree bootstrap guards against mutation - Split the fallback log on /\r?\n/ like the sibling cmd.exe mocks, so the assertion holds on the Windows lane's CRLF output. - Add a win32 variant bootstrapping with the native `Path` casing to pin the case-insensitive lookup. - Assert the smoke workflow's fail-fast flag, the install-before-clean step order, and the no-build guard via a substring on the raw job text. - Extend the pnpmfile rewrite fixture to devDependencies and optionalDependencies, which the committed lockfile already uses. - Assert the notice-skip guard by effect (writeFile never called) and add the flag-absent companion test with I/O stubbed. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore(deps): sync pnpm-lock.yaml with qwen-live ACP backend deps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtjyz3tdco * fix(dev): stop patch-package from corrupting the pnpm store The root postinstall runs patch-package, which rewrites files under node_modules in place. With pnpm's default import method those files are hard links into the content-addressable store, so the patch rewrites the store entry too and its contents stop matching the sha512 it is filed under. Every subsequent fresh worktree then fails its --offline stage with ERR_PNPM_NO_OFFLINE_TARBALL, silently falls back to the registry (exit 0), and the fallback's postinstall corrupts the store again, so the offline path never hits. The advertised offline timing was only reachable on a same-tree reinstall, which needs no relinking. Set packageImportMethod to clone-or-copy: a copy-on-write clone where the filesystem supports it, a plain copy elsewhere. Either way patch-package edits only the worktree's own copy and the store entry stays intact, so later worktrees resolve entirely from the store. Verified on an isolated fixture store: under the default method the node_modules file and its store entry share an inode, and an in-place edit moves the store file's sha512 off its address, after which a fresh offline install fails with ERR_PNPM_NO_OFFLINE_TARBALL; under clone-or-copy the same edit leaves the store hash unchanged and the fresh offline install reports reused 1, downloaded 0. Also add pnpm-lock.yaml to .prettierignore. prettier reflows it into a shape pnpm does not emit, so `npm run format` and `pnpm install` fight over the file; .yamllint.yml already ignores it for the same reason. Both facts are pinned by tests in scripts/tests/package-scripts.test.js. Claude-Session: https://claude.ai/code/session_012797rgiteWJxLT9TLkKq8G * fix(ci): refresh pnpm-lock.yaml for qwen-live prompts deps Main added prompts/@types/prompts (and the ACP sdk) to packages/qwen-live/package.json after this branch generated its lockfile, so the pnpm Worktree Smoke frozen-lockfile install failed with ERR_PNPM_OUTDATED_LOCKFILE. Regenerated with pnpm 11.24.0 (--lockfile-only); frozen-lockfile and supply-chain policy checks pass. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtlc9sozfg * fix(pnpm): close review findings on worktree bootstrap - Declare @qwen-code/qwen-code-core in vscode-ide-companion so the pnpm linker materializes it; regenerate both lockfiles and assert the link in the smoke workflow (R5-1) - Pin @types/node 20.19.1 in packages/core so the pnpm layout compiles against the same types npm hoists (D6-1) - Accept and pin the corepack +sha512 integrity suffix on packageManager; strip it for the npx fallback spec (D6-2) - Cover every workspace member in the pnpmfile rewrite set, mirror the npm channel list in pnpm-workspace.yaml, and cross-check both in tests - Run setup-worktree.js installs with cwd pinned to the checkout, gate the smoke clean-check on git status --porcelain, stop post-merge runs from cancelling each other, and extend check-lockfile.js to pnpm-lock.yaml * merge(main): realign pnpm workspace set and lockfiles Main removed the webui and cua-driver packages; drop both from the pnpm rewrite set and regenerate the pnpm lockfile against the merged manifests (picks up playwright, react-markdown, and the other main-side dependency changes) so the frozen bootstrap matches what the PR merge ref will run. * style(ci): quote smoke workflow scalars per yamllint * fix(dev): require Corepack for pnpm bootstrap * fix(dev): close pnpm bootstrap review blockers --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: yiliang114 <jinjing.zzj@gmail.com> |
||
|
|
f3d9fa59a4
|
fix(ci): isolate E2E fork test directories (#11312) | ||
|
|
55926e4617
|
feat(serve): allow concurrent standalone daemons with session fencing (#11207)
* feat(serve): allow concurrent standalone daemons with session fencing * fix(serve): address standalone ownership review feedback Preserve writer-blocked navigation and queued drafts, retain Live admission diagnostics, and cover concurrent daemon and filesystem ownership boundaries. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): handle all writer fences and legacy publishers Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): sync standalone concurrency allowlist assertion Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: jinye <3007091+doudouOUC@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
a1cbe75cf1
|
ci: run Linux E2E with Vitest forks (#11290) (#11306)
* ci: benchmark Vitest forks against E2E shards * ci: prepare E2E fork configuration for review |
||
|
|
63578c7eb3
|
fix(ci): reap leaked E2E sandbox containers (#11264)
* fix(ci): reap job-owned E2E containers * fix(ci): tolerate containers exiting during cleanup |
||
|
|
5014a091ab
|
chore(release): bump node repl to 0.1.3 (#11299) | ||
|
|
5ec29581a5
|
ci: give every workspace the shared-pool test timeout, not just five (#10915)
* ci: give every workspace the shared-pool test timeout, not just five `npm run test:ci --workspaces` runs 22 vitest projects and they all land on the same shared ECS pool, where an identical suite takes about 5x longer depending only on which host it draws (#10490). Five of those configs were given a raised ceiling there, one at a time, as each package's flakiness became somebody's problem: cli, core, acp-bridge, web-shell and vscode-ide-companion. Three more carry a generous flat value (node-repl 60s, sdk-typescript E2E_TIMEOUT_MINUTES, scripts/tests 90s). The remaining fifteen were still on vitest's 5000 ms default: all eleven channels packages, audio-capture, chrome-extension, webui and both external-context integrations. That is 99 test files and roughly 3170 cases whose only timeout on a contended host was 5s. The workaround pattern was already visible in the tree — four tests had been hand-patched past the default individually, each in its own commit: integrations/external-context/src/auto-recall.test.ts 12s integrations/external-context/src/provider-extension-local.ts 20s, 30s packages/channels/base/src/ChannelBase.test.ts 8s Every one of those is a case that needed more than 5s, found the hard way. This raises the ceiling once, at the lane, instead of one test at a time. Uses the `? 60_000 : undefined` form that acp-bridge and web-shell already use rather than cli/core's `? 60_000 : 15_000`, so the change is confined to the pool: off it these configs keep vitest's own default and a genuine hang on a developer machine still fails in 5s, not 60s. 60_000 is the value the five already-raised configs settled on. The parity sweep in scripts/tests/unit-vitest-configs.test.ts now covers all 22 configs plus webui's function-form vite.config.ts, so a new workspace cannot quietly join the lane on the 5s default. It asserts a floor rather than equality, which lets scripts/tests (90s) and sdk-typescript (180s) keep their own numbers, and a companion case pins that the off-pool branch stays `undefined`. Verified both ways: the sweep reports 60000 for every config under an `ecs-qwen-*` stub, `undefined` off it, and fails when the ceiling is removed from any single config. No test, fixture or production code changes; a test can only get more time than before, and only on the pool. Prerequisite for quarantining the seven raw wall-clock budgets that live in these packages (#10912): channels/base and channels/dingtalk guard quadratic stalls at 1000 ms whose measured regression cost is ~4.1s, so under the old 5s ceiling there was no room to relax those bounds for a contended host at all. * fix(ci): close the shared-pool timeout pin's own coverage holes The review found four ways the pin could certify a green the lane does not actually have. Close each one in the pin itself: - packages/qwen-live defines test:ci and already runs on the pool lane, but was absent from the swept map; register it (its 60s ceiling is unconditional, so it stays out of the off-pool sample). - Run every on-pool assertion under two runner names sharing only the documented ecs-qwen- prefix, so a gate narrowed past the prefix fails instead of passing on the stub's own name. - Stub QWEN_SCRIPTS_TEST_TIMEOUT_MS and E2E_TIMEOUT_MINUTES alongside RUNNER_NAME: scripts/tests and sdk-typescript derive their ceiling from those knobs, so the pin must test the configs' logic, not the ambient environment. The scripts-suite knob test's unset arm now deletes the variable for real (stubEnv undefined) instead of relying on the ambient value being absent. - Extend the off-pool sample to every gated ternary (all fourteen this PR gates plus acp-bridge/web-shell, which carried the shape already, plus the webui function form), so a flat 60s in any of them fails. Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com> Patrol-Run: qwen-pr-closeout/jmtlz4ubdgq * fix(ci): pin the unset-RUNNER_NAME state and correct the pin's comment Add an unset-state arm to the shared-pool timeout pin: the off-pool test only stubbed a foreign RUNNER_NAME, so a ternary treating a missing variable as pool (`?? true`, `=== undefined || startsWith(...)`) passed every test while developer machines — which run with RUNNER_NAME unset — silently moved to the 60s ceiling. The new arm deletes the variable via `vi.stubEnv(..., undefined)` and samples every gated ternary plus the webui function form. Mutation check: channels/base mutated to `?? true` fails only the new arm; restored, the suite is green. Reword the comment above configModules: the pin does not replace the hand-patched per-test timeouts (auto-recall 12s, provider-extension-local 20s/30s, ChannelBase 8s). A per-test timeout outranks the config-level field and stays load-bearing off the pool, so the pin guards the config-level ceiling only. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtme4yqxhl * test(ci): pin the shared-pool guard's own coverage instead of sampling it Three holes in the guard added for the #10490 flake class, all in the guard rather than in any config: cli and core were kept out of the off-pool sample on the stated grounds that they pin a lower value, but nothing asserted that value — flattening either ternary to the pool number passed the whole file, and a real hang off CI would have surfaced at 60s instead of 15s. Both off-pool arms now assert 15s. Those two arms also carried verbatim copies of the same 16-entry sample, so a newly gated workspace had to be registered in three places, and missing one left it with a pool floor but no off-pool assertion. One shared list and one helper now back both arms. configModules was hand-maintained with nothing cross-checking it against the workspace set, while `npm run test:ci --workspaces` runs whatever the root glob resolves — so a new workspace with a test:ci script could join the lane on vitest's 5s default with the guard green. The map is now checked against the repo's own workspace resolver, one-directionally. * refactor(ci): share the test:ci workspace selector between both suites The shared-pool timeout cross-check re-implemented the test:ci workspace selector that release-workflow.test.js already carried as getTestCiWorkspaces: same getWorkspacePackageJsonPaths root glob, same scripts?.['test:ci'] filter. Two copies of the selection could drift and let the two suites pin different workspace sets. Extract the selection once into scripts/workspaces.js as getTestCiWorkspacePaths(root) and consume it from both tests; the release-workflow site keeps only a pair adapter attaching the parsed package.json its assertions read. Pure extraction with no behavior change: both selectors resolved the same 23 workspaces before and after. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtmxfessij * refactor(ci): rename selector to getTestCiWorkspacePackageJsonPaths The selector returns workspace package.json paths, so carry the same PackageJson qualifier as its sibling getWorkspacePackageJsonPaths instead of making every caller privately know the entries end in /package.json. Updates both consumers added in this PR. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtnaadh9j8 * test(scripts): pin the test:ci workspace selector directly Every consumer derives its workspace set from the selector, so a filter mutation that drops a subset stays invisible to all of them. Add an exact-set fixture pin next to the getWorkspacePackageJsonPaths tests, with the with-ci workspace outside packages/ to mirror the integrations/* layout. Verified red against both an over-inclusion mutant (filter -> true) and a partial-drop mutant. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtnaadh9j8 * test(scripts): make the selector pin and off-pool arms witness-bearing Addresses the two low-severity coverage findings from the sandboxed verify report (comment 5548150043), both measured open at this head: - Finding A: the exact-set pin mirrored only the flat integrations/* layout, so a filter mutation dropping the nested packages/channels/* subtree stayed invisible to all three selector consumers (mutant S3 survived 105/105). Mirror the nested layout in the fixture too: explicit packages/channels/* entries in the root workspaces list (packages/* does not match nested directories), one member with a test:ci script and one without. S3 now fails the pin, naming it; S1/S2/S4 stay caught. - Finding B: OFF_POOL_DEFAULT and OFF_POOL_PINNED are hand-maintained and nothing cross-checked them, so dropping a name silently removed its off-pool assertion and a flattened ternary still passed the on-pool floor (mutants G5/G6 survived). Add a partition- exhaustiveness pin: every configModules entry must sit in exactly one off-pool arm and every arm entry must exist in the map. Both mutants now fail it. No production logic changed; pins and fixtures only. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtnn5c4cjw --------- Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
dd5982aa23
|
ci: enrich deferred-findings tracking issues with PR context and assignment (#11080)
* ci: enrich deferred-findings tracking issues with PR context and assignment
The autofix loop's deferred-findings upsert files one tracking issue per
PR for verified review findings whose fixes lie outside that PR's
footprint. The issue it created carried a constant title, a bare
boilerplate body, and no assignee, so a maintainer looking at the issue
could not tell what it was about or who should follow up — over the past
week that produced ~100 issues that read as scattered noise.
The create path now fetches the PR once and makes the issue
self-describing: the title carries the PR title, the body names the PR
context (number, title, author), cc's the author, and every rc bullet
deep-links to its original review comment. The issue is assigned to the
PR author at creation; external contributors are not assignable, so the
create retries once without the assignment rather than losing the
findings, and the cc in the body is what actually reaches them. All of
it is best-effort: a failed PR fetch degrades to the previous bare
title and no assignee — metadata must never lose findings.
The title is also the lookup's fallback anchor when a maintainer edit
drops the body marker. It now accepts both the bare form and the
enriched "base: <PR title>" form, guarded by the colon so PR #5's base
never prefix-matches PR #50's issue. The rc link suffix is safe on
identity because rc dedup is id-anchored; rv/ic bullets deliberately get
no link — their cross-round identity IS the rendered line, and a suffix
would render every already-persisted rv/ic item as new, a one-time
duplicate wave over the existing tracking issues.
The PR title is API-derived content published under the bot identity,
so it receives the same mention/comment-opener neutralization as the
reason rendering (canonical spelling, which moves the script's escape
census pin from 1 to 2), and its cap slice happens in jq so a CJK title
cannot be byte-cut under a C locale.
Tests: the recording gh stub learns the pulls endpoint and an
assignment-failure injection; new cases cover the degraded create, bot
authors, the assign fallback, enriched-title adoption, the number-prefix
collision, and title neutralization. The upsert test spawns ~70
subprocesses, so it gets an explicit 30s bound like its spawn-heavy
neighbors instead of flaking on exec-scanning hosts.
* fix(ci): create the deferred-findings issue once, assign separately
Address review round R1 on the deferred-findings tracking issue.
R1-1 (Critical): the assignee-less retry re-POSTed the non-idempotent
issue create. GitHub silently drops assignees on POST
/repos/{owner}/{repo}/issues for users without push access, so for the
external contributors the retry was named for, the first create returns
201 and the retry is dead code. The failures that DO reach it are the
ambiguous transport ones (connection reset, gateway 502, a read timeout
after the server already committed), where the re-POST mints a second
tracking issue carrying the same marker: the next round's newest-first
lookup adopts the newer one and the first is orphaned forever, publicly
duplicating every finding while the round logs clean success. That is
against this file's own rule that creating a duplicate is worse than
deferring persistence one round. Create once, unconditionally, then
assign best-effort with a separate idempotent call whose failure only
warns — the body's cc mention is what actually reaches an external
contributor.
R1-7 collapses by construction: the create call is now spelled once
instead of three times, and the assignability condition is evaluated once
(ASSIGNABLE, set alongside CC) instead of twice.
R1-2 (Critical): dropped the re-added per-test 30s timeout on this file's
heaviest case. scripts/tests/vitest.config.ts configures testTimeout
90_000 precisely because this case (~14s idle, and heavier in this diff)
exhausted 30s on contended release runners, and
|
||
|
|
21e1133514
|
feat(ci): surface the resolved model and CLI version in autofix round reports (#11124)
* feat(ci): surface the resolved model and CLI version in autofix round reports The round-report footers could only name the REQUESTED model — the repo variable, or 'default' when unset — so diagnosing a round (which model produced it, on which CLI build) meant downloading the run artifact and digging through agent.log. The CLI already reports both in its stream-json init event; run-agent.mjs now records them to agent-model on every settle path — a crashed or timed-out round is exactly when the footer needs to name the model that died — and the three report footers (push/no-op, handoff, issue-lane e2e report) prefer those values over the configured one, falling back to it when the file never landed. WORKDIR is agent-writable, so both read sites allowlist the values (the version's allowlist is the review footer's FOOTER_VERSION_RE shape) and cap their length before a bot-authored comment renders them; a forged file degrades to inert mojibake, never markup or a control marker. The repair step deletes the sentinel with its siblings so run 2 cannot inherit run 1's. The eval-marker grammar is untouched — six jq scan regexes anchor on it, and the footer rides the same comment, so per-round attribution stays greppable. * fix(ci): harden the agent-model sentinel reads and gate them with executed tests Addresses the round-1 review on #11124. The sentinel reads ran under -eo pipefail in PAT-bearing steps but assumed a readable regular file: a planted directory passed the old size test and killed the step at its preamble (losing the push and the round report), a planted FIFO wedged it until the job timeout, and a planted symlink pointed the read at an arbitrary host file whose head rode into a public comment. All three sites now take a regular file only, never a symlink, read through bounded head -c prefixes so sed cannot be made to buffer a forged huge line, and end every pipeline in || true so any read failure degrades to the configured model instead of aborting the step. The repair step's sentinel deletion list gains the same tolerance: rm -f exits non-zero on a planted directory, which previously aborted the step AFTER attempted=true was written, silently costing the round its repair. Tests: the read block is now replayed as REAL bash under GitHub's own flags against benign, hostile, binary, directory, symlink and FIFO fixtures — gating the preference ordering (a default assignment moved below it reverts every footer to the requested model) and the fail-safe shapes together; the writer's two-line invariant (newline flattening, length caps) is exercised through a newline-bearing oversized init event; and the repair deletion and artifact-dump lines gain the pins the review found missing. * test(ci): gate all three footer-block copies and both writer invariants Round-2 review on #11124 measured two surviving mutants. The executed replay extracted only the canonical script copy, so reordering the default assignment in either workflow copy — a plausible slip when hand-editing one of three duplicates in a 6.9k-line file — stayed green while that copy's footer silently reverted to the requested model; the replay now also asserts each workflow copy's CODE lines equal the canonical block's, modulo the ten-space indent and the shorter comment, in the same order. The flattening fixture parked the model's newline at index 250, past the writer's 200-char cap, so dropping the flatten while keeping the cap produced identical output and survived; the test now runs two fixtures — a newline inside both caps to kill split-drop mutants on both fields, and values exceeding both caps to kill slice-drop mutants on both fields. Measured: both workflow-copy reorders and all four writer mutants (split/slice × model/version) now fail the suite; the intact tree stays green (230 passed, 4 skipped). * fix(ci): align the agent-model writer caps with the published reader caps The sandboxed verification on #11124 measured the gap this leaves: the runner accepted 200 chars of model and 80 of version while every read site publishes at most 100/40, so a legitimate value between the two bounds was written in full, silently clipped on read, and posted as accurate attribution — a 154-char model published as 100, exit 0, no warning anywhere. The writer now caps at exactly the published bounds, and a contract test pins the two pairs equal, plus the head -c windows wide enough to cover the largest sentinel the writer can emit, so neither side can drift alone. The caps fixture moves to the new bounds in the same commit. Measured: writer-cap, reader-cap, slice-drop and window-shrink mutants all fail the suite (4/4 killed); intact tree green (231 passed, 4 skipped). * test(ci): bound the agent-model head windows in bytes, per read Round-3 review on #11124: the window guard computed the writer's maximum sentinel as caps + 2 = 142 bytes, but the caps count UTF-16 code units while writeFileSync encodes UTF-8 and head -c bounds bytes — a BMP code unit occupies up to 3 bytes (surrogate pairs fold to 2 per unit, lone surrogates encode as the 3-byte replacement char), so the real maximum is 422 bytes and the assertion was ~3x looser than the invariant its own comment claimed. Shrinking the model window from 400 to anything in [142, 300] kept the suite green while head -c cut line 1 mid-value and the footer published a truncated model prefix — the exact failure this test exists to prevent. Measured: that mutant passed before this change and fails after (300 < 301), and 400 -> 301 stays green on the inclusive bound. Each window is now bounded against the line it actually reads — the model read needs line 1 plus its LF, the version read needs the whole two-line file for sed -n '2p' to see line 2 — and the bound stays split per read because applying the 422-byte whole-file figure to the model window would fail it (400) for a line it never reads. * fix(ci): open the agent-model sentinel non-following and non-blocking Round-4 review on #11124 (Critical): the sentinel write was a plain O_WRONLY|O_CREAT|O_TRUNC open of an agent-writable path, executed in finish() AFTER every watchdog has been disarmed. A FIFO planted at the path — creatable from inside the sandbox, which bind-mounts the host tmpdir rw as the same uid — blocks the open forever with no timer left to kill it: measured, the runner never returned, burning the address step's 130-minute cap and, on a wedged repair pass, enough of the job budget to cancel the always() reporters into a silent round. A planted symlink was worse than a hang: O_TRUNC followed it and replaced a host file's content while the round exited 0 and reported success. The write now opens O_WRONLY|O_CREAT|O_TRUNC|(O_NOFOLLOW ?? 0)| O_NONBLOCK — af-053's rule for agent-writable paths, the shape run-ledger.ts's noFollow writes use: the symlink becomes ELOOP, the readerless FIFO ENXIO, a directory EISDIR, all landing in the existing best-effort catch, so the sentinel stays optional and no write failure escapes into the close handler. O_NOFOLLOW is undefined on Windows and folds to 0; the sentinel contract (two newline-terminated lines within the 100/40 caps) is untouched and its pins stay green. Tests: FIFO and symlink plant arms beside the writer tests. The FIFO arm bounds its own spawn (45 s SIGTERM) instead of the shared unbounded helper — measured, a flags-removed runner blocks the vitest worker thread synchronously, the 90 s suite testTimeout cannot fire on it, and the file hangs past 10 minutes instead of going red; the bounded spawn turns the same mutant deterministically red at ~45 s. Mutation battery: all flags dropped / O_NOFOLLOW dropped / O_NONBLOCK dropped — 3/3 killed; intact tree green (240 passed / 4 skipped). |
||
|
|
5b470befa0
|
ci: make the Prettier lane a real gate (#11117)
* chore: format the 35 files CI had been silently rewriting
`scripts/lint.js --prettier` and the release workflow's `Format Project`
step both run `prettier --write`, which reformats in place and exits 0.
Neither is followed by a dirty-tree check, so the rewrites are discarded
when the job ends and these files have stayed unformatted on `main`.
This is the output of `prettier --write` over exactly those files, so the
next commit can turn the check into a real gate without failing on a
backlog. No behaviour change: every hunk is whitespace, quoting or
wrapping.
Verified with prettier 3.6.1, the version pinned in package-lock.json.
`prettier --check .` over the whole repo reports these 35 and nothing
else.
Refs #11109
* fix(ci): make the Prettier lane a real gate
`runPrettier()` ran `prettier --write .`. Prettier exits 0 after
reformatting, and no caller checks the tree afterwards, so the lane
passed on unformatted code and CI threw the rewrites away — the 35 files
in the previous commit are what it had been silently fixing and
discarding.
ESLint does not cover the gap: eslint.config.js loads
eslint-config-prettier, which disables formatting rules rather than
enforcing them. With this change the only enforcement point outside the
husky pre-commit hook actually enforces something.
`npm run lint:all` now reports unformatted files instead of fixing them;
`npm run format` is the fix. Prettier's own failure message says so.
Refs #11109
* fix(release): drop Format Project, it writes and discards
The step ran `npm run format` (`prettier --write .`) and nothing in
release.yml inspected the tree afterwards — no `git diff --exit-code`, no
`git status --porcelain` anywhere in the file. The rewrites died with the
job, so the only failure it could produce was Prettier crashing on an
unparsable file, which the Run Lint step reports anyway.
It cost 13m58s on a quiet host and 20m45s on a contended one, against a
30-minute cap that `quality_static` hit twice today (runs 33957952281 and
33963757913, both cancelled mid-lint with nothing failing). The remaining
steps are `npm ci` (1m44s) and `lint:ci` (~8m20s), so the job now has real
headroom instead of finishing 24 seconds over the wall.
Formatting is now gated where it belongs: the previous commit makes the
PR-CI Prettier lane `--check`.
Refs #11109
* fix(ci): keep prettier --check out of .qwen/ and the pre-paint theme script
Two things the `--write` → `--check` flip broke that `--write` had hidden.
`.qwen/` is gitignored as `.qwen/*` and holds machine-written state —
pending-skills/, auto-skill-*/, learned-skill-*/,
computer-use/installed.json. Prettier does not read .gitignore, so it was
walking in and silently reformatting whatever a contributor had there;
under `--check` that turns into a failed `npm run lint:all` on files git
does not even track. CI never saw it (fresh checkout, tracked files only),
which is exactly why it went unnoticed. Adding `.qwen/` to
.prettierignore also drops the one tracked file under it —
skills/find-simplifications/SKILL.md, force-added past .gitignore — from
the previous formatting commit, since a directory the gate should not
police should not be reformatted for it either.
The inline theme script in web-shell's index.html is restored to its
original single line behind `<!-- prettier-ignore -->`. It is a
render-blocking script in <head> that applies the stored theme before
first paint; the expanded form ships ~600 more bytes on the critical path
and buys no readability the comment above it does not already give. A
change about CI hygiene should not be reshaping that.
Refs #11109
* fix(ci): keep prettier --check out of vendored packages/mobile-mcp
eslint.config.js has ignored `packages/mobile-mcp/**` since it landed —
"vendored mobile-next/mobile-mcp; has own eslint config" — and
.prettierignore already carries its sibling `packages/cua-driver/`, listed
one line above with the same rationale. mobile-mcp was simply missed.
`--write` hid that: it reformatted the vendored package's own eslint
config to our house style every run and threw the result away. The
formatting commit in this PR would have made that permanent, widening the
diff against upstream on the next sync for a package this repo has already
decided not to lint. That file is reverted here and the directory is
ignored.
Same class as the .qwen/ fix in the previous commit: the gate reaching
somewhere the repo had already decided it should not.
Refs #11109
* fix(ci): mirror eslint's remaining global ignores in .prettierignore
`--check` makes .prettierignore load-bearing in a way `--write` never was:
under `--write` a file prettier should not touch was reformatted and the
result discarded, so nobody noticed; under `--check` it fails the lane.
These three are the rest of eslint.config.js's global ignores, added for
the reasons it already states. `scripts/codemod/fixtures/` is the one that
matters: those files are "intentionally non-idiomatic ink input/output",
so the obvious response to a `--check` failure there — reformat to house
style — would corrupt the fixtures the codemod tests assert against. The
trap is not live today (all three currently satisfy prettier), which is
precisely why it is worth closing before someone meets it.
Refs #11109
* chore: format converter.test.ts, which drifted onto main since this branch
This branch merged main 32 commits after its base and `prettier --check .`
found one new offender. It is the same gate failing the same way: nothing
enforces formatting, so `--write` reformatted this file on every CI run
since it landed and threw the result away.
Worth noting as evidence rather than noise — the backlog this PR fixes was
not a one-off from before someone got careless; it accrues continuously,
and it accrued again while the PR was open.
Refs #11109
* Revert "fix(release): drop Format Project, it writes and discards"
This reverts the release.yml half of this PR. #11195 removes that step on
its own, in one file and four lines, and two PRs racing on the same
deletion helps nobody.
The reasoning and the evidence are unchanged and now live on #11195: the
dry-run release dispatched from this branch with the step removed
(34022919438) went 18/18 green in 65m43s, quality_static finished in
7m00s against main's 8m57s, and Run Lint was 4m52s vs 4m43s — nothing
downstream ever read what the step produced.
Dropping release.yml from this PR also widens who can review it. It was
the only file here owned by @pomelo-nwu and @wenshao alone; what remains
under CODEOWNERS is /packages/core/, whose owner list is six people. The
Prettier gate no longer waits on the two maintainers who must sign off on
release workflow changes.
Refs #11109, #11195
* fix(ci): make the gate resolve the file set npm run format resolves
Two review findings, one root cause: the gate was checking a different set
of files than the tree it is supposed to police.
**The `.qwen/` ignore rested on a false premise (R1-1, @chiga0).** Prettier
3.6's `--ignore-path` already defaults to `[.gitignore, .prettierignore]`,
so every machine-written path that entry's comment named —
`pending-skills/`, `auto-skill-*/`, `learned-skill-*/` — was outside the
gate before it via `.gitignore:32`. What the entry actually excluded is the
corpus `.gitignore` deliberately re-includes: `.qwen/commands/**`,
`skills/**`, `agents/**`, `team-memory/**`, `review-context.json` — 57
tracked, hand-authored files. The gate then passed because the surface had
been narrowed, not because the tree was formatted, and contributors editing
those docs could ship them unformatted with both enforcement points silent.
The entry is dropped and the first commit's formatting of
`skills/find-simplifications/SKILL.md`, reverted alongside it, is re-landed.
**The gate and the remedy used different CLIs (R1-2).** `runPrettier()` ran
the classic `prettier --check .` while `npm run format` runs
`prettier --experimental-cli --write .`. The classic CLI reads only the
repo-root `.gitignore`; the experimental one also honours nested ones, and
this repo has 23 of those. Verified on a scratch tree: given
`pkg/.gitignore` containing `lib/`, classic reports `pkg/lib/nested.js` and
experimental skips it. So the lane could go red on a locally built package's
output that `npm run format` provably cannot clear. The gate now uses the
same CLI as the remedy it names.
Refs #11109
* fix(ci): let the pre-commit hook cover what the gate now checks, and pin it
**The hook was narrower than the gate (R1-5).** `lint-staged` formatted only
`*.{js,jsx,ts,tsx}` and `*.{json,md}`, but the gate checks every extension
Prettier has a parser for. Of the 35 files the first commit formatted, 29
(83%) are in classes the hook never touched — 13 `.yml`, 12 `.mjs`, 3
`.css`, 1 `.html`. Left alone, the backlog this PR just cleared would
re-accumulate, and now as a red CI step on someone else's PR rather than as
silent drift. `.mjs`, `.cjs`, `.yml`, `.yaml`, `.css` and `.html` join the
Prettier-only group; ESLint's globs are unchanged, since the gap is about
formatting coverage, not lint coverage.
**Nothing pinned the flag (R1-4).** `scripts/tests/lint.test.js` exercised
only the linter-directory helpers, and `ci-platform-lanes.test.js` asserts
that the `Run Prettier` step exists and is profile-gated, not what it runs.
A refactor, or a well-meant "make lint auto-fix again", could restore the
silent pass with every suite green. Two assertions now pin it: that
`runPrettier()` checks rather than writes, and that it uses the same
Prettier CLI as `npm run format`, so the two cannot drift apart again the
way R1-2 found them.
Refs #11109
* docs: correct the instructions and comments the --check flip falsified
**Three tracked instructions to automation agents now say the opposite of
what the code does (R1-9), and one is fed to a CI lane today** —
`.github/workflows/qwen-triage.yml` re-pins `verify-pr` from the base commit
and hands it to the verifying agent.
- `verify-pr/SKILL.md` told agents never to run `scripts/lint.js` with no
arguments because it would rewrite the working tree under their A/B
harness. That is no longer why. The prohibition is kept — it is still
right — but re-grounded in what the no-arg form actually does now
(`setupLinters()` wipes the linter temp dir and re-downloads three pinned
binaries), and `--prettier` joins the enumerated non-mutating checks it
was the only flag missing from.
- `find-simplifications/SKILL.md` and its `references/land.md` told the
simplification agent that a formatting diff "carries zero signal" because
CI runs `--write`. An agent believing that would skip `npm run format`
after a deletion changed line shape — on the very extensions the hook does
not cover — and go red on formatting it was told could not matter.
- `update-ecs-runner-qwen-workflow.test.js`'s comment justified parsing two
matrix layouts by "CI runs `prettier --write .` before this suite". CI no
longer rewrites anything; the two layouts are still worth parsing, so the
comment is re-grounded in the checked-in form versus older branches.
**The index.html rationale overstated its own number (R1-8, @chiga0).** It
claimed the expanded script ships "~600 more bytes"; measured against this
repo's Prettier 3.6.1 and its `tabWidth: 2`, it is +258 source and +8
gzipped here, +287/+20 by the reviewer's method — either way roughly half
what was claimed, and "~600" is what the same expansion costs at
`--tab-width 4`. Since the prose ships verbatim inside the built
`index.html`, the fix is to drop the contested figure and keep the comment
short. The `<!-- prettier-ignore -->` guard itself stays: it is load-bearing.
Refs #11109
* chore: format classify-profile.test.mjs, drifted again after commit 1
Third instance in this PR's lifetime, and the sharpest: commit `
|
||
|
|
f5ce3ea8fd
|
fix(ci): give web-shell E2E Smoke the ECS timeout allowance its siblings have (#11212)
Four jobs route to the self-hosted pool through the same `ubuntu_runner` expression. Three of them scale `timeout-minutes` for `ecs-qwen` — test 120/60, lint_and_static 90/45, integration_no_ak 60/30 — and `lint_and_static` carries a comment explaining why: the pool's contention, with hosted runners kept on the tighter ceiling so a genuine hang there does not burn the ECS allowance. `web_shell_e2e_smoke` was the fourth, and the only one still priced flat at 20. It lands on the same hosts under the same contention and got none of that allowance, so it was beheaded twice on a single commit that changes nothing it tests: on hk3-9 `npm ci` alone ran 19m46s of the 20 and never finished, and on hk4-23 install took 10m33s against 4m53s on hk5-2, leaving the browser smoke 8m15s before the job died against 2m29s warm. Both phases running 2-3x is the pool rather than the dependencies. A cold npm cache would explain a slow install but cannot explain the smoke phase, which is a vite dev server plus a headless browser and touches no cache at all. The healthy path is 8-10 minutes, so a flat 20 only ever tolerated a 2x slowdown — exactly what the contended hosts deliver, and the per-host medians already measured in #10879 (hk3 34.6 min, hk4 27.5, hk5 12.3) say which attempts were going to die before they started. 40 gives the ECS path the same 2x its siblings use and covers the measured worst case with room for a 3x; hosted keeps 20 for the reason lint's copy gives. The lane was not covered by the guard that exists for exactly this regression. `ci-platform-lanes.test.js` already evaluates the real timeout expression for both routings so that, in its own words, "a regression to an unconditional ceiling fails here instead of reading as a passing constant" — but it pinned only the three siblings. Add this one on both routings. Three mutants applied, all killed by exactly the intended test: reverting to the flat 20, reducing the ECS allowance to 30 (under the measured worst case), and raising it to a flat 120, which the hosted-ceiling test also catches. |
||
|
|
92a8a8d179
|
fix(skills): register frontmatter hooks on the /<skill-name> path (#11068)
* fix(skills): register frontmatter hooks on the /<skill-name> path A skill's frontmatter `hooks:` were registered only by `SkillTool`, the model-invocation path. The user-invocation path — `SkillCommandLoader` and `BundledSkillLoader`, reached by typing `/<skill-name>` — injected the skill body and granted its `allowedTools`, but never registered its hooks. A `PreToolUse` gate is the one mechanism that enforces a skill's rule regardless of what the model decides to do, so users reach for it exactly where a model mistake is expensive. Dropping it on the slash-command path made that gate fail open, silently: nothing in the UI or the logs indicated that a declared hook had been skipped. Extract the side-effect application into `applySkillSideEffects` in `skill-utils.ts`, alongside the existing `applySkillAllowedTools` and `canApplySkillSideEffects`, and route all three call sites through it. Both underlying registrations already dedup, and project skills stay trust-gated on every path. Bundling the two side effects behind one helper also stops a future invocation path from applying half of them. Documents the `hooks:` frontmatter in the Skills guide, which had no user documentation for it, making the deterministic path hard to discover. Tests: unit regressions on both loaders (the two positive cases fail without this fix), plus an end-to-end parity test driving the real interactive CLI over a PTY, asserting the gate blocks the shell call on both paths. Fixes #11067 * fix(skills): address review — sandbox reachability, CI coverage, resume path R1-1: the parity test bound the fake server to loopback and hand-stubbed NO_PROXY, so under the docker/podman sandbox legs the containerized CLI could not reach it and both cases failed. Use `fakeServerHostOptions()` and `applyContainerSandboxNoProxy()` like every sibling fake-server test; both are no-ops outside a container sandbox. R1-2: the test sat outside every suite that gates a PR, so a regression would only surface post-merge. Add it to `test:integration:no-ak:sandbox:none` (it needs no credentials) and to the pinned list in the CI wiring guard test. R1-4: session hooks are in-memory only, so `--continue` / `--resume` restored a skill's body without its side effects — and the dedup guard answers "already loaded in context", so nothing prompted the re-invocation that would have re-registered them. The gate silently stopped firing after a resume: the same fail-open shape this PR fixes. `restoreLoadedSkillsFromHistory` now re-applies side effects per restored skill, through `applySkillSideEffects` so the folder-trust gate still applies. Documentation says so. R1-5: restore the trace the refactor dropped. Sessions that disable hooks (`disableAllHooks`, safe mode, bare mode, ACP `skipHooks`) build no hook system, so a skill's gate is discarded — previously with a debug line naming `hasHookSystem: false`, now silently. R1-6: cover the guard branch. It is reachable in hooks-disabled sessions, and deleting it would throw on `undefined` and crash every skill invocation there, with nothing going red. R1-3: `[hook](hooks)` 404s on GitHub — the target is `hooks.md`, and no docs-site generator resolves extensionless links. Each fix was verified to red when the fix is removed. * fix(skills): don't re-arm disabled skills on resume; correct the docs claim Both problems were introduced by the resume repair added in the previous round, and both have the shape this PR exists to close: an operator believes a gate is armed when it is not. R2-1: `restoreLoadedSkillsFromHistory` applied side effects for every matched skill with no enabledness check. `applySkillSideEffects` gates only folder trust, and the candidate map comes from `getCachedSkills()`, which does not consult `skills.disabled` — so invoke-skill → disable → `--continue` reinstated the skill's `allowedTools` auto-approval rules and re-registered its hooks. Both live paths refuse a disabled skill before applying anything (`executeDisabledSkill`; the loader's disabled branch), so restore was the only way back in. Gate the side-effect call on `isSkillEnabled`; bookkeeping of `loadedSkillNames`/`loadedSkillContents` stays unconditional, since the body is in the restored context either way and the dedup guard must know. R1-5: the docs bullet claimed resume re-registers hooks on both invocation paths. It does not. The restore matcher requires a `ToolNames.SKILL` functionCall/functionResponse pair, which only the model path writes; the slash command submits the body as an ordinary prompt and leaves no such record, so a `/<skill-name>` skill's hooks are gone after resume while its instructions stay resident. State the truth per path, say how to re-arm the gate, and note the disabled-skill case. Making the parity claim true needs a restorable trace for the slash path — out of scope here. Tests: a probe asserting zero hook and zero allow-rule calls for a disabled skill on restore, verified to red when the enabledness gate is removed. * fix(skills): drop the resume repair, returning this PR to its original scope The resume repair added in round 1 has produced one Critical and three Suggestions across two review rounds, all in the same two files, and none of them are the bug this PR set out to fix. Both maintainers and the review bot converged on the same advice: the cluster belongs in its own change. Removing it entirely, rather than continuing to patch it, is also the honest call. The integration leg written for R3-3 showed the repair does not work in a real resumed session: the gate fired 0 times after `--continue` and the shell command actually ran, while the unit tests stayed green because they mock the hook system wholesale. Shipping a re-arm that does not re-arm would reproduce the exact fail-open this PR exists to close. `restoreLoadedSkillsFromHistory` is now byte-identical to its pre-PR form. The docs bullet takes round 1's other offered option and states the truth for the reverted behavior: session hooks are in-memory only and are not restored on resume, on either path — re-run the Skill to re-arm its gate. What remains is the change reviewed as sound: one shared side-effect helper, both loader paths routed through it, and the tests and documentation for it. The resume gap is pre-existing, now documented, and left to a follow-up. Removed: the resume re-arm and its `isSkillEnabled` / `pendingConditionalSkillNames` gates, the mismatch-branch trace, three resume unit tests, and the resume integration leg. * test(skills): close the three suggestions left open from rounds 1-2 R1-6 (fix-induced): the trace added for the silent-drop finding was `debug`, which `debugLogger` drops entirely without `QWEN_DEBUG_LOG_FILE` — quiet was asked for, invisible was not. Raise it to `warn`: control only reaches this branch for a skill that actually declares hooks (`applySkillHooks` returns early on `!skill.hooks`), so it fires exactly when a promised gate is being dropped and cannot become a steady-state warning. The docs sold the guarantee without naming the exception, so the Notes list now says hooks are not registered in sessions that disable them (`disableAllHooks`, safe mode, bare mode, an ACP client's `skipHooks`), where the body and `allowedTools` still apply but the gate does not. R2-3: the no-session-id test pinned only the hooks half, so hoisting that guard above `applySkillAllowedTools` shipped untested. It now asserts the grant still lands, matching its no-hook-system sibling. R2-2: the parity test sequenced its legs with unconditional `setTimeout` sleeps that neither shortened on a fast runner nor extended on a slow one. Every wait now polls the condition it is actually waiting on — the command echoing, the model call the submitted body produces, the gate firing, the exit prompt. Incidentally 2.4x faster locally (15.6s -> 6.6s), which is the fixed budget that was being burned. Both new assertions are load-bearing: reverting the trace to `debug` reds the no-hook-system case, and hoisting the session-id guard above the allowedTools grant reds the no-session-id case. * fix(skills): correct the trust claim, pin the hookless early return, fix the recipe All four from @wenshao's round-3 verification report. The Notes list claimed folder trust is re-checked at fire time, so revoking it mid-session silences a project Skill's hooks without a restart. Measured on the real CLI, neither direction takes effect: `Config.trustedFolder` is `private readonly`, assigned once in the constructor with no setter, so the fire-time check returns a constant for the life of a Config — and both of the CLI's own trust-change paths set `needsRestart`. The sentence was copied faithfully from a pre-existing code comment, but this PR is what promotes it into user-facing docs where a reader will act on it, which is exactly the shape of overpromise this PR exists to remove. It now says trust changes take effect on restart. The surviving mutant: deleting `if (!skill.hooks) return;` from `applySkillHooks` left the whole core suite green, while that early return is what lets the branch below it be a `warn` rather than steady-state noise — without it every hookless Skill invoked in a hooks-disabled session warns. Pinned by a case that reds when it is removed. Two fixes to the doc recipe: it is the first copy-pasteable hook example, and run verbatim with the script left at 0644 the gate fails open silently, so it now says to `chmod +x`. The example also used `matcher: Shell`, which works but is the compatibility alias; hooks.md tells new configs to prefer runtime ids, so it uses `run_shell_command`. Also fixes a soundness bug the new assertion exposed: `debugLoggerSpies` is module-scoped and was never cleared between cases, so log assertions were reading calls made by earlier tests. Cleared in `beforeEach`, which makes the previous round's two log assertions meaningful as well. * fix(skills): correct the trust claim, guard an empty hooks block, wait for command registration Round 4 review (#11068): two Criticals and two Suggestions, all four verified locally with intact/flip arms rather than by reading the diff. - docs: the trust bullet rewritten in 80c2fcb asserted unconditionally that a mid-session trust change takes effect only on restart. `isTrustedFolder()` reads the IDE context store first and falls back to the readonly `trustedFolder` field only when the store is empty, so the value is live under an IDE connection. A probe on a real Config confirms it: with `trustedFolder: true`, an `ideContextStore.set({workspaceState: {isTrusted:false}})` flips `isTrustedFolder()` to false mid-session, and clearing the store restores true. State both branches instead, and align the `applySkillHooks` / `applySkillAllowedTools` JSDoc with the same wording so the two documents stop disagreeing about one gate. - docs: bare mode was listed with the sessions where only hooks are dropped. `listSkillsAtLevel` returns [] for every level in bare mode and the tool registry returns before the Skill tool is registered, so there is no body and no allowedTools there either. Safe mode differs — discovery is limited to ['bundled'] rather than emptied — so it stays in the first group. - `applySkillHooks`: `{}` is truthy, and `parseSkillContent` assigns an empty object for `hooks: {}` and for a block whose event names are all unknown. Such a skill fell through the `!skill.hooks` guard and, with the warn level this branch now uses, emitted a per-invocation warning about dropping a gate it never declared. Widen the guard and pin the empty-object shape with a test; dropping the new operand turns that test red and nothing else. - parity test: the poll that replaced the fixed sleep waited for the typed text to echo, which lands before the skill command registry does. Submitting into that gap gets `Unknown command: /gated-skill`, undetectable downstream because the error text contains the command. Poll for the completion menu rendering the skill's own description — a signal only a registered command produces — and assert the unknown-command error never appears. Verified with the skill command loader artificially slowed by 1500ms, same bundle, only the wait predicate differing: the echo poll fails 3/3 on the user-invoked leg with `Unknown command: /gated-skill`, the registration poll passes 3/3 and absorbs the delay (4.3s vs 3.0s). Unslowed, the fix arm is 3/3 green with zero unknown-command occurrences. `test:integration:no-ak:sandbox:none` 23 files / 187 tests, core tools+hooks+skills 1333 tests, CLI skill loaders 63 tests, typecheck, eslint and prettier all clean. * fix(skills): close the six deferred findings from rounds 2-4 Every deferral recorded across rounds 2, 3 and 4, verified with intact/flip arms and probes on the real classes rather than by reading the diff. - docs recipe: `$QWEN_SKILL_ROOT` was unquoted. The command string is handed to a shell (`bash -c`), so a project path containing a space splits into two words: measured `bash: /tmp/My: No such file or directory`, exit 127, which is not 2 and therefore fails open — the same shape as #11067, in the recipe that teaches the fix. Quote it, and say why. - docs: an omitted `matcher:` on a tool event is stored as '' and compiled to `^$`, which matches no tool name. A probe on a real SessionHooksManager confirms it: two hooks registered, only the `*` one matches `run_shell_command`. The hook registers and never fires, silently. Say so, and point at `*`. - docs: hook commands run through the platform shell — bash on macOS and Linux, Git Bash on Windows when `MSYSTEM`/`TERM` say so, otherwise cmd.exe or PowerShell, where `$VAR` is not expanded and a `.sh` is not executable. `shell: bash` forces bash but resolves to whatever is on PATH, so the note stops short of promising it works on a stock Windows box. - `BundledSkillLoader`: the load-time `skills.disabled` filter cannot help a command that was already built, so a skill disabled afterwards still had its side effects applied on invocation. `SkillCommandLoader` has refused this since before this PR; mirror it. Pre-existing for `allowedTools`, but this PR widened the same call to hooks. - `packages/core/src/index.ts`: stop exporting `applySkillHooks`. It is the one entry point whose caller must apply the folder-trust gate itself, it had no consumer outside its own module, and exporting it invites the bypass that caused #11067. `applySkillSideEffects` stays exported and applies the gate. - test: the untrusted-folder warn fired on `allowedTools?.length || hooks`, but the only case used a skill carrying both, so dropping the hooks operand left all tests green. Pin a hooks-only project skill. Both new tests are discriminating: removing the bundled enabledness check reds only the new bundled case (1 failed / 28 passed), and dropping the `|| hooks` operand reds only the new skill-utils case (1 failed / 23 passed). `test:integration:no-ak:sandbox:none` 23 files / 187 tests, core tools+hooks+skills 4588 tests, CLI services 551 tests, eslint and prettier clean. The Windows shell behaviour is read from `getShellConfiguration`, not executed — no Windows host was available. |
||
|
|
703678136a
|
perf(cli): import core modules directly instead of the package root (#10957)
* perf(cli): let tests resolve core modules individually, and stop two files importing the whole package
Importing from the core package root pulls in its entire export graph — a bit
over six hundred modules — however little of it a file actually uses. In a
release run the cli workspace spent 2223s collecting modules against 1372s
running tests, and a file that imports the package root costs about 11.5s
before its first assertion where one importing a single module costs about 2s.
cli's tsconfig already maps a wildcard subpath onto core's sources, so esbuild
resolves per-module imports when it bundles. Vitest does not read tsconfig
paths, and the alias list that stands in for them named only four subpaths, so
those imports did not resolve under test at all. This adds the wildcard there.
Expressing the alias list as an ordered array is what allows a pattern entry.
The package root has to become an exact match in the process: as a string it
would also match everything beneath it and rewrite each subpath into a path
under index.ts.
Two files move to per-module imports as a first check that the mapping holds
end to end. Both were picked because nothing that depends on them replaces the
core package with a mock factory — where a test does that, the mock stops
intercepting once the code under test imports the module directly, so those
call sites need their mocks moved in the same change and are left alone here.
* fix(cli): restore the named core subpaths the previous commit dropped
The previous commit was assembled from a working copy that predated main by
several weeks, so it silently reverted this file to that older state. Four
named core subpaths added since — envVarResolver, noFollowOpen,
subSessionConstants and toolWriteOrigin — disappeared with it, and the new
wildcard then claimed those specifiers and pointed them at files that do not
exist. 257 test files failed to load as a result.
All eight named subpaths are restored and kept ahead of the wildcard, with a
comment saying why that order matters and what a contributor adding a ninth
has to do. None of the eight can be derived from its specifier, so none of
them can be folded into the pattern.
The two migrated source files are rebuilt on their current contents for the
same reason; one of them had also been reverted by a line.
* perf(cli): import core modules directly where no test mocks the package
Importing from the core package root evaluates its whole export graph — a bit
over six hundred modules — however little of it a file uses. On the release
lane the cli workspace spends more time collecting modules than running tests,
and on the main lane it now takes 84 minutes on its own, most of it collection.
These 130 files ask for named modules instead. They were chosen by checking,
for every test whose module graph reaches them, whether that test replaces the
core package with a mock: a test that swaps the package wholesale stops
intercepting once the code under test imports a module directly, and a test
that spreads the real package and overrides a few names only matters if one of
those names is what the file imports. Files with either kind of coupling are
left for a later change that moves the mocks at the same time.
Only import statements move; every other line is byte-identical.
* fix(cli): point ProviderModelConfig at the module that declares it, and wrap long imports
Two problems with the previous commit, both found by CI.
The symbol map resolved a re-exported name to the module that re-exports it
rather than the one that declares it, so `ProviderModelConfig` was asked of
`models/types` when it is declared in `providers/types`, and the build failed
to typecheck. A checker now confirms, for every generated specifier, that the
named module really does export that symbol — following its own re-exports —
and it reports one bad pair out of 492.
The formatting pass that was supposed to run over these files had silently
done nothing: invoked from the repository root against paths outside it,
Prettier skips the files and still reports success, so long import statements
went out unwrapped. Rerunning it properly reflows 47 files.
Two files are left with an over-long line Prettier would wrap, because that
line is over-long on the base commit too and the lint gate does not flag it;
reformatting it here would be unrelated noise. Every other line outside an
import statement stays byte-identical.
* perf(cli): import core modules directly in another 114 files
The same mechanical change as the previous commit, over the files a corrected
reading of the test suite showed were always safe to move.
The earlier pass classified a test as replacing the core package if the text
of such a call appeared anywhere in it, including inside a comment. One file
only mentions the pattern in a doc comment explaining why it deliberately
avoids it, and being counted as a blocker there ruled out 217 modules that
nothing actually blocks. Ignoring comments when detecting the call raises the
number of files movable without touching a single test from 141 to 260.
Every generated specifier is checked against the exports its named module
really has, following that module's own re-exports — 865 pairs here, none
wrong. Outside import statements every line is byte-identical.
* test(cli): move three barrel mocks onto the modules they actually stub
Where a test replaces the whole core package with a factory, the code under
test cannot move to per-module imports on its own: the mock would stop
intercepting and the real implementation would load instead, quietly changing
what the test exercises while leaving it green. The mock has to move in the
same commit.
These three are the cases where that is unambiguous — every name the factory
stubs is declared in one module, and the code under test imports exactly those
names. Each pair moves together onto that module.
The pattern generalises: about sixty tests each hold back one or two modules
this way, and roughly ninety more modules are held by several tests at once
and need them changed together. Establishing the shape on the clean cases
first keeps the ambiguous ones honest.
* Revert the migrated modules that any failing suite depends on
Retargeting this PR at main got the unit suite to run for the first time, and
it failed: 16 files, 127 tests, none of them failing on the jsdom PR that
shares the same base. So they are this stack's doing.
Every one is the same shape — a stub the suite installs no longer intercepts
once the code under test names a module instead of the package root. The
static analysis that picked these files models three ways of installing such a
stub and misses at least two more: a spy planted on a namespace import of the
package, and whatever six of the sixteen suites do, which it cannot parse at
all. Sharpening the heuristic further is not the answer; it was already wrong
in a way no amount of local reading would have caught.
So this restores every migrated module that any failing suite reaches, 146 of
them, and keeps the 110 that nothing failing depends on. That is blunt — some
of the 146 are certainly fine — but it is the version that can be shown to
pass, and picking the survivors apart is work for a run that is green to begin
with.
* fix(core): let consumers outside the cli package resolve a core module by path
The integration gate failed to compile against the migrated files:
error TS2307: Cannot find module '@qwen-code/qwen-code-core/utils/debugLogger.js'
error TS2307: Cannot find module '@qwen-code/qwen-code-core/utils/editor.js'
Only packages/cli maps these specifiers, through a wildcard in its own
tsconfig. The integration suite lists the eight named subpaths and no
wildcard, and the package's exports map has entries for those same eight plus
the dist and src trees — so anything resolving the normal way, this suite and
any consumer of the published package alike, cannot name a core module.
Both gaps close here: the wildcard is added to the integration suite's path
mappings, and a catch-all maps a bare module path onto the build output. The
catch-all exposes nothing new; `./dist/*` already reaches the same files.
This is the part of the change with consequences beyond the test run. The
shipped CLI is a single bundle and never resolves these specifiers at runtime,
but the package is published, and until now a migrated import was only
resolvable from inside the one workspace that happens to map it.
* fix(cli): repair the mock pairs the revert split, and keep dev on source
Three findings from review, all of them consequences of earlier steps here.
Two mock pairs were left half-migrated. In one, the mock and the code under
test both moved to the module, but the suite's own import of the same two
functions still read the package root — so the suite held the real functions
while the code held the stubs. In the other, the revert restored the code to
the package root and left the mock pointing at a module nothing imports any
more, which stubs nothing at all. The first is completed, the second put back.
The exports catch-all also changed what `npm run dev` runs. Its loader
intercepts the exact package root and nothing else, so subpath imports fall
through — and where they used to fail to resolve, they now quietly reach
compiled output from whenever the tree was last built. The loader now redirects
them to the source tree when the source file is there, leaving the named
subpath exports, whose file names do not mirror their specifiers, to resolve
as before.
* Cover the exports catch-all, and stop it hiding from the architecture rule
Three review suggestions, all about the `./*` entry this branch added to core's
exports and the things that quietly depend on it.
Nothing exercised that entry. In the repo, cli's subpath imports resolve
through tsconfig paths or vitest aliases; in the published package neither
exists and Node resolves them against `exports`. So removing the entry, or
renaming the dist root, would leave every suite green and break `qwen` on its
first core subpath import. A new check runs Node's own resolver against the
built package — deleting the entry turns it red, which is the property that
makes it worth having.
The entry also widened what the utils-layer rule has to police. It resolved
self-references by exact key only, so a deep specifier like
`.../config/storage.js` matched nothing, returned early, and reported nothing —
while resolving perfectly at runtime. It now follows Node's own order: exact
keys first, then the longest literal prefix among patterns. Fixtures cover the
wildcard idiom, exact-key precedence, and a sibling utils import that must
still be allowed.
Finally the integration suite's path mappings. The guidance comment above them
said a bare wildcard falls through to dist, which stopped being true when the
wildcard was added — nodenext substitutes `.js` for `.ts`, and the program
resolves core subpaths through it to source today. Left alone, a maintainer
following that comment would delete the wildcard as a violation and silently
re-route live imports to dist declarations. The comment now describes what the
block does, and says why the six alias-style entries cannot be folded into the
pattern. Two entries that had drifted out of sync with core's exports map,
toolWriteOrigin and envVarResolver, are restored.
* fix(cli): point the session-picker branch mock at the module it imports
Review finding R1-4b. StandaloneSessionPicker now takes getGitBranch from
@qwen-code/qwen-code-core/utils/gitUtils.js, but the colocated suite kept
mocking the package root, so the override no longer intercepted anything the
picker resolves and the factory's importActual still pulled core's whole
index into the suite's module graph. The mock now names the same specifier
the component imports, and a small pin test goes red if the pair splits
again.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtme4yqxhl
* fix(dev): preserve source-backed core entry paths
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(scripts): close core subpath verification gaps
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(scripts): gate dep-only core subpath exports in the check
The exports check collected specifiers only from packages/cli/src, but
acp-bridge and sdk-typescript are runtime dependencies of the cli whose
compiled dist keeps core subpath specifiers verbatim. Entries named only
by those packages (./goalWire, ./transcriptRecords, ./subSessionConstants)
were therefore ungated: deleting one kept every gate green while
`node packages/cli` died with ERR_MODULE_NOT_FOUND on the serve/replay
path. Extend the source scan to packages/acp-bridge/src and
packages/sdk-typescript/src so all 95 collected specifiers resolve
through the exports map. Verified: removing ./goalWire or
./transcriptRecords from packages/core/package.json now makes the check
exit 1 (entries restored after the mutation probe).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtmxfessij
* docs(harness): note terminal-capture loader is manually exercised
No CI test executes the terminal-capture harness loader's subpath
redirect; it runs manually only via `npm run test:terminal-bench`.
Record that at the redirect site and point at
scripts/check-core-subpath-exports.mjs as the CI gate for the real
resolution path, so the `named` map stays consciously in sync with the
named entries in packages/core/package.json.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtmxfessij
* fix(scripts): reject core subpath targets outside the published dist
The exports check validated resolved targets against the repo tree, but
core's exports map also carries "./src/*": "./src/*" while package.json
publishes only dist, vendor and scripts/postinstall.js. A specifier
routed through that entry resolved to a real in-repo packages/core/src
file, passed the bare existsSync and exited 0 even though the published
artifact ships nothing — the installed CLI would die at startup with
ERR_MODULE_NOT_FOUND while the gate stayed green. Require the resolved
target to lie inside packages/core/dist/, where every legitimate
runtime specifier lands.
Add scripts/tests/check-core-subpath-exports.test.js: a fixture-tree
suite (per the scripts/tests convention) that runs a copy of the real
script in a temp workspace. It pins the dep-only scan from
|
||
|
|
133a1a39de
|
ci: skip product tests for ECS updater maintenance (#10548)
* ci: classify ECS updater maintenance as CI-only Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): harden CI-only classifier trust Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci: record ci.yml growth in the size baseline The ci-only-profile wiring added real workflow content; combined with prior main growth this pushed ci.yml 4853 bytes past its recorded 98043, over the 4096 allowance. Bump the ratchet to the current 102896 so the check reflects the reviewed growth. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): gate the classifier trust test to the Linux lane The fail-closed trust test drives the workflow step through `bash -c` with a Unix ':'-joined PATH and an extensionless shebang `gh` stub, which the Windows lane cannot express. Gate it the same way this file's other bash-driven cases already do, keeping the Linux lane as the authoritative coverage (scripts/tests/vitest.config.ts). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci: log the reason behind the classifier trust verdict The permission lookup discarded all diagnostics (2>/dev/null || true) and the log printed only the verdict, so an API outage, an empty permission, and a real below-write permission all produced the identical "PR classifier trusted: false" line. Record which branch made the decision and print it alongside the verdict; the can_trust_pr_classifier output and its fail-closed semantics are unchanged. Bumps the size baseline for the added lines. The trust test's helper now captures the step's stdout, and a new Linux-gated case asserts the denial reasons stay distinguishable. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): pin the remaining classifier trust reason branches The R1-3 reason logging added six per-branch reason strings, but the 'not a pull_request event' and 'PR author missing from the event payload' branches were unreachable from the tests: runClassifierTrust hardcoded GITHUB_EVENT_NAME and PR_AUTHOR, so dropping or swapping either assignment shipped with every test green. Give the helper optional eventName / prAuthor overrides and extend the reason test to assert both branches stay denied and distinguishable; the merge queue exercises the first one on every merge_group run. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): run PR classifier from trusted base Compute the CI profile once from an immutable base-SHA checkout, then pass the validated output to product jobs without executing the PR head's classifier. All missing, failed, or unknown classifications continue to fall back to full CI. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): prepare trusted classifier checkout Clean shared-runner residue before checking out the trusted base classifier and update the workflow test fixture to mirror its isolated path. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): sweep leftover trusted-ci-classifier before checkout actions/checkout keeps an existing checkout directory when its origin URL matches and runs `git checkout --detach` inside it without a core.hooksPath mitigation, so a leftover trusted-ci-classifier from an earlier classify_pr run on the shared ecs-qwen pool would execute whatever hooks residue planted there — code execution through the classifier checkout the sweep already defends. Add the path to the pre-checkout sweep loop in every ci.yml copy (the byte-identity pin keeps them in lockstep) and in qwen-code-pr-review.yml, per the documented third-residue-name contract, and pin the removal behaviorally. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmthmo4kw80 * test(ci): pin the shared-pool ownership restore step's existence The ordering assertion alone survives removing 'Restore workspace ownership': restoreIdx becomes -1 and cleanIdx > -1 still holds, while root-owned leftovers from a containerised job defeat the sweep and the checkout again — the EACCES incident class the step exists for. The step is new in classify_pr on this branch, so pin it explicitly on every shared-pool job. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmthmo4kw80 * fix(ci): pin the profile producer id and log the degraded fallback Two silent degradations in the new profile wiring: - The classify_pr job output resolves against step id ci_profile, but nothing pinned the producer step's id or its GITHUB_OUTPUT write: a rename empties the output and every consumer silently runs the full profile. Pin both on the producer step. - When classify_pr fails (continue-on-error) the consumers coerce the empty output to full and log the exact same line as a legitimate full classification. Emit a ::warning breadcrumb when the producer output is empty, on both consumer copies, and pin it. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmthmo4kw80 * test(ci): pin the profile consumers' allowlist and output write The consumer loop pinned every load-bearing line of both `Use trusted CI profile` steps except the allowlist case line and the GITHUB_OUTPUT write. If a later edit drops the echo in either consumer, steps.ci_profile.outputs.ci_profile becomes empty and every downstream profile gate mis-compares. Extend the pinning test to cover both consumer copies of those two lines (mutation-probed on all four consumer lines). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmthzj3978p * docs(ci): explain the third residue name in the review sweep comment The sweep loop grew to three names (adding trusted-ci-classifier), but the comment above it still said "those two known names". Update the count and add the explanatory paragraph for the third name, mirroring the ci.yml copy: the residue comes from ci.yml's classify_pr base-SHA checkout on the shared pool, and actions/checkout would reuse it and run git (hooks included) inside it. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmthzj3978p * fix(ci): run only dependency-free helper tests in github_ci_only lane (#10548) The github_ci_only helper step runs before any dependency install — the setup-node and `npm ci` steps are gated on the full profile — but it ran the entire HELPER_TESTS list, 8 of whose suites import the `yaml` npm package. A PR classifying into this lane (e.g. a fork PR touching only .github/workflows/update-ecs-runner-qwen.yml, which this PR admits to the fast lane) failed closed with ERR_MODULE_NOT_FOUND on a fresh runner. Split the list: the fast lane now runs HELPER_TESTS_DEP_FREE (suites whose import closure is node: builtins only); HELPER_TESTS keeps the full list for the full-profile helper step, and the merge queue's full run re-checks what the fast lane skips. A pinning test in ci-platform-lanes keeps the yaml suites out of the fast lane. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtire6cja9 * fix(ci): drop yaml-dependent helper suite from dep-free test lane web-shell-visuals-publish.test.mjs statically imports the `yaml` npm package, but HELPER_TESTS_DEP_FREE runs before any dependency install (npm ci is gated to the full profile), so the suite failed with ERR_MODULE_NOT_FOUND on fresh runners (review feedback from qqqys). Remove it from the dep-free lane only; the full-profile helper lane still runs it. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtj6easob2 * fix(ci): keep yaml-dependent visuals suite out of github_ci_only lane web-shell-visuals-publish.test.mjs statically imports `yaml`, so the dependency-free github_ci_only lane can never execute it. Listing it in GITHUB_CI_ONLY_FILES meant a PR touching only that suite got every required check green without the changed file running once, because the full-profile helper step is gated on ci_profile == 'full'. Drop the entry so such PRs keep the full profile, and pin the classification with a test assertion (verified red with the entry, green without). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtjj99h9bs * fix(ci): grant classify_pr pull-requests read scope and pin the fast-lane exclusion F1: classify_pr had no job-level permissions block, so its token carried only the workflow-level {checks: write, contents: read, statuses: write} grant. The classifier wrapper calls repos/{}/pulls/{}/files and repos/{}/pulls/{}, which 403 without pull-requests scope and fall back to full CI. Grant contents: read + pull-requests: read on the job and pin the block with an assertion in the no-ak integration test. F7: the fast-lane pinning test enumerated 8 of the 9 yaml-importing suites; add web-shell-visuals-publish.test.mjs (the suite the F6 commit excluded from HELPER_TESTS_DEP_FREE) and fix the comment count, so re-adding it to the fast lane fails the test. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtjlef91bw * ci: give the github_ci_only fast lane a single dependency-free owner lint_and_static's 'GitHub CI helper checks' step ran the full HELPER_TESTS list under the github_ci_only profile, where every dependency install in that job is gated on ci_profile == 'full' - so the 9 suites that statically import `yaml` died with ERR_MODULE_NOT_FOUND on a fresh hosted runner. This branch newly routes ECS-updater and actionlint PRs into that profile, which made the fast lane deterministically red on exactly the flow it exists to serve. Repoint that step at HELPER_TESTS_DEP_FREE and drop the duplicate copy in the `test` job: ci-platform-lanes.test.js reserves that step name for lint_and_static, so the copy also broke the shared-prelude byte-identity guard and the out-of-test pin. Select the fast lane by its gate rather than by the list it consumes, so a regression back to env.HELPER_TESTS cannot drop out of the filter. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmto8kxx1l2 * ci: keep the ECS updater workflow guard reachable from its own fast lane Routing .github/workflows/update-ecs-runner-qwen.yml into GITHUB_CI_ONLY_FILES left its guard unreachable on exactly the PRs that change it. The 21-case vitest suite under scripts/tests/ only runs under the full profile — test:scripts is gated on ci_profile == 'full', and test_macos / test_windows fire only on merge_group|schedule|workflow_dispatch — while the dependency-free node:test replacement was a strict 7-case subset. Renaming the matrix job name template, which breaks the reporter script startswith(poolPrefix) filter, or dropping the issues:write permission from report_failure, merged with every check green. Port the remaining 14 cases into the dep-free suite behind the same win32/jq capability gate the bash replays already used, and delete the vitest copy so the two cannot drift. The suite imports node: builtins only, so it stays inside the HELPER_TESTS_DEP_FREE contract. Verified locally: 21/21 pass, 0 skipped; renaming the matrix job name template reddens "filters the run jobs by the prefix the matrix job actually uses", and dropping issues:write reddens "reports a failed fleet update only when a pool actually failed". Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmto8kxx1l2 * style: apply prettier to the ported ECS updater guard suite The ported assertions were hand-converted from vitest and drifted from the repo Prettier style, which the full-profile Run Prettier step would have flagged on this PR. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmto8kxx1l2 --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
49e3ef6269
|
perf(cli): let tests resolve core modules individually (#10917)
* perf(cli): let tests resolve core modules individually, and stop two files importing the whole package Importing from the core package root pulls in its entire export graph — a bit over six hundred modules — however little of it a file actually uses. In a release run the cli workspace spent 2223s collecting modules against 1372s running tests, and a file that imports the package root costs about 11.5s before its first assertion where one importing a single module costs about 2s. cli's tsconfig already maps a wildcard subpath onto core's sources, so esbuild resolves per-module imports when it bundles. Vitest does not read tsconfig paths, and the alias list that stands in for them named only four subpaths, so those imports did not resolve under test at all. This adds the wildcard there. Expressing the alias list as an ordered array is what allows a pattern entry. The package root has to become an exact match in the process: as a string it would also match everything beneath it and rewrite each subpath into a path under index.ts. Two files move to per-module imports as a first check that the mapping holds end to end. Both were picked because nothing that depends on them replaces the core package with a mock factory — where a test does that, the mock stops intercepting once the code under test imports the module directly, so those call sites need their mocks moved in the same change and are left alone here. * fix(cli): restore the named core subpaths the previous commit dropped The previous commit was assembled from a working copy that predated main by several weeks, so it silently reverted this file to that older state. Four named core subpaths added since — envVarResolver, noFollowOpen, subSessionConstants and toolWriteOrigin — disappeared with it, and the new wildcard then claimed those specifiers and pointed them at files that do not exist. 257 test files failed to load as a result. All eight named subpaths are restored and kept ahead of the wildcard, with a comment saying why that order matters and what a contributor adding a ninth has to do. None of the eight can be derived from its specifier, so none of them can be folded into the pattern. The two migrated source files are rebuilt on their current contents for the same reason; one of them had also been reverted by a line. * fix(cli): name the migrated core imports so plain Node resolves them The tipHistory / RemoteInputWatcher migration used .js-suffixed subpath specifiers that match no entry in packages/core/package.json exports, so the built-but-unbundled CLI (npm start / build-and-start, whose tsc dist keeps specifiers) crashed at module load with ERR_PACKAGE_PATH_NOT_EXPORTED while typecheck (tsconfig paths), unit tests (vitest wildcard alias) and the bundle (esbuild paths) all bypassed exports and stayed green. Switch the two files to named subpaths (storage, atomicFileWrite, debugLogger), following the convention of the eight existing entries, and add the matching exports entries and vitest aliases. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtlz4ubdgq * test(scripts): guard core subpath exports resolution under plain Node Every core subpath specifier statically imported from packages/cli/src must resolve through packages/core/package.json exports in a real child node process — no vitest aliases, no tsconfig paths. Without the matching exports entries the built-but-unbundled CLI dies with ERR_PACKAGE_PATH_NOT_EXPORTED while every gate that bypasses exports stays green; this guard goes red the moment an entry is removed. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtlz4ubdgq * fix(cli): name every core subpath import in cli tsconfig paths The named subpaths cli sources import from @qwen-code/qwen-code-core had no named `paths` entries, so the wildcard composed nonexistent files and esbuild (bundle) plus the dev loader chain fell back to the exports map, loading packages/core/dist copies while every package-root import loads packages/core/src — two instances of barrel-exported, stateful modules (debugLogger, storage, atomicFileWrite, envVarResolver, toolWriteOrigin, memoryScopes) in one process. Add the six missing named entries beside the existing ones so bundle and dev resolve all subpaths into the core src tree, consistent with the vitest alias list. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtme4yqxhl * test(scripts): pin subpath exports targets and guard bundle resolution Complete the resolution guard and harden its probe: - Cover every core subpath statically imported from packages/cli/src (adds toolWriteOrigin and memoryScopes) plus the subpaths npm start reaches through @qwen-code/acp-bridge (subSessionConstants, goalWire, transcriptRecords), instead of the previous five specifiers. - Pin each specifier to its expected dist target: assert the resolved URL equals the pinned path and the target file exists, so a typo'd or redirected exports target fails the guard (import.meta.resolve alone accepts both). This makes a built core dist a prerequisite, which vitest-global-setup already fail-fasts on. - Add a bundle-resolution guard: esbuild-bundle every cli/src core subpath under packages/cli/tsconfig.json and assert no input comes from packages/core/dist, pinning the tsconfig paths entries. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtme4yqxhl * fix: cover goalWire in the core-subpath guard and make it OS-independent - Add goalWire to the bundle arm and named tsconfig paths entries in packages/cli and packages/acp-bridge: acp-bridge's transcript-replay imports @qwen-code/qwen-code-core/goalWire, and without a named entry the wildcard resolved to a nonexistent ../core/src/goalWire and fell back to the exports map, bundling packages/core/dist/src/goals/ goal-wire.js next to src-resolved core modules — the module-identity split #10908's Known risks name. Verified with an esbuild metafile probe: dist input before the paths entry, src input after. - Normalize esbuild metafile input keys to forward slashes before the dist-leak filter and src-target assertions so the guard behaves the same on Windows runners, where esbuild emits backslash separators. - Correct the header: this lane's vitest config does not wire scripts/vitest-global-setup.js (it is a globalSetup only in the packages/core and packages/cli configs, and its DIST_PREREQUISITES has no key covering this lane), so a missing dist surfaces as the existence assertion naming the absent file. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtmourmvi4 * test: guard acp-bridge-routed core subpaths in the bundle arm The bundle arm seeded goalWire into the cli-routed buildSync case, which passes an explicit packages/cli/tsconfig.json — but the shipped bundle resolves that import under packages/acp-bridge/tsconfig.json (mainBuild in esbuild.config.js carries no tsconfig option, so esbuild discovers the nearest tsconfig per importing file, and goalWire is imported only from packages/acp-bridge/src/transcript-replay.ts). The acp-bridge goalWire paths entry was therefore guarded by no arm, and transcriptRecords / subSessionConstants were probed by none at all: removing the acp-bridge goalWire entry left the test green while a production-shaped build pulled packages/core/dist inputs. Add an acp-bridge-routed buildSync case — no tsconfig option, resolveDir packages/acp-bridge/src, seeding goalWire, transcriptRecords, subSessionConstants and noFollowOpen — asserting no input lands under packages/core/dist and each expected core src target is present. With the new arm, removing the acp-bridge goalWire entry goes red (4 dist leaks) where previously nothing did. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtmxfessij * fix(cli): guard the conversationsRuntimeMarker subpath route R6-1: @qwen-code/qwen-code-core/conversationsRuntimeMarker is statically imported from packages/cli/src (config/shared-env-keys.ts, serve/run-qwen-serve.ts) but carried no named entry in packages/cli/tsconfig.json paths and was seeded into neither guard map. The wildcard composed a nonexistent ../core/src target and fell back to the exports map, so the cli-routed bundle loaded a packages/core/dist copy next to the core src copy while every guard arm stayed green. Add the named paths entry beside the ones this PR already adds and seed both guard maps with the specifier. Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com> Patrol-Run: qwen-pr-closeout/jmtngpuscjl * fix(sdk): guard the sdk-routed core subpath resolution R6-2: the cli bundle reaches sdk sources through the cli tsconfig @qwen-code/sdk/* mapping (ui/utils/export/export-transcript-document.ts imports @qwen-code/sdk/daemon/transcript, which re-exports from daemon/ui/chat-record-transcript.ts), and chat-record-transcript.ts imports @qwen-code/qwen-code-core/transcriptRecords. With no paths in packages/sdk-typescript/tsconfig.json, mainBuild's per-importing-file tsconfig discovery found no mapping and fell back to the exports map, bundling a packages/core/dist copy while the guard suite stayed green. Add the named entry to the sdk tsconfig, mirroring acp-bridge, and a third guard arm probing the sdk route. Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com> Patrol-Run: qwen-pr-closeout/jmtngpuscjl * fix(sdk): reference core from the sdk composite reference project The paths entry added for the sdk-routed guard resolves @qwen-code/qwen-code-core/transcriptRecords to the core source file, which in the composite tsc --build graph belongs to the core project. Without a project reference the cli build failed with TS6059/TS6307; declare the core reference, mirroring packages/acp-bridge/tsconfig.json. Verified with npm run build in packages/cli and npm run typecheck in packages/sdk-typescript. Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com> Patrol-Run: qwen-pr-closeout/jmtngpuscjl * fix(sdk): keep the declaration build on the exports map The paths entry added to tsconfig.json for the bundle's esbuild discovery is inherited by tsconfig.build.json; in that plain declaration build it pulled core sources into the program, re-rooted the inferred rootDir above the package, and nested every emitted .d.ts under dist/sdk-typescript/src/, dropping dist/daemon/index.d.ts that web-shell imports (TS7016). Reset paths in tsconfig.build.json so the declaration build resolves core through its exports map. Verified with npm run build in packages/sdk-typescript: dist/daemon/index.d.ts restored and the daemon browser bundle byte-identical (236252 bytes, its pre-existing warning threshold breach is unchanged). Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com> Patrol-Run: qwen-pr-closeout/jmtngpuscjl * fix(scripts): route core subpaths to src in the dev loader The dev loader intercepted only the exact package root, so a named core subpath fell through to the package's exports map and loaded packages/core/dist while every package-root import loaded packages/core/src. One dev process then held two instances of the same module: Config binds the debug session on the src copy (setDebugLogSession(this)), so RemoteInputWatcher's dist-copy REMOTE_INPUT logger read an empty session and every debugLogger(...) call there silently no-oped with QWEN_DEBUG_LOG_FILE enabled. Storage split its static state the same way. Derive the interception map from the core exports map so every named subpath short-circuits to its packages/core/src file and stays covered as subpaths are added. The exports entries (built-but-unbundled CLI) and the tsconfig paths entries (bundle lane) are untouched. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtow5pgslz --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com> |
||
|
|
101b003f93
|
test(ci): make verify-capture fallback assertion rasterization-safe (#10758)
* fix(ci): stabilize verify-capture fallback-grey test on fontless hosts (#10757) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): require near-neutral pixels in the verify-capture fallback scan (#10758) A brightness-only count was also satisfied by the hardcoded #9cdcfe title fill and by a tinted FG_DEFAULT, so the defect this case guards — a deleted bounds guard shipping fill="undefined", which librsvg paints black — could escape unnoticed as soon as a capture is captioned. Both endpoints of the blend axis are neutral, so requiring a channel spread <= 12 keeps the count specific to the fallback grey at any coverage, with tolerance enough for subpixel antialiasing that the exact-colour scan it replaced did not have. Probes on a fontless host (fc-list = 0): guard deleted, guard deleted with --title, and FG_DEFAULT='#ff8080' each fail this case now, and all three pass with the clause neutralised; --title alone still passes. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): keep a mid-grey fallback out of the verify-capture count (#10758) The brightness floor admitted an FG_DEFAULT of exactly 0x80, so a mid-grey fallback still satisfied the count: measured on a host without fontconfig, a '#808080' mutant cleared it in 6 of 12 renders. Make the floor strict, which kills that mutant in 12 of 12 while the real '#d4d4d4' control still counts 35-70 pixels against a threshold of 1. Restate the predicate comment to what the tolerance actually excludes — a channel spread up to 12 is admitted, so only a grossly tinted fallback is ruled out, not every tint. * test(ci): tighten the verify-capture fallback spread bound (#10758) The 12-unit channel-spread tolerance was justified as absorbing subpixel antialiasing, but that mechanism never reaches this rasteriser: both ends of the blend axis are neutral greys, so #d4d4d4 over #1e1e1e yields r=g=b at every coverage and a glyph pixel's spread is 0 by arithmetic. Measured here over 8 renders and 440 bright pixels, max spread was 0 and the <=4 and <=12 counts were identical; the fontconfig control in this round's review report shows rgba=rgb and antialias=false both render byte-identically, so the fringing the slack was reserved for cannot occur. The slack cost a mutant. FG_DEFAULT='#d4d4c8' has spread exactly 12 and passed the old bound; at <=4 it fails. The mid-grey '#808080' and the deleted-bounds-guard mutants still fail, so no detection is given up. Restate the predicate comment to the measured reason instead of the fringing that cannot happen. Net line change is zero. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: 易良 <1204183885@qq.com> |
||
|
|
827c8aa492
|
fix(ci): salvage superseded review runs and hold the loop's report-time base refresh (#10123)
* fix(ci): salvage superseded review runs and hold the loop's report-time base refresh A synchronize used to cancel-in-progress the in-flight review run no matter how close to done it was: on PR #9729 a push cancelled run 32726618419 4h06m in, minutes from posting, and cancelled review runs wasted ~7h41m of runner time across that one PR. A declarative cancel cannot weigh the run's progress, so the supersede decision moves into the run: cancel-in-progress now fires on closed only, a synchronize run queues pending in the PR-scoped group, and the in-flight run's new supersede watcher either ends the attempt early (pre-threshold: the queued replacement reviews the new head from scratch, within a poll interval) or, past the salvage threshold (compose artifact present, or elapsed >= 50% of the size-aware budget, tunable via QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT), finishes and posts against the head it reviewed. The gh-wrapper write guard gains a salvage escape pinned to the reviewed head; the CLI's existing presubmit head-drift gate annotates and caps the verdict, and a new step posts a historical-head note naming the anchor the next incremental review starts from. The delay job now also skips a queued run whose event head went stale while it waited. The other half covers the one loop-owned head move outside the #8888/#8899 dispatch hold: the autofix report step's stale-base update-branch. It now probes for a live review-pr first (same probe pair as the scan gate, fail-open on probe errors) and defers the refresh with the retry branch's sentinel semantics instead of superseding the review; the deferred round joins the consecutive-failure exemptions. Rationale recorded as qwen-autofix.md#af-148, and the af-099 delay-window entry is updated for the new supersede semantics. Both workflows' .size-baseline entries are bumped in this PR per the growth ratchet: the review workflow gains the watcher/salvage machinery (+15 KB) and the autofix report step gains the probe pair (+6 KB) — real feature growth, with the long-form rationale kept in the design record. Fixes #10110 * test(shepherd): classify the deferred stale-base headline as transient The fleet-shepherd contract test requires every autofix HEADLINE to be named terminal or transient; the report step's new deferred-stale-base headline (#10110) was unclassified. It is transient — the loop retries on the next scan — and the shepherd's REASON regex already does not match it, so only the classification lists needed the new needle. * fix(ci): scope supersede-salvage per attempt and reap the watcher safely Round 1 review findings on the salvage machinery. The EXIT trap's watcher reap killed an already-reaped subshell on the salvage-cede path: under errexit the failed kill aborted the trap before the SALVAGE_DIR cleanup and turned the deliberate clean exit into exit 1 — the kill now tolerates a reaped watcher. The salvage threshold parse accepts leading-zero values that the watcher arithmetic reads as octal (`08` kills the first salvage_eligible call, `050` silently applies 40%); the value is coerced decimal after the digit guard. Salvage eligibility is now per attempt, matching the threshold's own rationale: the retry loop restarts the review from zero, so attempt 1's compose sighting is unlatched and its elapsed fraction no longer counts against attempt 2 — the loop resets the latch and records an attempt-start timestamp the watcher reads (self-bound stays per run). The supersede file is also re-checked at the top of the loop: a cede landing during the retry backoff (pkill matched nothing — qwen not running) used to be noticed only after a full re-review of the dead head. Docs-only runs always cede on a moved head: their only posting vehicle skips any moved head and the salvage-note step excludes them, so an armed KEEP could never post. The watcher's KEEP/CEDE dispatch is now replayed with stub gh/pkill/sleep (marker carries the reviewed head, supersede file the live one, docs-only and fresh-attempt cedes pinned), the parse and the trap are replayed too, and runScenario gains the backoff-cede and latch-reset scenarios. Six mutation probes confirm each guard is witnessed; all eight changed tests fail against the pre-round workflow. The .size-baseline entry is bumped per the growth ratchet. * fix(ci): verify the live head before ceding and validate salvage signals The supersede/salvage cedes trusted agent-writable marker files by existence alone, letting a forged file suppress a whole review with the job green, and the composed-artifact latch trusted a bare existence check on an agent-derivable workspace path. Re-read the live head at every cede decision, latch only on a parseable composed verdict, delete the artifact in the per-attempt reset, and shape-check moved-to before the $GITHUB_OUTPUT write. Each guard is pinned by a replayed-bash witness that turns red when the guard is removed. * fix(ci): make salvage signal IO FIFO-safe and cede decisions outcome-aware Round-3 review fixes for the supersede-salvage mechanism (#10110): every signal write goes aside-then-rename (write_signal) and every read gates on [ -f ], so planted FIFOs at the agent-derivable signal paths can no longer block the watcher, the finished step, or the posting path; the compose latch refuses non-regular artifacts; the cede message validates the supersede target to the watcher's 40-hex shape before interpolation; the salvage outputs block re-checks the live head like the cede sites; the post-attempt supersede check carries the OUTCOME guard its sibling has; and the per-attempt reset also runs before the retry backoff. The replay harness pins AUTO_REVIEW off so an exporting environment cannot arm the watcher inside a replay. Each guard has an executed replay witness, all mutation-probed red. * fix(ci): cede genuinely superseded runs and restart the retry watcher - Cede clean at the terminal fail when the live head really moved (a poll-gap move left no signal file) or when the PR timeline proves a move-then-revert landed during the run — a superseded run must not go red while its replacement is already queued. - Restart the one-shot watcher when a retry outlives it and clear the dead attempt's marker/moved-to/superseded signals, so the fresh attempt is watched and meets fresh state. - Measure salvage eligibility against the attempt's own budget share, not the whole run's, so a late retry can still reach KEEP. - New executed replays witness each guard; runScenario now returns the exit status so the load-bearing clean cede exits are pinned. * fix(ci): close the salvage cede races and hostile-state aborts * fix(ci): gate supersede cedes to automatic runs and dedup same-head reviews (#10123) * fix(ci): bound salvage-signal reads and authenticate the delay-step dedup (#10123) Address the round-8/9 review Criticals: - The delay-step head-level dedup called `gh api --arg`, a flag gh does not have, so the lookup died at flag parse and the dedup was dead code. Drop the flag and interpolate the constant bot login into the --jq filter. - The dedup parsed the FIRST `"sha":"…"` hit out of model-authored review bodies, a forgeable anchor. Parse only the LAST ledger marker per body (the CLI's parseLedger convention) and authenticate the sha against the review's server-recorded commit_id before skipping; every lookup trouble still fails open. - The revert-cede accepted the watcher's kill record alone, but the record's dir is enumerable under RUNNER_TEMP — non-export stops derivation, not discovery by the same-uid agent. Accept the record only corroborated by a server-side back-push onto the expected head; the timeline pair stays the fallback for a lost record. - The salvage-signal reads gated on [ -f ] and then reopened agent- derivable paths with unbounded cats: a FIFO rename-swapped into the check-then-open window wedged the watcher, the posting path, or the cede forever, and a huge plant could be slurped whole. Each read is now one `timeout 5 head -c N` open; the posting path fails closed. - The replay harnesses supply a bound-enforcing timeout(1) stub (macOS lanes ship none), and the gh stub rejects unknown flags like real gh so the --arg fiction cannot be re-emulated. New replays witness each fix (forged kill record, forged/first markers, commit_id mismatch, rename-swapped FIFOs, huge plants); mutation probes confirmed every witness turns red when its guard is removed. The workflow's .size-baseline ratchet is bumped for the real growth. * fix(ci): cap the compose-latch read, poison-proof cleanups, and authenticate the cede/skip witnesses (#10123) * fix(ci): drop the forgeable delay-step dedup and scope the report hold to lifecycle runs (#10123) * fix(ci): pin cede/salvage live-head reads to the real gh (#10123) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ci): pin supersede timestamp conversions to the real date, seal replay harness envs (#10123) * fix(ci): pin salvage clock/utility resolution and bound planted salvage signals (#10123) * fix(ci): pin the supersede watcher's utilities and bound future attempt-start plants (#10123) Address the round-21 Critical findings on the review workflow: - The supersede watcher resolved bare sleep/pkill/id through the agent-writable $proxy_bin PATH prepend; capture them in configure_qwen_network before the prepend like gh/git/date/rm/tee and read the captures with :- fallbacks (R21-2). - The attempt-start guard bounded only the low side, so a 10-digit future epoch flipped KEEP to CEDE through a negative elapsed; add the pinned-clock upper bound (R21-3). - runScenario pinned the lane-inherited real-utility captures and AUTO_REVIEW but not QWEN_CI_REVIEW_SALVAGE_OK_FILE, so on this workflow's own review lane the replayed retry reset rm-rfed the parent run's live salvage marker; pin it empty ahead of extraEnv, together with the sleep pin the armWatcher replays need (R21-1). Witnesses: a lane-simulation canary for the marker, proxy-bin plant arms for the watcher's poll and kill, and a future arm in the attempt-start bounds test; each goes red with its guard removed. * fix(ci): refuse bot-authored force-push events in the supersede revert cede (#10123) * fix(ci): finish the review utility pin sweep and scope the cede kill (#10123) Three gaps in the review lane's supersede hardening, all reachable by an agent that can write the $proxy_bin PATH prepend: - The pin sweep stopped six commands short. timeout, head, node, mktemp and mv still resolved through the prepend at thirteen added sites, and write_signal called rm bare 26 lines below the comment stating the rule. These fork from the step shell or the watcher subshell, not from the qwen invocation, so the invocation-scoped $GITHUB_* decoys never covered them and a plant inherited the real command files and GH_TOKEN. Capture all five before the prepend and pin every site. - The CEDE kill identified its target by this PR's review URL, which is not this run's identity: an explicit run gets a unique per-run concurrency group and reviews the same URL concurrently, so a ceding automatic run killed it too. Its attempt then died with 143 — neither timeout nor success — so it went red over a push it had nothing to do with. Scope the sweep to this step shell's own children; GNU timeout signals its whole monitored group, so this run's tree still dies. - START_TS, the baseline of both timeline bounds, read the unpinned date, so a plant answering +%s with a past epoch dropped both bounds and let the run's own triggering force-push corroborate a forged kill record — a green cede with nothing posted for a head that never moved. Every guard is witnessed: replayed proxy-bin arms for each pinned utility, a real-signal CEDE replay that spawns a decoy for this run and one for a concurrent run of the same PR, and a capture-block property test the env-pinned replays cannot see. All eleven mutation probes were measured red without their guard. * fix(ci): kill the superseded review attempt's process group, not just its timeout (#10123) The supersede watcher's TERM->KILL escalation swept -P $$, which only ever matched the timeout wrapper: SIGKILL is not forwarded and the wrapper's own --kill-after timer dies with it, so a TERM-resistant member of the attempt tree survived holding the tee pipe's write end. run_review_once then never returned, the post-outcome cede checks were never reached, and the run went red at the job timeout instead of ceding clean while the queued replacement waited in the concurrency group. Escalate against the process group that timeout leads instead, resolving its pid through a pre-prepend pgrep capture before the TERM sweep retires it. The sweep stays scoped to this step shell's own child matching this run's review URL, so a concurrent explicit run of the same PR is still untouched. * fix(ci): make the salvage replay suites truthful on macOS and pin the loop's last bare clocks Takeover commit after 27 autofix rounds. Running the four suites on macOS 26.6 (the merge_group/schedule-gated test_macos lane no round could reach) turned up nine reds and eight silently skipped cases in qwen-pr-review-workflow.test.js: - The replay harness pinned QWEN_CI_REAL_TEE to /bin/tee; macOS ships tee in /usr/bin, so every replayed cede exited 127 under errexit. Resolve rm/tee through realUtilityPath like the other pins. - hasMkfifo probed `mkfifo --help`, which BSD mkfifo rejects, so all FIFO wedge tests were skipped on the one platform the probe exists for although /usr/bin/mkfifo is present. Probe presence (ENOENT only); the eight cases now run and pass on macOS. - The ~1.5 GB over-cap plants came from `head -c /dev/zero | tr`, ~65 s each on macOS (68 s per case in the suite). Generate them with yes(1) instead — same real non-hex bytes, ~2 s. A sparse file would not do: bash drops NUL bytes from a command substitution, so the unbounded-cat mutant these cases exist to catch would ship green. Also, from the open Copilot threads: - The retry loop's two budget computations still read bare `date` after the agent-writable $proxy_bin prepend; read the pinned QWEN_CI_REAL_DATE like START_TS does. - The pre-loop attempt-start write_signal was the one salvage write not `|| true`, contradicting the reset's best-effort contract. - The autofix design-doc test pinned the pre-existing af-149 anchor instead of the hold's own af-155 entry. - .size-baseline entries set to the exact committed sizes (main-side growth of qwen-autofix.yml had drifted the ratchet by 2 KB). * fix(ci): neutralize inherited review-lane captures in the replay harnesses and bound the salvage percent before arithmetic R28-1: every replay harness spreads process.env into the replayed shell, and the review lane exports the production QWEN_CI_REAL_* captures into the reviewed agent's environment. Only GH/DATE/SLEEP were neutralized, so on that lane the bounded-read witnesses resolved `timeout 5 head -c` to the real utilities: runWatcher's two rename-swap FIFO wedge tests passed without a FIFO ever being swapped in (28 ms / 11 ms instead of ~5 s), and the runCedeRead / runSalvageOutputs swap witnesses went red. A sweep now empties every inherited QWEN_CI_REAL_* key ahead of each harness's own pins (runScenario, runWatcher, runCedeRead, runSalvageOutputs, and the resolve suite's gh-wrapper harness) — a sweep, not a name list, so the next capture production adds cannot reopen the door. Mutation-proven on Linux under the exported-capture shape: the fixed tree keeps all seven wedge tests green and exercised, the pre-fix tree reproduces the vacuous pass and the two reds, and removing the `timeout 5` bound from the workflow turns six of the seven red on both lanes. Sandboxed verification F1: the salvage-percent parse clamped AFTER `$((10#…))`, so a repo variable >= 2^63 wrapped silently — 2^64 to exactly 0, "salvage always", and the run never ceded. Strip leading zeros, then bound the digit count before any arithmetic; the replayed parse test pins the wrap values, `0050`, `000` and `1000`. F2: the retry loop's remaining bare `date` (deadline epoch, stop token) and the backoff `sleep` now read the pinned captures like START_TS. Round-28 deferred, taken because they mirror existing gates: the post-loop `salvaged=` emission carries the same AUTO_REVIEW gate as the five cede sites (an explicit run arms no watcher, so a marker there is a forgery and must not become a historical-head note; replay gains the explicit-run arm), and the af-099 design-record text describes the supersede semantics instead of the removed synchronize cancel. * fix(ci): keep the deadline-epoch line bare — packages/cli pins it as the units contract packages/cli/src/commands/review/lib/deadline.test.ts ("the CI wiring contract") pins the exact line `QWEN_REVIEW_DEADLINE_EPOCH="$(( $(date +%s) + attempt_timeout ))"` so the epoch stays whole seconds; e76afd48d4's clock pin on that line turned the contract red in CI (Test (ubuntu), 1 failed / 28135 passed). Restore the bare `date` there with a comment naming the contract; the stop-token and backoff-sleep pins stay, and no other test pins them. * docs(ci): record why the revert witness excludes only the agent's credential, and pin the queue half of the supersede model R29-1/R29-2 asked supersede_reverted_during_run to exclude the PR author from both witness loops. Declined, and the reasoning now sits at the actor filter: the witness exists to keep the reviewed agent from forging a move with the credential it inherits; the author needs no forgery — one real head move per run before the salvage threshold already cedes that run green by design, so an author-acted away-and-back pair corroborating planted signals adds no capability, while excluding the author would turn the author's own legitimate in-run revert into a red failure. Two round-29 deferrals taken in the resolve suite: the planted-FIFO wedge asserts mkfifo really created a FIFO (a silent plant failure let the case pass for the wrong reason), and `synchronize`/`closed` are pinned as pull_request_target triggers — every CEDE branch relies on a push queuing the replacement lifecycle run. * fix(ci): make the supersede watcher's TERM->KILL wind-down immune to its reapers R30-1: every reaper — the EXIT trap on a cede or fail(), the retry branch, the post-loop reap — TERMs the one-shot watcher and waits, and a TERM landing inside the 15s wind-down ended the watcher before its group KILL, the only signal a TERM-resistant tree member answers; that member then outlived the step on the shared runner holding the job's credentials. The watcher now sets `trap '' TERM` at the top of its CEDE branch, before the first signal write: the polling phase stays TERM-killable so a normal step end reaps it at once, and once the decision is made the sleep and the kill inherit the ignore, so every reaper's kill+wait spans the bounded wind-down and the KILL lands first. The EXIT trap gains the wait the other two reapers already had, covering cede_superseded's exit 0 and fail() alike. Persisting attempt_pgid to a SALVAGE_DIR signal and re-firing the KILL at each reap site was not taken: that file is agent-writable, and a planted pgid would aim the group KILL at any process group of the runner user. R25-1's group-KILL contract, the unconditional relaunch and the 15s wind-down (<< the 60s backoff) are unchanged. Witness (replayed watcher, real pkill/pgrep): the realCedeKill decoys plus a backgrounded watcher with a 3s real wind-down, TERM+wait fired the moment the supersede file appears — reap >= 2s, own attempt and tree member dead, concurrent run alive. Dropping the TERM ignore turns it red (reap 0s, tree member alive) on macOS and Linux. The trap pin covers the added wait; a shape pin keeps the ignore inside the CEDE branch. * fix(ci): refresh on the capped round instead of deferring, and hand the review skill the salvage contract R32-1 (regression): the report-step hold deferred the stale-base refresh to "the next scan" even on the last permitted round, where the scan's round gate parks the PR before any report step could run — the refresh never happened and the headline promised a retry the cap forbids. The deferral is now taken only while a next round exists (`MARK_ROUND -lt MAX_ROUNDS`, the step's own cap); at the cap the hold yields to the update-branch PUT, the pre-#10110 shape, and af-155 says so. Witness: the rollup-probe-through-PUT block replayed with a stub gh and the real jq — below the cap a live review defers, at the cap it refreshes; dropping the guard turns the at-cap arm red. R32-2 (new surface): the salvage escape admitted the historical-head post, but the skill's presubmit rule (references/posting.md, anchorsAtRisk=true) commanded abandon-and-restart in exactly the drifted state a salvage creates — compose latches the artifact before presubmit runs — so the modal salvage restarted instead of posting. The review step now exports QWEN_REVIEW_SALVAGE_POST=1 inside the AUTO_REVIEW-gated arming block (an explicit run never carries it), and posting.md gains the one exception: with that signal AND a marker whose content equals headDrift.reviewedSha, submit as in the anchorsAtRisk=false branch and consume no restart. The historical-head note step reads back the latest review by its own credential and claims the anchor only when its commit_id equals the reviewed head, so a restart that completed at the new head never gets an old-head note. Cross-pinned by name: the review suite pins the gated export (exactly one site) and replays the note step both ways; SKILL.test.ts pins the exception sentence. The guard comment now names anchorsAtRisk. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
aab0258857
|
fix(test): put the scripts suite's quiet-host budgets on its own knob (#11098) (#11106)
Release run 33957952281 lost Quality Checks (Scripts) to two budgets that were measured on a quiet machine and met a contended one: - install-script.test.js capped itself at 30s with vi.setConfig, overriding the 90s floor scripts/tests/vitest.config.ts sets for exactly this reason (#10870). A standalone-packaging case that costs 3s idle timed out at exactly 30000ms. The file's two per-test 30s arguments and its 15s/60s describe ceilings were the same quiet-host figure, so they go too. - qwen-autofix-workflow.test.js bounded its deferred-findings subprocess at 30s. That case costs 0.9s idle; killed mid-run, the stub `gh` recording truncates, so the timeout reported itself as a content mismatch on the first recorded call. The file's four hung-runner bounds now ride QWEN_SCRIPTS_TEST_TIMEOUT_MS. Its 3s/2s bounds pair with a stubbed QWEN_TIMEOUT_MS and are fast-fail guards, so they stay. Both signatures were reproduced locally by shrinking each budget, then reverted. A witness in unit-vitest-configs.test.ts fails if a file in the suite caps the floor again; mutation-probed both ways. The same run also lost Quality Checks (Static) to its 30-minute job timeout, and the re-run on current main lost Static and Build the same way. That is release-workflow capacity rather than test code, so it is escalated on the issue instead of changed here. Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
7bd863daaf
|
fix(ci): stop the scripts-suite timeout knob from failing open on an empty value (#10910)
`scripts/tests/vitest.config.ts` read its ceiling as
`Number(process.env['QWEN_SCRIPTS_TEST_TIMEOUT_MS'] ?? 90_000)`. `??` only
catches `undefined`, so an empty value yields `Number('')` === 0, and vitest
reads 0 as "no timeout at all" — the knob meant to raise the ceiling would
instead remove it, and a hung test would run until the job cap. `NaN` from a
typo does the same.
`''` is not a hypothetical spelling. It is exactly what this repo's
`${{ cond && 'x' || '' }}` idiom renders when the condition is false, which is
how the sibling `QWEN_SKIP_LATENCY_BUDGETS` knob one line away in ci.yml is
wired. Nothing sets this variable in a workflow today, so the fault is latent
rather than live.
Measured, on this config:
undefined -> 90000 '' -> 0 'abc' -> NaN '5000' -> 5000
`||` instead of `??` sends every non-positive spelling to the default. The
cost is that 0 can no longer be passed to mean "no timeout"; that is a footgun
rather than a feature, and no caller uses it.
The companion pin could not have caught this. It stubbed `''` and then called
`vi.unstubAllEnvs()` on the next line, so both of its arms measured the unset
path and the one value that would fail was discarded. It now asserts `''`,
`'abc'` and `'0'` alongside the two original arms; `vi.stubEnv(k, undefined)`
deletes the variable, so the unset arm no longer needs the if/else. Verified
both ways: the rewritten pin fails on the old expression
(`expected +0 to be 90000`) and passes on the new one.
Also drops six per-test `}, 30000)` ceilings in qwen-autofix-workflow.test.js.
They predate the suite ceiling and now shadow it, pinning exactly the
bash-spawning cases that the 90s default exists to protect back down to the old
flat 30s. None of them asserts a duration property — each is a "give this
subprocess-spawning test room" budget, and the last one says so in its own
comment, which is updated to point at the suite ceiling. The three
`timeout: 30_000` options that remain are `spawnSync` bounds, deliberately
separate because spawnSync blocks the event loop where vitest's async timeout
cannot fire.
The ceiling removals were first proposed by qwen-code-dev-bot in #10858, which
conflicts with the knob that landed in #10870 and is superseded here.
Follow-up to #10870; the empty-value fault was raised there by doudouOUC (S1)
and chiga0 (R3-1) and confirmed post-merge by wenshao.
|
||
|
|
689453ef96
|
fix(release): block the transport-timeout pass-through on any exception header (#10902)
* fix(release): block the transport-timeout pass-through on any exception header The workspace-tests step may exit 0 over a Vitest worker-RPC timeout, but only with proof the run reached its end and nothing else broke. The last leg of that proof anchored on a bare `Error:`, and Node prints an unhandled exception under its own class header — `TypeError:`, `AssertionError:`, `SyntaxError:` — so any crash that is not literally `Error:` cleared all four legs and shipped as a green release, behind an annotation claiming no other error was reported. Reproduced against the step's own script with npm stubbed: a log carrying the transport timeout, a `TypeError: Cannot read properties of null` with a stack, and a passing tally exits 0 on main and 1 with this change. Widening the header match to `[A-Za-z_$]*Error:` also covers the whole-file/per-run gap the same leg had — a passing tally from one workspace no longer covers a later crash, since that crash's header is now seen. Two red probes pin both shapes. The legitimate pass-through (timeout plus a passing tally, nothing else) is unchanged, as are the signal-death, `FAIL`-line and `Error: write after end` paths. Found by qwen-code-dev-bot reviewing #10805, which merged ~10 seconds before that review landed, so the finding never gated it. Claude-Session: https://claude.ai/code/session_01AWWgJEqafyAT1Mc75T8N7h * fix(release): match coded and digit-bearing exception headers too The previous widening to `[A-Za-z_$]*Error:` still enumerated header shapes and left corners open, as the review probe showed: - Node prints coded internal errors as `Name [ERR_CODE]: message` (`AssertionError [ERR_ASSERTION]:`, `Error [ERR_MODULE_NOT_FOUND]:`), with the bracketed code between the class name and the colon, so the anchored pattern never matched the line. - The character class omitted digits, and this repo throws a digit-bearing class (`LargeNonUtf8TextError`). Both now match, and `Exception` suffixes are accepted alongside `Error`. Re-probed against the step's own script with npm stubbed: `AssertionError [ERR_ASSERTION]:`, `TypeError [ERR_INVALID_ARG_TYPE]:`, `Error [ERR_MODULE_NOT_FOUND]:`, `LargeNonUtf8TextError:` and the plain `TypeError:` all flip to exit 1, while a clean transport-timeout log stays exit 0 so the `transport timeout, run completed` case keeps its pass-through. This narrows the class rather than closing it: a producer-defined name without an `Error`/`Exception` suffix (`PoolTimeout:`) still clears the guard, verified still exit 0. Closing it properly means deciding on an authoritative signal — vitest's machine-readable report, or re-running the shard and requiring green — instead of a wider header regex. Recorded here rather than left implicit. * test(release): pin each constituent of the exception-header matcher Mutation-checked the committed suite and confirmed the review's finding: three parts of the widened matcher had no probe, so deleting any one left everything green while reopening the hole. (Error|Exception) -> (Error) DOMException: exit 1 -> 0 [A-Za-z0-9_$] -> [A-Za-z_$] LargeNonUtf8TextError: exit 1 -> 0 [A-Za-z0-9_$] -> [A-Za-z0-9_] Foo$Error: exit 1 -> 0 Each new row goes red under its own mutant and green at HEAD, so they pin the constituent rather than decorate the table. `LargeNonUtf8TextError` is this repo's own class (`packages/core/src/utils/read-text-range.ts`), not a hypothetical. * fix(release): gate the transport-timeout pass-through on a green shard re-run The pass-through certified the run by enumerating exception-header shapes in the log, but the header is producer-chosen: a crash under a class with no Error/Exception suffix (PoolTimeout:) beside a passing tally cleared every guard and shipped green (R1-1). No enumeration closes that class, so the log is no longer allowed to grant the pass — the conditions only fast-fail on clear breakage evidence, and the pass now requires re-executing the same shard once and demanding a green exit. A real break fails the re-run; a transient worker-RPC death passes it. The probe rows flip accordingly: the suffix-less header now yields both verdicts depending on the re-run. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(release): certify the transport pass-through by Vitest's own error count The pass-through certified a run by reading crash headers out of the log, and no reading of headers can be complete: the class name is producer-chosen (`PoolTimeout:`, a bare throw, a coded internal), which is the standing R1-1. Re-running the shard would settle it, but the job's 45-minute budget is set by contention and a shard that lost its transport has already spent up to 36 of it, so a re-run does not fit in the one situation that produces these timeouts. Vitest already publishes the number the guard was trying to infer: it prints `Errors N errors` whenever unhandled errors occurred. Summing that across the log and comparing it with how many carried `[vitest-worker]: Timeout calling` certifies the run without naming a single class. It closes the suffix-less and bare-throw shapes no enumeration reaches, and it stops ordinary `Error:` lines that a test merely printed from refusing the pass — the log this guard was written for carries three of them. Two pre-existing defects left the guard unreachable in production, and the count is meaningless without them, so both are fixed here: - The step ran under GitHub's default `bash -e {0}`, so `npm … | tee` yielded tee's status, the `||` handler never fired, and a shard with a genuinely failing test exited 0 (release run 33806806226, job 100824085040). `shell: bash` restores `-o pipefail`. - The log is ANSI-coloured, which puts escapes between every label and its value, so three of the four conditions never matched real release bytes. `NO_COLOR` matches what ci.yml already sets on three steps. 18 probe rows drive the step extracted verbatim from the workflow; under mutation each condition, the whole-file sum, both anchors and the plural in the summary pattern dies to its own row. * fix(release): print both compared counts when the pass-through is refused The refusal annotation enumerated four causes, but the count comparison adds a fifth that none of them described: `errors` and `timeouts` simply disagreeing, with no crash in the log — the same transport message on two lines is enough. A shard refused for that reason was told one of four things happened when none did, and was not given the two numbers whose mismatch was the whole story. The pass-side annotation already interpolates `${errors}`, so name the fifth cause and print both figures. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtolfwlblj * test(release): pin the step declarations and anchored patterns the guard needs Three things the guard depends on had no test opposing their removal: - `shell: 'bash'` is the step's only source of `-o pipefail` (the workflow has no `defaults:` block), so it is what makes the guard live rather than dead code. The probe harness passes `-o pipefail` itself, so deleting the line as "redundant" left every row green while GitHub fell back to `bash -e {0}` and a failing shard exited 0 into the release. - `NO_COLOR: 'true'` is load-bearing for every anchored pattern: Vitest colours from the mere presence of CI, and escapes between a label and its value match nothing. The stub npm prints plain-text fixtures, so nothing noticed. - The `^[[:space:]]*` and `$` anchors on the `Errors` sum had no discriminating row: the loosened pattern sums identically on every existing fixture. A summary-shaped line a test printed at column 0 is what separates them, and it fails closed — the false-red this PR removes. Also adds the row for the count-mismatch refusal, asserting the annotation carries both figures. Each mutant was run: dropping `shell`, dropping `NO_COLOR`, unanchoring the awk sum, and dropping the interpolation each turn exactly one of these assertions red. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtolfwlblj --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
948872b148
|
fix(web-shell): close the four deferred #9812 review follow-ups (#11107)
Each of the four suggestions deferred out of #9812 was left with nothing pinning it, so the behaviour each one protects could regress silently. Pin the historical sessionStorage key. `clientLifecycle.test.ts` only ever round-tripped through `SESSION_CLIENT_ID_STORAGE_PREFIX`, so renaming that constant moved the read and the write together and left every test green, while a tab that persisted its id under the WebUI-era key lost it across the migration. Three assertions now spell the key literally, including its percent-encoded session suffix. Make the ChatRecord export script testable and cover both paths. It had no exports and ran `main()` at import, which is why the legacy-JSONL rejection — a behaviour flip #9812 introduced — had no test. The input gate is now `selectChatRecords`, the render is `renderHtmlFromObjects(objects, api)` with the export API passed in, and `main()` runs only as the process entry point. The CLI behaves as before. The new suite asserts the rejection message verbatim and that the renderer is never reached, so removing the throw fails rather than falling through to the neighbouring error. Restore the substring-trap fixture the retirement deleted, under a live path. `Shellfish.tsx` guards the keyword being the head of a stem, which is a different loosening from the `web-shell` compound case already covered. Replace the deleted `InputForm` in the public hook's docblock with `ChatEditor`, which declares the three props, naming `ChatPane` and `App` as the in-tree hosts. Verification brief for the two vitest-backed items is committed alongside; item 3 was run here, mutation included, and the mutant survives without the new fixture. Refs #11076, #9812. Co-authored-by: yiliang114 <jinjing.zzj@gmail.com> |
||
|
|
0c945a6136
|
fix(release): price the static quality lane from the reserved host (#11127)
* fix(release): price the static quality lane from the reserved host (#11121) The Quality Checks (Static) lane runs prettier and then eslint on the pinned hk4 host, whose load — not the tree — sets how long they take. Measured on that pool: 17s + 141s on a quiet runner (run 33741188587), 928s + 645s eight hours before the run that needed 1348s for prettier alone (run 33941624603), and then the flat 30-minute ceiling landing with eslint still going. Run 33963757913 lost attempts 1, 3 and 5 that way, on three different runners, each with nothing failing anywhere; the aggregate quality job then failed closed on the cancelled lane and opened #11121. Give the lane the same retunable bound workspace_tests already uses, defaulting to 60 minutes: enough for the measured worst case with room for the pool to keep moving, and adjustable by an operator without a PR when it does. Both pins in the release workflow suite — the new one and the existing shared-pool bounds map — fail if the ceiling goes back to a literal. * docs(release): correct the evidence recorded in the static lane comment "Nothing failing anywhere" overstated attempt 1: the build lane was cancelled at its own 45-minute ceiling there too. Say what was actually observed — no lint diagnostic and no failing test in any of the three attempts, and a cancelled lane failing the quality aggregate. |
||
|
|
e133150ed1
|
fix(release): let an operator retune the quality lane timeouts (#11104) (#11110)
* fix(release): let an operator retune the quality lane timeouts (#11104) Release run 33963757913 lost v0.23.1-preview.0 with nothing wrong in the tree: quality_static was killed at 30m13s in Run Lint and quality_build at 45m13s in Pack Build Outputs, every earlier step green, both thirteen seconds past their own timeout-minutes. Four heavy lanes had started within a second of each other on the reserved hk4 host and shared its CPUs — `npm run format` alone ran 20m45s. A timeout kill reports as 'cancelled', the quality aggregate fails closed on any component that did not succeed, and notify_failure filed the issue against a clean tree. These were the next lanes to go, not a surprise. The last successful release (33941624603) cleared quality_static at 29.1m against its 30m bound and quality_build at 43.7m against 45m — 54 and 78 seconds of headroom. Runtime tracks fleet load rather than the work, which is why the workspace shard bound is already a variable; the operator has since raised it, and those shards ran 61.8-64.5m past a 45m fallback. The two lanes that just died had no knob to raise. Same remedy: both bounds read a repository variable and keep today's numbers as the fallback, so behaviour is unchanged until an operator sets one. The aggregate still fails closed on a timeout — a release that never finished validating should notify — so this makes the bound movable, not the failure quieter. Defaults are deliberately unchanged, so stopping the recurrence means setting QWEN_RELEASE_STATIC_TIMEOUT_MINUTES and QWEN_RELEASE_BUILD_TIMEOUT_MINUTES. The observed maxima suggest 45 and 70, but the right numbers are a fleet-load call for whoever operates the host. * fix(release): report the quality lane timeout budget in effect (#11104) Review follow-ups to the two quality lane knobs. No bound and no default changes. A knob whose value the run never states is indistinguishable from a knob nobody set: a misspelled variable name renders '' and the lane dies at its fallback again, and a variable set under Settings -> Environments does the same because neither lane has an `environment:` key. Each tunable lane now reports the budget it resolved and whether the variable reached the job at all, in the `::notice::` idiom this file already uses. The reported budget is the lane's own `timeout-minutes` expression rather than a copy of it, so the trace cannot drift from what the runner enforces; the knob test pins that equality and goes red if the step, the notice, or the set= signal is removed. The value is free text under a name ending in MINUTES, so the accepted format is now named beside the knob rather than left to the variable name: a bare positive integer, not `70m`, `1h` or a quoted `'70'`. Both measured consequences are recorded there too — GitHub refuses to create the job and the run blames 'a workflow file issue' instead of the variable, and `0` is not "no limit" because the string '0' is truthy in a GitHub `||`, so the knob wins and GitHub then ignores the zero bound it was handed. The fail-closed consequence is recorded because it is the reason no clamp was added: a value GitHub cannot turn into a number drops that one lane, the quality aggregate reads it as 'failure', and a bad value can refuse a release but never ship one unvalidated. Clamping only these two knobs would also have left three same-kind knobs in one file on two different contracts, since QWEN_RELEASE_WORKSPACE_TIMEOUT_MINUTES already uses the raw form and is set to 75 in the live repository. sdk-java.yml justified its own 45 by citing "the 45 tier release.yml's quality_build uses". That number is mutable now, so the citation names the variable and its default instead. Citation only: daemon-e2e derives 45 from its own npm-ci measurements and runs on ecs-qwen, not release.yml's ecs-qwen-hk4-host, so nothing there was retuned or knob-ified. |
||
|
|
c516740a27
|
chore(ci): remove retired WebUI dependency guard (#11095)
* test(ci): include WebUI guard in lane contract * fix(release): extend quality check timeouts * chore(ci): remove retired WebUI dependency guard |
||
|
|
077c57d200
|
refactor!: retire @qwen-code/webui (#9812)
* feat(transcript): add cross-host document export pipeline Establish a shared transcript model and document-mode projection so Web Shell, VS Code, and HTML export can consume the same stable conversation semantics without changing interactive rendering. - Preserve daemon and ACP segment identity across replay and normalization - Add export-safe previews and a versioned transcript document builder - Add document-mode Web Shell rendering with bounded Mermaid processing - Lock direct-daemon and ACP behavior with contract fixtures - Cover render and export equivalence in integration tests * feat(transcript): wire real VS Code and HTML export consumers Move transcript prevalidation onto real product paths while preserving the legacy VS Code timeline as the default fallback. - Add version-bound document HTML with strict CSP and schema checks - Add opt-in ACP projection with stable source identity - Reuse product export code across hosts and the integration runner - Remove test-only production probes and consolidate shared validation Note: Browser, host-action, VSIX, and packaging gates remain pending. * build(deps): sync transcript workspace dependencies Keep the npm lockfile aligned with the VS Code and HTML export package manifests added by the transcript consumer migration. - Lock the VS Code Web Shell workspace dependency - Lock Web Templates SDK, Web Shell, and React build dependencies * feat(vscode-ide-companion): reuse WebShell transcript UI behind experimental flag Bridge ACP session/update notifications into the shared SDK daemon transcript reducer and render the result with the WebShell transcript component, gated on qwen-code.experimental.webShellTranscript (default off). The WebShell renderer and its heavy transitive dependencies (echarts, mermaid, shiki, codemirror, katex) are lazily loaded via esbuild code splitting, so the default configuration keeps the ~700KB webview bundle unchanged. * fix(vscode-ide-companion): grant wasm-unsafe-eval for shiki WASM when WebShell transcript enabled * fix(transcript): harden export and identity paths Resolve review findings across document export and the VS Code ACP timeline while preserving default interactive and readonly semantics. - Preserve stable text and non-text identity across live and replay - Harden export projection, budgets, URL handling, CSP, and nonces - Make document rendering complete, inert, and browser-validated - Keep VS Code transcript scope, theme, copy, and flags reactive - Restore fixture, schema, hash, and compatibility contract locks Note: The overall gate remains failed pending VSIX, host-action, and packaged-artifact evidence. * feat(vscode-ide-companion): adopt WebShell transcript as default timeline Drop the experimental flag and the legacy MessageList renderer. The companion timeline now always renders through the shared WebShell transcript component, fed by ACP session/update notifications via the SDK daemon transcript reducer (lazy loaded through esbuild code splitting). The flag-gated wiring is removed: the qwen-code.experimental.webShellTranscript setting, the conditional CSP/body attribute in WebViewContent, and the legacy MessageList path in App.tsx (~850 lines). The webview CSP now grants wasm-unsafe-eval unconditionally for Shiki's Oniguruma WASM. * fix(vscode-ide-companion): reset WebShell transcript state on session switch The experimental useAcpTranscript hook only consumed transcriptUpdate messages, so its reducer state survived session boundaries. When the extension switched sessions it kept the webview mounted and replayed the newly-selected session through ACP, causing the previous session's blocks to merge with the new replay (e.g. user text "alpha" from session A leaked into session B as "alphabeta"). Reset both the reducer state and the rendered blocks on the same boundaries the legacy message flow uses: qwenSessionSwitched (sent before the ACP replay of the selected session) and conversationCleared (new session). Adds a regression test that replays two sessions with a switch between them. * fix(vscode-ide-companion): harden WebShell transcript session boundaries - reset the transcript on `conversationLoaded` too, closing the same cross-session leak the previous commit fixed for `qwenSessionSwitched` and `conversationCleared` (agent reconnect posts only this boundary) - track the active session id and drop late `transcriptUpdate` frames whose `sessionId` no longer matches, so a previous session's trailing frames cannot contaminate the next session's timeline - seed the transcript from cached messages carried by `qwenSessionSwitched` so offline restores and load-failure fallbacks render their history instead of a blank timeline - dispatch `assistant.done` on `streamEnd`/`sessionLoadComplete` so the final assistant/thought block of a turn (or history replay) does not stay `streaming: true` forever * fix(transcript): harden identity and document export Close the latest review findings across transcript identity, VS Code rehydration, and the HTML export security boundary. - Terminate discrete ACP segments and bind automatic turn provenance - Reset VS Code transcript scopes across replay and reconnect lifecycles - Align Markdown sanitization and envelope budgets with document rendering - Run browser gates with Chromium and complete third-party notices * fix(vscode-ide-companion): adopt live ACP session id after load-failure fallback * fix(vscode-ide-companion): echo user prompt into WebShell transcript * fix(vscode-ide-companion): keep WebShell transcript expanded and clear of the composer * fix(vscode-ide-companion): surface local error and interrupt notices in the transcript area * fix(vscode-ide-companion): restore file-link opening from the WebShell transcript * fix(vscode-ide-companion): restore contributed copy commands for the WebShell transcript * fix(vscode-ide-companion): add localOnly marker to TextMessage state type * fix(vscode-ide-companion): restore /insight progress card and report link in the transcript UI * fix(vscode-ide-companion): finalize in-flight tools on timeout and pin session-switch seeding guard Map streamEnd reasons timeout/session_expired onto the reducer's error reason so abandoned mid-tool turns no longer spin forever (ceuI). Add qwenSessionSwitched cases with no messages field and an empty cache array; the no-messages case fails when the seeding guard is forced true, pinning its false side (ceuN). * fix(vscode-ide-companion): remove unreachable editMessage backend and dead submit options The user-message edit/rewind UI was dropped in the WebShell-transcript migration, leaving editTargetTurnIndex/onSubmitted options in useMessageSubmit and the full editMessage/rewind flow in SessionMessageHandler unreachable. Remove the dead options, the editMessage dispatch case, the rewind/snapshot flow with its recovery branches, and their tests (R1-8 direction b). * fix(vscode-ide-companion): drop write-only loadingMessage bookkeeping The waiting-message renderer was removed with the WebShell transcript migration and the user prompt is echoed into the timeline at send time ( |
||
|
|
0b8a926348
|
feat(serve): add workspace-scoped Skills runtime (#10697)
* feat(serve): add workspace-scoped Skills runtime * test(serve): update Skills capability baseline * fix(serve): address workspace Skills review findings * fix(serve): address follow-up Skills review findings * fix(serve): harden workspace skill management * fix(serve): harden workspace skills management * refactor(web-shell): remove inert skill management gate * fix(web-shell): scope skill actions to active workspace * fix(web-shell): keep workspace skills state scoped * fix(web-shell): retry ready skills catalogs * fix(serve): fail closed on unreadable skill directories * fix(serve): invalidate global skills cache on errors --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
e3d26283e6
|
ci: close the guard holes round 2 found in the lint lane (#10919)
* ci: close the guard holes round 2 found in the lint lane
Nine follow-up findings on #10756's guards, merged before the round posted; all confirmed, fixed here.
The serializer at the heart of the prelude-identity guard was wrong: JSON.stringify's array replacer is a property allowlist applied at every nesting level, so each step's nested with:/env: serialized as {} and the guard could not see the fields it claimed to guard — Checkout's ref/fetch-depth, setup-node's versions, and the TRUSTED_CI_PROFILE binding whose drift to a literal would skip every profile-gated step while reporting green. Replaced with a recursive canonicalizer. The divergence exemption is now the field, not the step: a whole-step exemption left the collector's uses: revision — the one action pin in the job — compared by nothing, so a retag to a mutable @v4 was invisible.
Order is now pinned too: name-keyed maps are order-blind, and the steps context reads '' for steps that have not run yet, so a profile producer parked below its consumers would evaluate every ci_profile == 'full' gate false and complete the lane green having executed nothing. And the payload itself — the twenty moved steps the job exists to run — was pinned by nothing at all: deleting Run Prettier, flipping its gate, or re-adding a copy to test all stayed green. An exact ordered-list assertion now holds the full-gated payload, keeps it out of test, and pins the two deliberately-different gates as different.
runs-on joins the pinned surface (whole-expression, equal to test's): pick_runner is the fork-trust boundary and a literal pool label array would bypass it whole while desyncing from the timeout expression that reads the same output. no-ak's trusted-profile consumer loop gains the third consumer.
Two fixes to the collector work itself: it moved to the job's last step — failure() is evaluated when a step is reached and never revisited, so at its old slot just after install it had already been passed, unfailed, before any lint step could die, and could only ever collect a prelude failure. And a failure-gated disk-state dump now precedes it: the install sampler dies with that step's trap EXIT, so the artifact otherwise held only install-window samples for a death in any of the 19 later steps. The dump appends; a bare > would truncate the window it exists to complement.
The lane's banner no longer overstates its case: the property that justifies the required-check role is deterministic outcome, not lightness — the bundle-closure step builds and bundles the CLI, the helper suites spawn subprocesses, and the pool contention the old text denied is exactly what the routed 90/45 ceiling prices. It also records the honest limit: the incidents that motivated the split failed in the unit-test step that stayed in test, and this lane would have been green on those merges.
All ten mutations red: nested fetch-depth drift, TRUSTED_CI_PROFILE literal, producer below install, payload deletion / gate flip / leak-back, collector retag, runs-on literal, collector at its old slot, dump removed.
* fix(ci): keep the failure dump when the disk is full, and pin four guard holes
Review round 1 found five holes in the guards this PR added. Four are fixed
here; the fifth is a comment that misstated its own mirror.
The state-at-failure dump wrote its only copy into the samples file — the very
file whose writability the dump exists to investigate. Under ENOSPC the
redirect failed, `2>/dev/null` and `|| true` swallowed the failure and its
status, and the step exited 0 having recorded nothing, so oncall could not tell
"dumped, nothing interesting" from "could not write". Mirror through `tee -a`,
which still emits the data to stdout when the file write fails and reports why
on a stderr the block does not swallow. An executed case occupies the target
path with a directory and asserts the dump survives in stdout while the step
stays green.
The other three are pins that did not bite:
- The shared-prelude floor stayed at 12 after the divergence exemption narrowed
and re-admitted the collector, making the intersection 13. A floor below the
real count lets any one shared step be deleted out of either job unnoticed.
- The serializer at the heart of this PR had no fixture. Both sides of every
comparison flow through it, so regressing the call site to the pre-PR array
replacer compares {} to {} and the drift loop reports green — the exact blind
spot the recursion replaced. Pin a known nested key on the serialized OUTPUT
rather than on the serializer alone, because the regression that matters is a
call site swapped back to an allowlist.
- Nothing pinned the `needs` edge that makes every classifier output this suite
reads non-empty at runtime; deleting it decouples the lane silently, dropping
runs-on to hosted and taking the degraded empty→full profile fallback.
And the collector's comment cited `test` as its mirror for lastness while
`test`'s own collector sits four steps from the end, so the hole this PR closes
here stays open there. Correct the comment and leave that job to a follow-up
rather than widen this change into a second job.
Mutations measured red: needs edge deleted; a shared prelude step deleted; the
serializer call site regressed to the array-replacer form; tee reverted to a
bare redirect, which reds both the pin and the executed case.
* ci: restore the blank line separating lint_and_static from web_shell_e2e_smoke
Appending the failure dump and the collector after the pre-existing blank
line left the job ending flush against the next job key -- the only one of
ci.yml's eight job boundaries with no separator. Semantically inert: the
parsed workflow hashes identically before and after.
* ci: correct the HELPER_TESTS file count in the lint lane comment
The comment justifying `lint_and_static` as a required check argues the lane
is not light, citing "the 23-file HELPER_TESTS battery". The env list at
ci.yml:82 enumerates 24 files and is byte-identical to main's, so the count
was wrong on arrival -- in the one paragraph that says it exists so the
required-check follow-up is argued "on true premises".
Same class as the collector comment
|
||
|
|
a9c045561f
|
ci: schedule dependency audits and use native secret protection (#11005)
* ci: schedule dependency audit daily
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci: track scheduled dependency audit failures
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci: avoid duplicate post-merge secret scans
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci: use native secret protection
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci: extract dependency audit issue tracking
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci: harden scheduled audit tracking
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* refactor(ci): simplify scheduled security checks
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(ci): address dependency audit review feedback
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(ci): fail closed on unknown audit results
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci: stream the audit output instead of capturing it
With an 8-minute cap and one retry this step can now run for sixteen
minutes. Capturing into a variable and printing after each attempt
returns leaves the job silent for that whole time and then emits
everything at once, already labelled a failure. `tee` puts the output in
the log as npm produces it.
`${PIPESTATUS[0]}` rather than the pipeline's own status: `defaults: run:
shell: bash` sets pipefail, so the pipeline would carry npm's code today,
but nothing in the file says the classifier depends on that. Reading
npm's slot directly means a successful `tee` can never stand in for it.
Scope note, because the review that asked for this expected more from it:
streaming does not rescue an advisory from a killed attempt. npm emits
its report at the end, so an attempt killed before that point has nothing
to show either way — verified by A/B-ing both shapes against a stub that
reports only at exit: the progress line survives in both, the advisory in
neither. What keeps advisories in the log is the 8m cap sitting above
npm's own 422s error ceiling, which is already in this branch. This
commit is a log-legibility change, not a correctness one.
Behaviour is unchanged on every arm, driven with a stub npm against the
`run:` block extracted from the YAML: clean exits 0 after one call; a
genuine high-severity finding exits 1 after one call and is not retried;
an endpoint error and a timeout each retry once and exit 1 and 124.
The contract test pins `| tee "$log"`, `${PIPESTATUS[0]}` and the absence
of the old capture, so a silent regression to buffering fails.
Not run locally: the vitest suites and actionlint; CI is the authority.
* fix(ci): quote the explicit audit shell
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
|
||
|
|
9062725ef4
|
fix(test): sync the pinned no-AK integration script list (#11050) (#11052)
The wiring test keeps a verbatim copy of the focused no-credential integration command so that changing which specs run without an API key has to be deliberate in both places. Adding the prompt-latency policy spec to that command updated only the manifest, so the comparison has failed deterministically since: the nightly release for v0.23.0-nightly.20260904.74fe3a659d lost its Quality Checks (Scripts) lane to exactly that assertion, which failed the aggregate quality gate and blocked the publish. Add the missing entry to the pinned list. Nothing about what CI runs changes; the pin stays an exact comparison. |