mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-26 00:53:48 +00:00
4137 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2855149d47
|
fix(core): detect long verbatim repetition loops in content and reasoning streams (#9668)
* fix(core): detect long verbatim repetition loops in content and reasoning streams The chunk-hash content loop rule only treats repeated 50-char chunks as a loop when their occurrences cluster within 1.5 chunk lengths (75 chars), so a verbatim-repeated unit longer than that (the ~300-char analysis block chanted in issue #1775) never fires. Add a long-period rule: five equally spaced occurrences of an identical chunk mark a candidate period, and the spanned region is verified to be exactly periodic with that stride before halting. Raise the content history window so long units stay observable. Also route thought text into the content-repetition detectors when the structured thought check does not fire: OpenAI-compatible providers stream reasoning as thought parts that getResponseText filters out of Content events, so chants in the thinking stage never reached the chunk-hash rules. * fix(core): isolate reasoning deltas from the content channel's markdown state Route thought-sourced text through an append-and-analyze-only entry point instead of checkContentLoop. Reasoning text is raw chain-of-thought, never rendered markdown: an unbalanced code fence in a thought used to flip the shared inCodeBlock parity — which nothing clears mid-turn — silently disabling visible-content chant detection for the rest of the turn, and list/heading-shaped thought deltas reset the shared history, erasing already-accumulated content evidence when a provider interleaves thought and content parts. * fix(core): grow the periodic-rule verified region with the repetition count The long-period rule only inspected the last five occurrences, pinning the verified region at 4 x stride + 50 chars: units of ~76-237 chars fell in a gap between the clustered rule's 75-char bound and the 1000-char region floor at any repetition count, and units of ~1 KB or more could never fit five occurrences into the 4000-char history window at all. Extend the candidate run backwards over the longest equally-spaced suffix of occurrences so the verified region grows with the repetition count, and once the history saturates accept a shorter run (>= 3 occurrences) when the entire retained region is verified periodic back to the history start, so earlier occurrences truncated out of the window cannot hide a chant. Also correct the constants' comments describing the rule's domains. * test(core): cover post-truncation chant detection after a long varied turn Add the realistic #1775 shape that had no positive coverage: a long varied turn filling the history window, then a ~700-char chant streamed as misaligned deltas. Asserts detection at exactly the fifth in-window occurrence, pinning MAX_HISTORY_LENGTH, truncateAndUpdate's index adjustment, and the long-unit case together — a shrunken window would fire early via the truncated-run path once the filler flushes, and a broken index adjustment would never fire. * fix(cli): widen chanting halt label to cover reasoning-stream repetitions Reasoning-stream chants fire CHANTING_IDENTICAL_SENTENCES via checkReasoningContentLoop, but getResponseText filters reasoning out of visible output, so the headless label 'repeated the same sentence in its output' sends users looking for a repetition that is never rendered. Widen the label to 'output or reasoning' and add a headless-path regression test asserting the wording. * refactor(core): share the append/truncate/analyze tail across loop channels checkReasoningContentLoop duplicated the streamContentHistory append, truncateAndUpdate, analyzeContentChunksForLoop tail of checkContentLoop, leaving the history contract in two copies that a future fix could let drift. Extract the tail into appendToContentHistoryAndAnalyze and call it from both entry points. * perf(core): compare periodic regions in place instead of slicing history isRegionPeriodicWithStride sliced up to ~4 KB of history per invocation. Near-periodic chants fail verification repeatedly while their occurrence runs persist, so once a run reaches length 5 the check fires on up to every streamed character -- a probe measured ~136 MB of transient copies over one 49k-char stream. Index the existing string directly instead; comparison semantics are unchanged. * fix(core): reset stream-content loop state on retry replays and model fallback A replay (non-continuation) retry re-streams the failed attempt's content and reasoning through the chunk detectors — the #7832 transport-replay gate admits thought-only cuts, and with deterministic decoding the re-stream is verbatim. The Retry case in addAndCheckHeuristicLoops cleared only the tool-call counters, so the accumulated identical copies could fire CHANTING_IDENTICAL_SENTENCES mid-way through an otherwise healthy attempt. Continuation retries (isContinuation) keep the delivered text and append new output, so their state stays. ModelFallback had no case at all: the fallback model restarts from scratch, so mirror the replay resets for it. A genuine chant simply re-accumulates after the restart. * perf(core): defer content-history truncation with a hysteresis slack Once streamContentHistory saturates, truncateAndUpdate walked the whole contentStats map on every streamed event — Θ(window) entries in steady state, since the stride-1 sliding window hashes every position (~385 µs/event at window 4000 vs ~12 µs pre-saturation). With high-frequency small reasoning deltas now routed through the path, healthy long-thinking turns paid thousands of events of synchronous CPU. Trim only when the length exceeds MAX_HISTORY_LENGTH by a TRUNCATION_SLACK margin (1000 chars), slicing back to exactly MAX_HISTORY_LENGTH, so the index-rebase walk is amortized over appended chars. The change is behavior-neutral: the detection rules now always operate on the logical window of the last MAX_HISTORY_LENGTH chars — occurrences the window has passed are dropped at lookup (the exact set a per-event trim would have removed) and the periodic rule's escape valve verifies from the window start, i.e. exactly the content a fully-trimmed history retains. Tests pin pre-change fire offsets across saturation and multiple trims, plus the deferred-trim mechanics. * feat(core): log a chanting-region excerpt on loop halt for debug A reasoning-channel halt exits headless runs with empty stdout and only the loop-type label on stderr; neither the LoopDetected event (loop_type + prompt_id only), telemetry, nor any log carried an excerpt of what repeated, leaving no way to tell a true repetition from a detector misfire without instrumenting a repro. Capture one period of the matched region (the span between the last two occurrences, capped at 80 chars) when the chanting detector fires and emit it through the config debug logger at the firing site. The LoopDetected event contract is deliberately unchanged. * fix(core): preserve subagent continuation retries * test(core): cover plain subagent retry forwarding * fix(core): omit plain retry continuation flag |
||
|
|
b2edb80a57
|
fix(cli): probe microphone permission on recording start, not voice warmup (#8912)
* fix(cli): probe microphone permission on recording start, not voice warmup Voice warmup called recorder.microphoneStatus() as soon as the input prompt mounted with voice dictation configured. On macOS an undetermined TCC status maps to 'prompt', so every startup appended a "Voice dictation needs microphone access" notice to the chat history, including for users who never record. warmupVoice now only preloads the recorder backend. The permission probe and its 'denied'/'prompt' notices move to a checkMicrophonePermission callback that useVoiceInput invokes from startRecording, so the notice reaches only users who are actually trying to dictate. The dedup ref moves up to Composer and reaches InputPrompt as an optional prop, matching clipboardUnavailableShownRef. A per-instance ref reset on every InputPrompt remount, which is what produced the duplicate notice. Fixes #8877 * fix(cli): hold voice mic-permission dedup in AppContainer, not Composer Dialogs (tool approvals, auth, settings) swap Composer out of the layout, so a ref held in Composer reset on every dialog round trip and the notice could repeat on the next recording. The ref now lives in AppContainer, which owns dialogsVisible and never unmounts, and reaches InputPrompt through uiState like mainControlsRef. Also from review: delegate setupRecorder to setupRecorderWith in the InputPrompt tests, cover the prompt->denied status transition (re-warns as an error), and assert Composer forwards the session ref with stable identity across input-active toggles. |
||
|
|
78eadd4bf1
|
feat(core): declare create_sub_session only under qwen serve (#9425)
* feat(core): declare create_sub_session only under qwen serve create_sub_session needs the daemon bridge, which only exists under `qwen serve`, yet it was declared in every session. Interactive TUI and headless runs therefore carried a tool that can never succeed, polluting the model's action space and ToolSearch results. The tool is now registered by the ACP session at the same point it wires the sub-session spawner, so it exists exactly where it can work and nowhere else. * fix(core): keep create_sub_session on registries built with a wired spawner Dropping the unconditional registration also dropped the tool from every registry rebuilt after the daemon session starts: sub-agent and override registries are built through createToolRegistry with forSubAgent, and copyDiscoveredToolsFrom carries discovered tools only, never built-ins. Daemon sub-agents therefore lost the capability silently. Restore the lazy registration but gate it on a sub-session spawner being wired onto the Config, so interactive, headless and SDK runs still do not advertise a tool that cannot work there, while daemon sub-agent and override configs pick it up through prototype delegation. Going back through the lazy path also restores the PermissionManager.isToolEnabled gate for these registries. Harden the negative test to assert on both registration entry points; a regression that re-adds the tool eagerly never touches registerFactory, so the previous assertion would have stayed green. Add a positive test covering a subagent registry rebuilt after the spawner is wired. * docs(core): align setSubSessionSpawner doc with the new gate The setter's JSDoc still described the pre-PR behaviour — that leaving the spawner unset makes the tool report itself as daemon-only. With the spawner-gated registration the tool is never registered in interactive TUI or headless, so nothing reports anything there, and the comment contradicted the three sibling doc sites this PR already updated. * fix(cli): permission-gate the daemon create_sub_session registration The eager registration in the Session constructor called ToolRegistry.registerTool() directly, which honors only `tools.disabled` — so a daemon whose operator restricts `tools.core` or denies the whole tool still advertised create_sub_session and failed every call with EXECUTION_DENIED, exactly the "declared but unusable" pollution this change set out to remove. Registration now lives in an awaited helper that applies the same PermissionManager.isToolEnabled() check the core-side gate in createToolRegistry applies, and the daemon calls it once per session it creates, after the Session has wired the spawner and before the session is published. Also drops the unused CreateSubSessionParams public export. * fix(cli): declare create_sub_session only on daemon-backed sessions, revealed to the model Address the R4 review findings: - Wire the sub-session spawner only when the daemon's QWEN_CODE_SERVE=1 stamp is present. A standalone --acp session's peer is the editor, which answers the bridge's qwen/control/* ext methods with JSON-RPC -32601, so the tool was declared there but could never run. Gate registerCreateSubSessionTool on the spawner being wired so the tool exists exactly where it can execute. - Reveal the deferred tool and refresh the declaration snapshot after registering: the registration lands after startChat() froze the chat's declarations, so without the reveal the model was never offered the tool for the session's first lifetime. - Pin that newSession awaits the registration before the session is served, so the first prompt's declarations always include the tool. * fix(core): pin the create_sub_session reveal across /clear resets The reveal applied at daemon-session creation was permanently lost by the first /clear whenever the deferred-tool startup preload did not fit its all-or-nothing schema budget (or was disabled by a <= 0 / non-finite operator threshold): resetChat() clears the revealed set, the preload restores nothing, and registerCreateSubSessionTool never re-runs — the tool silently dropped out of the declaration list for the rest of the session. Add ToolRegistry.pinDeferredToolReveal(): pinned reveals are session-setup state (not ToolSearch discovery) and are re-applied by clearRevealedDeferredTools() while the tool stays registered and deferred, so the fresh session's startChat -> setTools() re-declares it. Pin create_sub_session at registration. * test(cli): pin create_sub_session registration on the permission-manager-enabled path * docs(core): correct DAEMON_ONLY_MESSAGE reachability in create-sub-session header Per wenshao's runtime verification (N2): the guard is reachable only for a daemon session whose spawner was cleared mid-flight; in non-daemon sessions the tool is absent from the registry, so a stale direct call hits the registry-miss error before execute() is reached. |
||
|
|
a369b4fac6
|
fix(cli): prevent input border overflow on resize (#8991)
* fix(cli): prevent input border overflow * fix(cli): harden border width invariant |
||
|
|
9b27184903
|
fix(cli): normalize win32 drive-letter casing in MCP approval keys (#9779)
* fix(cli): normalize win32 drive-letter casing in MCP approval keys Windows paths are case-insensitive, but the two entry points that produce a project root disagree on casing: the CLI stores process.cwd() as typed (D:\project) while IDE integrations pass VS Code's workspaceFolders[0].uri.fsPath with a lowercased drive letter (d:\project). normalizeProjectRoot() only resolved the path, so an approval recorded by the CLI was invisible to the IDE and the server showed as configured-but-pending. Fold case on win32 following the existing getProjectHash()/sanitizeCwd() convention, and fold stored keys at load time so decisions written by older builds are not orphaned; duplicate-cased keys merge into one entry and are rewritten normalized on the next save. Fixes #9775 * fix(cli): address review feedback on win32 MCP approval key folding Fold only win32 absolute paths (drive-letter/UNC) at load time so foreign POSIX keys synced from a Linux machine round-trip verbatim (R1-4). When duplicate-cased keys collide, prefer the rejection over an approval: records carry no timestamps and file order does not track recency, so a stale approval can never re-enable a server the user rejected (R1-2). Reuse isApprovalRecordMap in setState instead of the inline duplicate predicate (R1-3). Make approve.test.ts persistedStatus() fold the lookup on win32 to match the stored key shape, so the suite passes on Windows runners whose temp path contains uppercase letters (R1-1). Add win32 regression tests: non-drive component casing, POSIX-key preservation, rejection-wins merge (R1-5). * fix(cli): address round-2 review feedback on MCP approval key folding Use a null-prototype merge target so a server named __proto__ keeps its decision when duplicate-cased project keys merge on win32 (R2-1). Skip non-record values during merge so a corrupt null record no longer throws and poisons the whole approvals file (R2-2). Pin the rejection-wins merge invariant across both key orders (R2-3) and add a legacy UNC-key fold case (R2-4). * test(cli): cover null-record guard when merging case-collided win32 keys The non-record-value guard in mergeApprovalRecords (added for R2-2) was only reachable on win32 via normalizeStoredProjectKeys and had no test exercising the merge path, so deleting it shipped green on every platform. Add a win32-gated case: two case-variant keys for one project where the later-iterated key holds a null record value. Assert the load succeeds with no errors and the valid decision under the other casing survives. Addresses the round-3 review suggestion. * chore: re-trigger automatic review --------- Co-authored-by: zhou2024NAU <zhou2024NAU@users.noreply.github.com> |
||
|
|
eea98f3b04
|
refactor(cli): extract ACP skill management (#8865)
* refactor(cli): extract ACP skill management * test(cli): cover ACP skill safety guards * fix(cli): harden ACP skill mutation guards * fix(cli): handle ACP skill frontmatter variants * test(cli): deduplicate ACP skill fixtures * fix(cli): handle multiline Skill enablement fields * fix(cli): recognize escaped Skill enablement keys * refactor(cli): restore ACP skill extraction scope Restore the three post-review files to the initial extraction commit. The removed changes addressed pre-existing Skill behavior and test coverage rather than regressions caused by the module split. Latest origin/main changes only unrelated ACP agent sections, so no extracted Skill logic needs to be carried forward. --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
ed5c56d840
|
fix(core): clear tool display list before awaiting completion callback (#9602)
* fix(core): clear tool display list before awaiting completion callback The TUI completion callback commits the finalized tool_group to history and then awaits the tool-result continuation, which since #9121 spans the entire next model turn. The display-list clear was chained after that callback in the finally block, so the completed group stayed in the live pending list - pinned at the bottom of the virtualized list - until the next tool call arrived or the loop ended (#9420, regression in v0.21.13; v0.21.12's fire-and-forget submission cleared same-frame). Notify observers that the display list is empty immediately before invoking the completion callback (no await in between, so the clear and the history commit land in the same React render); the finally-block notify remains as the error-path fallback. Adds a regression test that fails on main. * test(core): strengthen finally-notify assertion in display-clear regression test (#9602) * fix(cli): hold in-flight flags across the tool completion callback window (#9602) --------- Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
3a1f86d805
|
feat(review): give verifiers a do-not-refute list and a constructible rejection bar (#9799)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(review): give verifiers a do-not-refute list and a constructible rejection bar Step 4's verifier brief already floors uncertain Criticals at low confidence instead of rejection, but it never names the states in which "too speculative / depends on runtime state" is not a valid rejection. The finder side carries the recall rule (do not silently drop a candidate); the verifier side lacked its counterpart, so real-but-uncertain findings could die in Step 4 on a plausibility vote instead of surfacing under "Needs Human Review". Close the same leak on the verifier side: - Rejection is now defined as direct counter-evidence constructible from the code — one of four shapes: factually wrong (quote the misread line), provably impossible (type/constant/invariant, shown), already handled in this diff (cite the guard and show it covers the trigger), or pure style / an Exclusion Criterion. A rejection constructing none of them downgrades to confirmed (low confidence) instead of dropping. - A third masquerading state joins "I could not verify it" and "its evidence is somewhere I did not look": "it is too speculative". A finding whose failure scenario names a realistic state the code does not exclude is PLAUSIBLE by default — concurrency races, nil/undefined on a rare-but-reachable path, falsy zeros treated as missing, off-by-one on a boundary the code does not exclude, retry storms and partial failures, patterns that lost an anchor. SKILL.md's Step 4 summary and the user-facing code-review docs are synced to the new semantics. The pinning test asserts every shape, every ground, and the downgrade consequence — a mutation flipping the consequence into "reject" survived the subject-only assertion, so the consequence clause is pinned too. Fixes #9789 * fix(review): sync the rejection-bar summaries with the brief's four grounds (#9799) * fix(review): sync the plausible-by-default wording and re-head the probe option (#9799) --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
fd9c452dc8
|
fix(auth): let Vertex AI authenticate with Application Default Credentials (#9017)
* fix(auth): let Vertex AI authenticate with Application Default Credentials Vertex AI auth required an API key, so an ADC or service account setup could not start. Supplying a placeholder to satisfy the check made it worse: an explicitly passed key switches the Google SDK to Vertex Express mode, which clears the project and location and rejects the request with "API keys are not supported by this API". Treat a configured GOOGLE_CLOUD_PROJECT as sufficient credentials for the vertex-ai auth type, in both the CLI pre-flight check and the core model config validation, and leave the API key absent so the SDK resolves ADC itself. The missing-credentials errors now mention the keyless path instead of pointing only at envKey. Fixes #9016 * fix(auth): select Vertex mode explicitly and keep declared key vars authoritative Review follow-ups on the Vertex ADC change. Vertex mode no longer depends on the GOOGLE_GENAI_USE_VERTEXAI side effect. Only the CLI pre-flight check writes that variable, and the startup call to it sits under the sandbox branch, so a plain interactive or ACP session built a client pointed at the Gemini API endpoint instead of Vertex. The flag is now derived from the auth type at construction, and left untouched for the other auth types so the SDK keeps its own environment fallback there. An entry that declares its own key variable no longer falls through to ADC when that variable is unset. It keeps failing on the declared variable, so a secret that failed to inject cannot silently authenticate as a different principal. The keyless hint is suppressed for those entries as well, since it would be advice that cannot work. The ACP pre-flight cell reports an indeterminate state for a keyless Vertex setup rather than a confirmed token: a configured project is routing configuration, not evidence that a credential resolves. All three gates now share one definition of a configured project, so whitespace is handled the same way everywhere, and the CLI missing-key message carries the same keyless hint as the core errors. Docs corrected on two counts: the environment-only row now says a keyless setup must select the auth type explicitly, since it is not inferred from the project alone, and the provider note names every key source the resolver folds in. --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
c2d63fbe58
|
fix(web-shell): show reasoning effort before session creation (#9599)
* fix(web-shell): show reasoning effort before session creation * fix(web-shell): harden reasoning preview lifecycle * chore(desktop): refresh frozen bun lockfile * fix(web-shell): restore reasoning preview after session clear * test(webui): pin session-clear model restoration on all reset paths (#9599) Witness the four back-to-welcome reset handlers' models re-projection (session_closed, stream auth failure, terminal stream error, heartbeat clear) with mutation-visible assertions: each test attaches a session whose live context displaces the provider models, then verifies the workspace reasoning preview returns after the reset. Also pin the providers-absent fallback in getConnectionAfterSessionClear so older daemons keep the pre-clear model list. --------- 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> |
||
|
|
d52bb4d678
|
fix(config): allow prompt hooks in settings schema (#8779)
* fix(config): allow prompt hooks in settings schema (#8752) * fix(cli): make prompt hook schema test type-safe * test(cli): preserve hook type schema coverage --------- Co-authored-by: nothing <nothing@U-DQY4PXFJ-0222.local> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
4ddbf227e8
|
feat(mcp): add MCP 2026 core and WebShell Apps host (#8992)
* feat(mcp): add 2026 protocol negotiation * feat(mcp): render MCP Apps in WebShell * fix(mcp): keep legacy tool discovery lenient * fix(mcp): keep Apps HTML out of TUI and honor tool visibility TUI and history compaction dumped mcp_app HTML as JSON, and discoverTools registered app-only tools for the model. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): stabilize AppBridge lifetime and close sandbox CSP gaps Theme toggles and transcript reseeds were tearing down MCP Apps; the host CSP also allowed any loopback port and form posts bypassed connect-src. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): list under-declared modern MCP capabilities over the wire v2 typed helpers return [] without a request when a capability is omitted. Use them only when the server declared the capability, and keep Apps unmounted in collapsed tool rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep Apps sandbox reachable and list past 64 pages Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): reject empty compacted html and keep MCP Apps expanded in multi-tool groups Fixes R3-1 and R3-2 review comments: R3-1: getMcpAppDisplay now rejects empty html strings (from compaction) so session replay shows fallbackText instead of mounting an empty iframe. R3-2: ToolGroup now checks for MCP apps across all tools (not just singleTool), auto-expands when any tool has an MCP app, and keeps MCP app rows expanded (summaryOnly=false, forceExpanded=true) even when adjacent tool calls are merged into the group. * feat(web-shell): fold thinking into the compact-mode tool summary (#9148) Compact mode used to drop thinking messages entirely, so a running turn gave no indication of the thinking step. Keep the thoughts and aggregate them with the adjacent tools into one summary: a streaming thought reads "Thinking…" with the running shimmer, and a completed thought settles into a click-to-expand row in its original interleaved position. The translate action is preserved on both the thinking block and the folded thought rows, and the merged group gets a synthetic id so its expanded state never leaks into non-compact mode. Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> * fix(mcp): address app discovery and sandbox regressions * fix(web-shell): keep MCP apps expanded in compact summaries * Revert "feat(web-shell): fold thinking into the compact-mode tool summary (#9148)" This reverts commit ab2eebc5d36f17a51ce94e423db5745dcbb273fe. * fix(web-shell): render compacted MCP App fallback and teardown before unload Compacted history keeps type:mcp_app with empty html; show fallbackText instead of a blank sandbox, and wait for ui/resource-teardown before unloading the iframe. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): bound the discover probe and raise the daemon bundle cap Silent legacy stdio servers inherited the 10-minute request timeout for server/discover. Cap the probe at 5s so fallback fits the discovery window, and raise the browser bundle budget after the main merge overflowed CI by 47 bytes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): skip version-negotiation probe on remote transports SDK v2 rejects HTTP server/discover timeouts without falling back to initialize, and the 5s probe consumed the entire remote discovery window. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web-shell): keep MCP App iframe src across deferred teardown Deferred unload() was clearing src on the live iframe after a remount, so the new AppBridge never saw sandbox-proxy-ready. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): honor listing-level MCP App CSP and permissions registerAppResource puts ui.csp/permissions on resources/list, and resources/read does not merge that metadata into content entries. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): reuse session client for list and emit app fallback text Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep mcp list and IPv6 sandbox CSP valid Give qwen mcp list leftover handshake budget after the 5s discover probe, and stop emitting invalid [::1] CSP origins. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep modern list and short discovery budgets working Drop the era-illegal ping after mcp list connect, shrink the stdio discover probe to the discovery window, and document that remotes stay on legacy initialize until the SDK can fall back. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep the 2026 slice free of review-only extras Drop the global tools/list page cap, generated companion notices, and the review screenshot so this PR stays on stdio 2026 plus the WebShell Apps host. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): restore generated companion notices after the SDK v2 bump CI regenerates NOTICES.txt from the lockfile; the file has to ship with the new MCP client dependencies. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): isolate the Apps proxy from WebShell storage Drop allow-same-origin on the outer sandbox iframe so a default localhost daemon cannot read the WebShell session token. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): harden fallback and app sandbox * fix(core): preserve large and app-only MCP catalogs * fix(mcp): preserve legacy negotiation compatibility * fix(mcp): default stdio negotiation to legacy --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: ytahdn <1294726970@qq.com> Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> Co-authored-by: YungSen Hsin <yungsenhsin@U-G0HXNQM1-2052.local> |
||
|
|
56db17bd4c
|
refactor(cli): enforce utils leaf-layer dependency direction (#9146) (#9737)
* refactor(cli): enforce utils leaf-layer dependency direction (#9146) Move domain-coupled modules out of packages/cli/src/utils into the directories that own them: config/ (dialogScopeUtils, settingsUtils), i18n/ (languageUtils), ui/ (handleAutoUpdate, standalone-update, systemInfo, systemInfoFields, update-relaunch, commands, doctorChecks), nonInteractive/ (nonInteractiveHelpers, chat-recording-failure, tool-result-boundary-diagnostics, permission-suggestions), serve/ (sandbox), services/housekeeping/ (scheduler, non-interactive-scheduler), and commands/review/ (findings). Extract the generic normalizePartList helper into utils/normalize-part-list.ts so utils consumers keep importing downward, and move the MergeStrategy enum into utils/deepMerge.ts (its owner). Add an eslint architecture rule (no-utils-upward-import) that forbids value imports from utils/ back up into a domain directory. Type-only imports stay exempt: they are erased at compile time and cannot create a runtime cycle (Settings in modelConfigUtils, CommandContext in sessionPaths). No behavior change: typecheck, build, and the affected unit tests pass. * fix: use Qwen Team 2026 license header on new files (#9146) * chore: refresh stale utils/ path references after leaf-layer move (#9146) * docs: reconcile no-utils-upward-import header with the allowed type-only set (#9146) * fix(cli): allowlist sandbox process.env accesses after leaf-layer move (#9146) * chore(ci): re-record qwen-autofix.yml size baseline after #9677 (#9146) #9677 recorded qwen-autofix.yml at 392111 bytes while the file it committed was already 397656, so every PR that merged main after it tripped the growth ratchet. Re-record the actual size; the file itself is unchanged by this PR. * fix(review): drop the stale utils/findings.ts digest root after the leaf-layer move (#9146) The #9146 move returned findings.ts to commands/review/, but the digest root lists merged from main still pinned it under utils/, where the file no longer exists — the absent root darkened every review's staleness check and failed review-source-digest.test.ts. Drop the stale file-shaped root from both digest copies and their pins; the commands/review/ directory root covers the validator at its new home, and the two utils helpers keep their file-shaped roots. * fix(review): colocate seatbelt profiles with the sandbox module (#9146) * fix(review): exempt inline type-only specifiers from the utils upward-import rule (#9146) * fix(review): report upward inline type-specifier imports under verbatimModuleSyntax (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): pin mixed-specifier and zero-specifier upward imports in the utils rule (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): anchor the nested-checkout utils rule fixture on the last marker (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): pin that the utils/findings.ts digest root stays removed (#9146) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(review): reword stale-bundle SCOPE header to the post-move helper shape (#9146) * test(review): drop the pre-move utils/findings.ts from the skill-parity fixture (#9146) * test(serve): derive the seatbelt colocation tripwire from BUILTIN_SEATBELT_PROFILES (#9146) * fix(architecture): fail closed on computed dynamic imports in the utils leaf rule (#9146) * fix(cli): point settings.test.ts at the post-move settingsUtils path (#9146) main updated settings.test.ts after this branch moved settingsUtils.ts from utils/ into config/, and the merge kept main's old import specifier, which vite fails to resolve. Repoint it at ./settingsUtils.js; every other consumer already uses the new path. * fix(cli): close utils boundary review gaps * test(cli): cover utils boundary allow paths --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
f877fb3525
|
feat(review): add the persistently-critical convergence advisory (land-with-residual-risk) (#9526)
* feat(review): add the persistently-critical convergence advisory
The severity floor converges a healthy loop — Suggestions stop posting and
the volume falls to the Criticals, then to zero as those get fixed. But a
loop whose Criticals never clear — the security-sensitive PR under
adversarial review — posts Criticals every round forever: the floor engages,
the Suggestions stop, and the volume flatlines at the Critical count instead
of falling. Nothing before this said so.
This adds the shape detector and its ONE recommendation:
- lib/convergence.ts — `convergenceAssessment` computes one fact from the
carried telemetry (Criticals stood in the previous round's work-list AND
stand again this round, with the two-round posting window present and not
shrinking) and, when it fires, returns the `land-with-residual-risk`
recommendation. Pure data, never authority: no threshold, no blocking, no
merge/close — every input degrades OPEN, so absence is fail-safe, never a
suppressed finding.
- compose-review wires it: `prevLedgerFacts` now recovers the previous
work-list's Critical presence beside the round and volume; the assessment
surfaces on three surfaces — a structured `convergence` field on the
composed JSON, a rank-1 non-capping body disclosure, and a terminal
CONVERGENCE line — each advisory-only and self-disclaiming, with a blank
residual-risk inventory scaffold (attack surface · attacker-dependency ·
blast radius) for the maintainer's risk-acceptance decision.
The exit the floor cannot provide: when the loop is provably stuck on
Criticals, the tool names the maintainer's decision (merge, carrying the
residual risk) instead of opening another round. Advisory only — it never
blocks this review.
Closes the convergence-exit gap in #9278; evidence and design in #9410.
* fix(review): surface the convergence advisory on every reachable event, gated on floor engagement (#9526)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): give the trimmed convergence advisory its own disclosure rank (#9526)
The advisory shared trim rank 1 with the deferral display, but every
rank-1 disclosure surface names "the deferred-findings list" — a fired
zero-deferral round whose body overflowed posted a trim notice asserting
a deferral list that never existed while the dropped advisory went
unnamed. The advisory now holds its own rank (and RANK_NAMES entry),
yielding after the deferral display and before the not-reviewed
disclosures. Adds the overflow fixture that pins the yield and the
relocated-arm firing fixture that pins the third thisCriticals term,
and corrects the prevLedgerFacts threat docstring: under `auto` the
floor-engagement conjunct is forgeable via the carried round, so the
only unforgeable conjunct is this round's own standing Critical.
* fix(review): count the script-lint gate's Criticals in the convergence signal (#9526)
The persistently-critical signal read `thisCriticals` before the gate
pushed its Criticals into `bodyCriticals`, and the ledger work-list
feeding the next round's persistence half omitted them too. A loop
whose standing blocker is the deterministic [lint] gate — the exact
shape the signal exists to name — held the whole conjunction
semantically while the advisory stayed silent: the count was taken
before the array was complete, and the gate-only round recorded no
sev 'C' for its successor to recover.
The assessment now runs after the relocated and gate pushes and reads
the completed array with the same semantics as the verdict's own `c`
(the explicit relocated term drops with the push that already carries
it), and the gate's Criticals join the marker work-list. Adds the
handler fixture arming the gate end to end — advisory fires, marker
records sev 'C' — and pins both branches of the trim notice's
copy-location conditional, which had no oracle on either side.
* fix(review): close the round-5 oracle gaps on the residual-risk advisory (#9526)
Round 5 reviewed the merge that landed #9461 underneath this branch and
found four suppress paths the merge introduced with no end-to-end oracle,
plus one standing comment overclaim. Each finding was reproduced as a
surviving mutant before the fix and re-run after, so every test added here
is one that actually kills something.
R5-2 — `residualRisk` is carried into the durable artifact instead of being
omitted from it. The omission's stated reason ("the advisory rides the
persisted body") is false on exactly the rounds that need the record: rank 2
sheds before the not-reviewed disclosures, so a fired-but-trimmed round left
a maintainer reading `.qwen/reviews` a "did not fit" breadcrumb and none of
the facts behind the `land-with-residual-risk` call. Its sibling
`convergence` is allow-listed one paragraph up for that precise reason, and
the merge had put the two on opposite rules. Shape-checked like every other
field on this boundary rather than passed through.
R5-1 — the persistence conjunct had no silence fixture. Every firing
fixture carries sev `C` in the prev ledger, so replacing the derivation with
a bare `true` shipped the suite green while a round introducing its FIRST
Critical would fire `land-with-residual-risk`. Added a fixture whose
predecessor holds Suggestions only, all other conjuncts true.
R5-3 — the enforcement-vs-reporting floor reading had no oracle for the one
input where the two disagree: a genuinely ABSENT `severityFloor` at round
>= 6, which the reporting reading folds to `auto`. Every advisory fixture
passed `severityFloor: 'auto'` explicitly, so the swap shipped green and
would publish "The severity floor will not converge it" over a round whose
enforcement backstop moved nothing. Added a fixture with no `severityFloor`
key at all.
R5-4 — the two silence fixtures asserted only absences. `prevLedgerFacts`
swallows every recovery failure into round 0, so a predecessor that never
loaded produced the same silence and the arms they claim to pin were
vacuous. Both now assert the VOLUME line quoting the predecessor's volume
as a positive recovery sentinel.
R4-1 — the marker path's second `scriptLintGate` run is left in place: it
lives in a different function from the body composer's, threading the value
across would add a seventh positional parameter for plumbing, and the two
agree because the gate is pure in `planPath` over inputs immutable within
one synchronous compose. What was wrong was the comment claiming it was
"the same gate the body ran"; it now states the actual invariant and the
actual hazard — an edit that filters what the BODY pushes must change this
list too.
packages/cli: typecheck, ESLint and Prettier clean; src/commands/review
4301 pass / 1 skipped. Mutation matrix (compose-review + save-artifact,
475 tests): baseline green; `prevHadCritical: true`, the reporting-reading
swap, dropping `residualRisk` from the persisted verdict, and a vacuous
ledger recovery each turn the suite red.
* fix(review): act on the round-6 deferred list for the residual-risk advisory (#9526)
Round 6 posted no findings and deferred ten observations under the
convergence posture. Eight are addressed here; each was reproduced as a
surviving mutant first and re-run after, and the two that are not addressed
are recorded below with the reason rather than left silent.
Correctness:
- The volume window straddled a posture change. The round the floor engages
on compares a Critical-only volume against a predecessor that was still
posting Suggestions — a drop that is the posture, not the loop — and on a
flat pair the advisory could publish "the severity floor will not converge
it" after one round of the floor. `ConvergenceFacts` now carries
`prevFloor` and a recorded `o` predecessor suppresses. Read the way the
sibling diagnosis in the same module reads it: a floor that was never
recorded is not a floor that DIFFERS, so pre-field markers evaluate
exactly as before. Pinned in both directions — deleting the guard and
tightening it to reject unrecorded floors each turn the suite red.
- `noteTrimmedRanks`' tail clause keyed on the advisory instead of on the
disclosures. Over a combined rank-2-and-3 drop it read "another copy — the
advisory also rides the composed JSON", telling the operator the trimmed
set was backed up when the half that is not backed up was exactly the half
the sentence exists to rescue; over a rank-0 drop it read "their only
other copy" for a paragraph the composed result does carry. It now keys on
rank 3. The artifact stays unnamed here — naming it sent the operator to a
deferral list that does not exist, which the existing test caught.
- The terminal `RESIDUAL-RISK:` record spread one labelled line over seven,
six of them unlabelled, because the advisory carries a markdown table for
the body. Collapsed at the print site only: the pipes survive, so the
inventory's three columns still reach the operator on the round where the
body budget shed the formatted copy.
Accuracy of the record:
- The `PersistedVerdict` comment claimed `residualRisk` sheds "before
anything else". It is rank 2; `convergence` is rank 0. What they share is
that both CAN go.
- The bundled skill enumerated two of the four trim ranks and stated the
no-durable-copy rule without its exception. Both assertions in
`SKILL.test.ts` move with the prose.
New oracles (test-only):
- The advisory-only guarantee — the claim the whole feature rests on — was
unpinned: a fired round now asserts the event stays where the findings put
it and that `cappedBy` gains nothing.
- The floor-futility sentence was pinned only negatively; it now has a
positive assertion in both languages.
- The zh advisory's scaffold columns and its Critical-count interpolation
slot had no oracle. `FIRE` is deliberately asymmetric (2 Criticals, volume
3/3) so a template reading the wrong slot shows.
- The rank-ordering guard could not tell rank 2 from rank 3; the combined-
drop test closes it — the `trim: 3 -> 2` mutant now fails five tests.
Not done, deliberately:
- The validator does not re-assert `criticals >= 1` / `posted >= prevPosted`.
Those are `convergenceAssessment`'s construction invariants, and a second
statement of them at the save boundary is a rule free to drift from the
first — with the artifact, the durable record, as what gets thrown away
when it does. Identity is pinned instead (`shape`, `recommendation`) and
the counts are shape-checked.
- The marker path's second `scriptLintGate` run stands (R4-1); the reasoning
is on that thread and at the call site.
packages/cli: 4305 pass / 1 skipped. packages/core skills: 376 pass.
Typecheck, ESLint and Prettier clean on both workspaces.
* fix(review): measure the residual-risk window on fresh findings, not totals (#9526)
Round 7 posted one Critical and it is correct. The volume conjunct compared
posting TOTALS, and Step 6 re-posts every still-standing ledger Critical
under its original id — so the total only ever rises and a converging loop
reads as a stuck one. Reproduced through the real `composeReview` before
touching anything: round 6 posts 5 first-time Criticals; the author fixes 3;
round 7 re-posts the 2 that stand and drafts 4 new. Fresh 5 -> 4 is a loop
settling, the total went 5 -> 6, and the advisory fired
`land-with-residual-risk` over it.
The window now runs on the fresh pair the marker already carries —
`postedFresh` and `prev.fresh`, the same numbers the loop-settling
observation in the same module trends on, so the two features cannot
disagree about what a round produced. `prev.fresh` absent degrades open.
Applying only that change would have introduced a second false fire, so it
does not ship alone. The posting total was silently covering a case the
fresh window is blind to: a reviewer finding nothing new for two rounds
while the author clears blockers sits at fresh 0 against fresh 0, which
"not falling" reads as stuck. Probed on the pre-change code — backlog 5 -> 3
with zero fresh both rounds is silent today (3 < 5) and would have fired
under a fresh-only window. The assessment therefore also takes the standing
Critical count and vetoes on observed shrinkage. A veto rather than a
requirement, on positive evidence only: the work-list it counts is the one
the marker's byte budget may have shortened, and an undercount can only hide
shrinkage, never manufacture it — so an unknown predecessor abstains instead
of silencing a genuinely stuck loop.
Unlike the sibling diagnosis, this signal does NOT require `prev.fresh > 0`.
That module is about a loop generating work; this one is about work that
never clears, and Criticals standing round after round with nothing new is
the shape itself, not a quiet loop. The backlog veto is what separates it
from a backlog being worked down.
The reported numbers are renamed with what they now measure — `posted` /
`prevPosted` become `fresh` / `prevFresh` on `ConvergenceFacts`,
`ConvergenceAssessment` and the persisted artifact — and the advisory prose
follows in both languages. Feeding fresh counts into fields printed as "the
posting volume" would have swapped one false record for another.
Verified as five shapes through the real command, then pinned as tests: the
reported fresh-shrinking loop is silent; the clearing backlog is silent; a
pre-fresh marker is silent; and both firing shapes still fire — the same
Criticals re-posted at zero fresh, and new Criticals every round.
Mutation matrix (539 tests): reverting the window to totals, deleting the
backlog veto, and tightening the veto to suppress on an unknown predecessor
each turn the suite red.
packages/cli: 4310 pass / 1 skipped. Typecheck, ESLint and Prettier clean.
* fix(review): prove the predecessor's floor enforced, don't trust its stamp (#9526)
R8-1 is correct. The posture-change guard paired two different readings
across the window's ends: this round's engagement is the strict
`criticalFloorInEffect`, but the predecessor's `floor` stamp is written from
`criticalFloorKind`, the reporting fold — which folds an absent
`severityFloor` into `auto` and stamps `c` on any round >= 6 the enforcement
backstop never touched. Reproduced through the real code first:
criticalFloorKind(undefined, false, 6) = 'auto-resolved' -> stamps 'c'
criticalFloorInEffect(undefined, false, 6) = false -> Suggestions post
so a predecessor that still posted Suggestions passed the guard, and the
advisory published "the severity floor will not converge it" one round after
enforcement actually started.
Neither fix direction the finding names is taken. Restamping the marker from
the enforcement reading would leave the sibling diagnosis comparing this
round's reporting stamp against a predecessor's enforcement stamp — the same
cross-reading defect moved into #9623's feature — and #9623 chose the
reporting reading deliberately, because its advice quotes the floor back to
the author. Special-casing a "newly named" floor needs the predecessor's raw
`severityFloor`, which no marker carries.
The evidence is already in the work-list instead. Enforcement moves drafted
Suggestions out of the posting set before the marker is built, so an engaged
round's list is Critical-only and an un-enforced one is not — measured
through the real composer across all four postures:
floor=critical (engaged) work list ["C"] stamp c
floor=auto, round 7 (engaged) work list ["C"] stamp c
floor ABSENT, round 7 (folded c) work list ["C","S"] stamp c <- the hole
floor=suggestion (not engaged) work list ["C","S"] stamp o
`prevPostedSuggestion` is that fact, and it suppresses on the POSITIVE
observation so the two ways it can be wrong land on opposite sides: a
shortened list that shed its Suggestion reads as engaged (the truncation
caveat the backlog veto already carries), while a pathless Suggestion an
engaged round left inline reads as un-enforced and costs one round of
silence. Unknown abstains, like every other fact read off that list.
Mutation matrix: deleting the guard, tightening it so an unknown predecessor
suppresses, and pointing the wiring at the wrong severity each turn the suite
red — the first on both the unit arm and the end-to-end fixture built from
the finding's own witness.
packages/cli: 4432 pass / 1 skipped. Typecheck, ESLint and Prettier clean.
* fix(review): refuse a pure-foreign work-list as this account's history (#9526)
Correct, and reproduced through the real composer before changing anything.
Recovery adopts the highest-round marker whoever posted it. Where that marker
was NOT merged over this account's own findings, this account's entries are
in no work list at all — the state `openCriticals` already refuses to infer
across, one screen up in the same function. Every prev-round fact this signal
reads comes off that list, and it read it unconditionally:
pure-foreign {foreign:true, merged:false} -> FIRES
own list {foreign:false} -> FIRES
merged {foreign:true, merged:true} -> FIRES
An own round-6 marker that was a clean LGTM (empty findings, fresh 0, floor
stamped `c`), a foreign same-round marker carrying Criticals and no
Suggestions winning recovery, and one Critical drafted this round were enough
to publish "Criticals stood in the previous round's work-list and stand again
this round — land-with-residual-risk" over this account's own LGTM.
All three list-derived facts are withheld on that state, not just
`prevHadCritical`: it alone silences the assessment today, but leaving the
other two reading a stranger's list is a hole waiting for the next edit to
re-open. `prevPostedSuggestion` in particular reads ABSENCE, and a stranger's
Critical-only list is exactly the shape that reads as "the floor enforced".
Merged foreign lists are deliberately NOT withheld: the union keeps this
account's own certified entries under their own ids, which is the part that
makes the list speak for this account again — the same distinction
`openCriticals` draws.
The test drives all three arms and asserts them as one table, so the fix is
pinned in both directions: a mutant disabling the gate fires on the stranger,
and a mutant widening it to any `foreign` marker silences the merged arm.
Both turn the suite red, as does un-gating `prevHadCritical` alone.
Not changed, and recorded rather than left implicit: a TRUNCATED work-list
still reads as this account's. Truncation shortens our own list, which is a
different thing from a stranger's, and the direction it errs in is already
documented on `prevPostedSuggestion` and the backlog veto. Requiring
completeness would silence the advisory on precisely the deep-work-list
rounds it exists for.
packages/cli: 4433 pass / 1 skipped. Typecheck, ESLint and Prettier clean.
* fix(review): stop a gate Critical compounding, and qualify a truncated reading (#9526)
Round 11's two Criticals. Both reproduced through the real composer before
anything was changed.
R11-2 — a standing gate Critical entered the posting set twice, and the pair
compounded. This is a regression from this branch's own commit
|
||
|
|
431a0bd9b0
|
fix(daemon): keep restored ask_user_question valid after load (#9763)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(daemon): keep restored questions valid across load, send, and replay Post-merge review of the restore path found illegal provider history, phantom rewind snapshots, dropped resume notices, and replay that finalized a question the load was about to re-hang. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): pin ask_user_question restore suppress wiring in acpAgent Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): skip persistence for a whole restored batch that ends unattended Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(core): pin restorable ask_user_question preservation on a real Config --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
f1b1305a76
|
feat(models): support dual-role image generation models (#9650)
* feat(models): support dual-role image generation models * fix(models): address dual-role image selector review * test(cli): cover image model resolver rejection * fix(models): preserve legacy vision image routes |
||
|
|
1e062a4d0f
|
perf(cli): raise VP scroll rendering to 60 FPS (#9681) | ||
|
|
509226260c
|
feat(review): back comment-status and presubmit for Aone Code targets (#9627)
* feat(review): back comment-status and presubmit for Aone Code targets
A second `--comment` round on an Aone MR re-posted every still-valid
finding as a new comment and never downgraded a self-MR review — both
flows were skipped for lack of a1 backing. Route Aone targets at the a1
reads (mr view / mr status / mr comment list / auth whoami) through the
same pure classification cores the GitHub path pins, so the report
schemas and the Step-7 downgrade semantics stay one contract:
parentNoteId threading, closed → resolved, outdated → stale (a
rewritten line stays re-postable), no commit anchors (code facts
degrade to unknown), and drift with no compare API fails safe. The
context-unavailable verdict cap stays until pr-context lands.
Closes #9613
* fix(review): harden Aone runners' pr_number guards and null gate payload
Address round-1 review findings on the Aone backing of comment-status
and presubmit:
- extractStatusChecks no longer throws a TypeError when a1 answers a
bare null to `mr status`; the payload now reads as the designed
unreadable gate state (undefined), capping the verdict like a
still-running check instead of crashing presubmit with no report.
- comment-status and presubmit validate pr_number with fetch-pr's
/^[1-9]\d*$/ grammar before Number() coercion, refusing '012'/'1e3'/
'0x1f'/' 12'/'12.0' tokens that would query a different MR than the
caller's label carries.
- Pin the two subject_type combinations no test covered (pathless
comment WITH outdated:true; the live path+line shape) with
mutation-probed assertions.
- Align the --host describes with the sibling commands' detection
wording (omission no longer promises github.com), name the real
bucket (`resolved`) in the review skill's Aone dedup note, and scope
the design doc's remaining-unbacked claim to its own section.
* test(review): pin the Aone dedup seams the round-2 review named (#9627)
Four mutation-verified pins on the existing Aone backing, each closing
a round-2 Suggestion:
- classifyAoneChecks: the continue-scan cell of aoneCheckState — an
unrecognized value in an earlier key beside a recognized verdict in a
later key reads the verdict, not pending (a first-present-key mutant
now fails)
- classifyAoneChecks: a context-keyed FAILED gate carries its name —
the passing context-keyed case pinned nothing because passing gates
never collect names
- both comment mappers: `note` beats `body` when BOTH keys are present
(`??` does not coalesce `body: ''`, so an inverted priority would
blank every recognition signal and re-post the whole review)
- aoneCommentToPresubmitComment: parentNoteId maps onto
in_reply_to_id, including the absent-stays-unset half
No source changes; each pin fails under its named mutant and passes on
the current code.
* test(review): pin the five Aone seams the round-3 review named (#9627)
* fix(review): align Aone comment reads with measured a1 facts (#9627)
* fix(review): read fully-dropped Aone checks array as pending, not all-clear (#9627)
* fix(review): match SKILL.md self-PR wording to the revert-guard test
The merge resolution reworded the self-PR note to "matched against the
'a1 auth whoami' account", but SKILL.test.ts's revert guard (#9616, #9627)
pins the exact phrase "the MR author is matched against 'a1 auth whoami'".
Restore the pinned wording (semantics unchanged) so the bundled-skill test
passes.
* fix(ci): record qwen-autofix.yml's actual size in the workflow ratchet
The workflow-size ratchet failed on this PR: qwen-autofix.yml is 397656
bytes but .size-baseline recorded 392111 (5545 over, allowance 4096).
The oversize was inherited from main, not introduced here: main's ratchet
commit (
|
||
|
|
7f2c4416b3
|
docs(review): name the axis when two of them both call it "rank 3" (#9759)
The trim-rank move gave `trim` a rank 3, and the file already used a bare
"rank 3" for the `keep` default on the last-resort cut's axis. The two mean
opposite things — `trim: 3` is the LAST rank the ladder sheds, `keep: 3` is
the FIRST thing the cut spends — so a reader landing on the cannot-tell
block's "Deliberately untagged (rank 3, spent first by the last-resort cut)"
now reads it against a comment three hundred lines up saying rank 3 goes
last.
Every mention of the number on either axis now says which axis it is on:
the cannot-tell block states the collision outright, and the four trim-side
mentions and the one keep-side test comment are qualified. The `keep`
comment that already disambiguated itself ("No `trim` rank rides here") is
unchanged.
Comments only, no behaviour. Raised by the review as a deferred, non-blocking
item; taken now because it is the same class of drift the previous commit
closed, and shipping the ambiguity would have seeded the next one.
|
||
|
|
72d3a845f7
|
fix(review): count a fix-induced re-report as first-time work (#9744)
* fix(review): count a fix-induced re-report as first-time work Closes #9674. A carried id has meant two different things since the fix-induced disposition shipped, and the volume trend's first-time count read both as re-posts. One is a re-post: a finding re-asserted under the id it already had. The other is a new defect wearing the id of the entry whose fix produced it, carried deliberately so the author reads one thread per churning site instead of a new one every round. Counting that as a re-post made the trend understate new work exactly where the loop was creating the most of it — measured on the pull request that introduced the disposition, a round that newly identified six defects and re-reported four of them under earlier ids recorded a first-time count of two. Neither count moves. They measure different things and both readings are correct, which is why the two reconciliations the issue rules out stay ruled out: excluding carried-id re-reports from the census would put the attributed count outside it and every such census would be refused as impossible, and counting them as first-time posts wholesale would tell the trend a re-assertion is new work. What was missing is the distinction itself, so the comment now carries it: a fix-induced re-report is marked, and the reader of drafted comments passes that through to the count. The marking sits after the id and its separator, never inside the id grammar. That grammar is shared with the ledger's own carry, so widening it to swallow a parenthetical would put a finding's identity on the same regex as a model-written adjective — a spacing the wider grammar failed to anticipate would stop matching the id and silently renumber the finding. Read after the id, nothing about the token can cost it, and the reading is correspondingly lenient about case and spacing because it governs only whether a comment counts as first-time work. An unrecognised marking leaves the draft counted as a re-post, which is what every round did before this existed; a marking wrongly added to a still-stands is the expensive direction, so the skill restricts the token rather than offering it as a way to flag any carried finding as interesting. The token is stripped from the claim before it reaches the work list. Left in, it would ride into the next round as part of the text Step 6 re-locates the claim by and the status table prints — machine vocabulary about how to count one round, outliving the round it described. Beside no id it is ordinary claim text and survives, because there is no entry there for it to qualify and editing a finding's own words on the strength of a word it opened with is not this token's business. * fix(review): resolve orphaned readback doc and record the fresh-count seam (#9744) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(review): name the zero-prev masking round in the fresh-count seam note (#9744) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(review): stop the blocker's docblock forbidding what this PR ships Three passages written when a carried id could only mean one thing now forbid the rule this branch adds. The blocker's docblock rules out "counting them as first-time POSTS" as one of two reconciliations that must never be made; the skill's census paragraph says the volume trend is the count "where a carried id is a re-post"; and a test comment restates the same premise. Each was true before a fix-induced re-report could be marked, and each now tells the next reader to undo the code beside it. The distinction the passages were protecting is real and stays. What they ruled out was reading a carried id as first-time work BY INFERENCE, which would count every re-assertion of a standing finding as new work — still wrong, and still what `isFreshDraft` refuses. What this branch added is narrower and is not an inference: the round marks the re-report, and only a marked one counts. An unmarked carried id is a re-post to the trend exactly as before, so the two counts still diverge by design; what is gone is the premise that a carried id can mean only one thing. The census paragraph gains the consequence that follows for whoever writes the comment: a fix-induced finding counted in the census but left unmarked in the body is counted by neither number. Prose only — no logic, no test assertion changes. Reported twice by the review as a deferred finding and left standing under the code-age rule, which is correct as a posting decision and not a reason to leave a contradiction in the file. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
ec8a8a1a97
|
feat(review): back pr-context on Aone Code targets (#9621)
* feat(review): back pr-context on Aone Code targets pr-context was the one read subcommand still gh-direct, so every Aone run was forced context-unavailable: the verdict capped at COMMENT (the wired a1 approval could never fire), Agent 0 skipped, and the machine ledger never recovered from posted summaries. Route it through the platform reader with a normalized context bundle; Aone serves it from mr view + the flat comment list (thread comments carry the ledger), GitHub's implementation is an extraction of the existing calls — its output stays byte-identical. The forced cap leaves the Aone write path for parity with GitHub's state-claim handling, and the refetch commands a context file emits bake --pr on Aone, where comment bodies are addressed per-MR. * fix(review): keep Aone ledger carriers out of the blocker re-check (#9621) On Aone this pipeline's own round summaries are path-less comments, so they ride pr-context's issue channel, where their visible **[Critical]** lines self-promoted every prior Critical-bearing summary into "Blockers to re-check" — rendering each prior Critical three times (beside the ledger section and the inline roots that own the same findings) and spending the section budget on the pipeline's own prose until genuine human blockers degraded to snippets. Exclude bodies carrying the ledger marker from issue-channel promotion and the stdout count, strip the marker out of the settled snippet, and switch the pr_number guard to the canonical isPositivePrNumber so 0x10/5. spellings cannot fragment side-file continuity. Pin the witnesses the round's findings name: the guard, args.host forwarding, the issue-kind --pr refetch branch, the account-first author keying, and the GitHub test suites' independence from the cwd-origin probe. * fix(review): refuse pr_number spellings that do not round-trip (#9621) isPositivePrNumber alone admits two spellings whose Number() value does not round-trip to the raw string: leading zeros (007 fetches 7 but the raw string labels the heading and the prev-ledger side file, so a later 7 run reads a different side file and the round counter restarts) and digit strings above Number.MAX_SAFE_INTEGER (Number() silently rounds them, fetching a different PR than the labels announce). Add the safe-integer and no-leading-zero conjuncts — matching fetch-pr's [1-9]\d* rule — so every admitted input satisfies String(Number(x)) === x. Also pin the witnesses the round-2 review names: the commit_id round-trip through the GitHub reader and toRawReview into the persisted side file (both spreads were unwitnessed), the stale force-applies comment in submit-aone.test.ts the cap removal outdates, and the setup batch's Aone carve-out for the unbacked comment-status call. * docs(review): align Aone docs with the landed no-ancestry anchor rule and comment-status skips D6 described the AGit-Flow anchor as inert until the incremental rule landed, but that rule (#9630) merged while this branch was in flight — anchors now delta-scope Aone re-reviews. SKILL.md's comment-status section and Step 6's report-existence guard now name the Aone skip the setup batch already carries, so no path sends an Aone run at the unbacked command or at a report that was never written. * docs(review): annotate #9616 as landed and define the report-less re-check rule The out-of-scope list still read self-PR detection as open work although #9629 shipped it into this branch's merge base — annotate it like the sibling #9618 entry. Step 6's report-existence guard pointed report-less runs at a re-derivation the skill never defines; replace it with the explicit rule: no per-thread status routing, no hand-derived substitute, rule from the code at the reviewed commit, cannot-tell over a guess. * fix(review): route the context head through aoneHeadSha and close the round-5 findings getReviewContext read sourceBranch raw while every other head read trims — a padded server value diverged the context file from the rest of the run (phantom-drift shape). getCurrentUser now honors the seam contract on the anomalous whoami shapes instead of leaking untagged throws and non-string accounts. Step 6's report-less rule no longer contradicts the comment-status failure contract: runs where the command ran and failed keep the "re-derive if needed" fallback. The Aone paragraph names comment-body among the backed reads, and witness tests pin the identity gate's carriers key and the head normalization. * fix(review): shape-check the Aone comment listing in getReviewContext a1 can answer repo mr comment list with an exit-0 a1.error/v1 error object (backend auth failure or client timeout — measured by cleanup's a1CommentList on the identical payload). Without a guard the object survives the ?? [] coalesce and .filter throws an untagged TypeError, losing the envelope's actionable message at exactly the recoverable moment. Guard as the provider family already does and surface the cause; witness tests pin both envelope shapes (mutant-checked). * test(review): pin getCommentBody's body-field fallback (mutant-checked) * fix(review): union resolved comments into the Aone context bundle The default comment list excludes resolved comments (measured by the cleanup audit) while GitHub's REST fetches include them, so a resolved blocker/marker root never reached the re-check walk or the fail-closed identity gate. Union the default and --resolved listings as the audit does, dedupe by id, fail closed on either listing's error envelope, and disclose the residual that resolved replies stay invisible; witness tests mutant-checked. * fix(review): serve resolved comments and guard the envelope in getCommentBody getCommentBody queried only the default comment list while the context bundle it serves refetches for unions in resolved comments — a resolved id named by a truncation note threw "not found" every time, and an exit-0 a1.error/v1 envelope threw an untagged TypeError that lost the actionable message. Extract the shape-checked default+resolved union helper and read both sites through it; witness tests mutant-checked. * ci: correct qwen-autofix.yml size baseline to its actual post-migration size #9677 shrank qwen-autofix.yml from 431526 to 397656 bytes (prose moved to the design record) but recorded the baseline at 392111, 5545 below the file's own post-change size, so the first PR to run the ratchet tripped it. This branch introduces zero growth to the file (byte-identical to main); the bump aligns the baseline with reality. No workflow content changes. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
98fa2e9770
|
feat(cli): enable dynamic workflows from a settings key (#9098)
* feat(cli): enable dynamic workflows from a settings key `ConfigParameters.workflowsEnabled` is declared, defaulted, and read by `Config.isWorkflowsEnabled()` — but `loadCliConfig` never writes it, so no setting has ever reached it. The only way to turn dynamic workflows on is the undocumented `QWEN_CODE_ENABLE_WORKFLOWS=1`, which has to be exported in every shell that launches qwen. AGENTS.md names this shape directly: an optional field that is declared and read but never set by any caller is a dead switch. Add `tools.workflowsEnabled` to the settings schema and populate the field from it. Precedence is unchanged and still resolved in core: `QWEN_CODE_DISABLE_WORKFLOWS` beats everything, then `QWEN_CODE_ENABLE_WORKFLOWS`, then the setting. Because `settings.merged` already folds the System scope, an operator gets a fleet-wide force-off with no extra code. `requiresRestart` is load-bearing rather than decorative: the Workflow tool is registered once while the tool registry is built, `/workflows` is gated when commands load, and keyword steering resolves at startup — so a mid-session toggle would leave the dialog claiming the feature is on while the tool is absent from the registry. The setting description also disambiguates it from the unrelated `experimental.sessionWorkflow` plan-and-review view, which shares the word "workflow" and would otherwise be easy to confuse in the settings dialog. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(cli): clarify workflow feature controls * test(cli): cover workflow command gating * fix(cli): restrict workflow opt-in scope * fix(cli): keep workflow opt-in user-owned * test(cli): cover workflow system scopes * refactor(cli): drive workspace-restricted settings from one list R4-3: the restricted set was hand-maintained in three parallel places — a per-key warning block, the condition in `stripWorkspaceRestrictedSettings`, and that function's destructure. Adding one restricted setting needed three synchronized edits, and either omission is silent: forgetting the warning discards a workspace value with no diagnostic, forgetting the strip honors a value the warning says is ignored. `WORKSPACE_RESTRICTED_SETTINGS` is now the single source, and the warning loop and the strip both derive from it. It lives in `settingsUtils.ts` rather than `settings.ts` because `settings.ts` already value-imports that module — defining it there and importing it back would close a runtime import cycle. R4-2: `tools.workflowsEnabled` is the first setting that is both `showInDialog: true` and stripped from Workspace scope, so the dialog offered a toggle that silently never took effect — it renders from the raw scope file, so it kept showing the value it wrote while the feature stayed at its merged value, leaving a dead entry in the repo's .qwen/settings.json. `getDialogSettingKeys` gained `excludeWorkspaceRestricted`, which the dialog passes when the selected scope is Workspace. The scope comparison stays in the component so settingsUtils keeps its type-only dependency on settings.ts. Unlike `showInDialog: false` (what the two pre-existing restricted settings use), the setting stays visible and editable under the scopes that honor it. Verified: settings 169/169, settingsUtils 85/85, BuiltinCommandLoader 13/13; packages/cli typecheck clean. Mutation-checked both ways — forcing the filter off fails 1 test, dropping a key from the list fails 3. SettingsDialog.test.tsx's 23 failures are pre-existing and environmental: identical counts on upstream/main and on this branch before the change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(serve): reject workspace-restricted settings at the daemon API too R8-1. The workspace restriction stopped at the TUI dialog. `stripWorkspace RestrictedSettings` drops these keys before every merge, so a workspace-scope write through the settings API persists a committable dead entry into the repo's `.qwen/settings.json` and answers 200 + `requiresRestart: true` while the feature never turns on — GET then reports `workspace: true` beside `effective: false`, and the warnings channel carries only `corrupted`, so the client never learns the write was inert. Exactly the trap the SettingsDialog comment in this same PR says it eliminates, one layer over. `tools.workflowsEnabled` is the first workspace-restricted key with `showInDialog: true`, which is what puts it in `getDialogSettingKeys()` and therefore in `getAllowedKeys()` — the two pre-existing restricted keys are `showInDialog: false` and never reached the API. Both POST handlers now call one shared `rejectWorkspaceRestrictedWrite`, answering 400 `workspace_restricted_setting`. One helper rather than two copies, for the reason the previous commit collapsed the warning/strip pair. User scope is untouched — that scope honors the key, and a guard that reached it would kill this PR's whole enablement path. Verified: workspace-settings 22/22, settings 169/169, settingsUtils 85/85. Mutation-checked three ways — dropping either call site fails a test, and widening the guard past workspace scope fails the user-scope test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
bd247128fd
|
feat(goal): account the tokens a Goal spends (#9583)
* feat(goal): account the tokens a Goal spends A Goal reported how many turns it had run and how long it had been active, but never what it cost. That is the number a user needs to judge whether a long autonomous run is worth continuing, and the one every future limit has to be expressed in — a budget cannot be enforced against a figure nobody keeps. `GoalRecord` now carries `tokensUsed`, summed across the Goal's turns by `reduceGoalTurnFinished`, and `get_goal` reports it in the unpermitted `lastGoal` summary beside the turn count. The figure comes from the chat recorder, which already receives every assistant turn's usage stamped with the Goal permit that produced it. Attribution is therefore settled where the spend is recorded rather than reconstructed afterwards from session totals: a user turn interleaved with an autonomous run belongs to no Goal turn, and a resumed session's replayed history is not a Goal turn's spend either. The runtime asks the ledger for one turn by id when that turn finishes, which is also why the accounting needs no coordination with session swaps. A runtime with no ledger, a ledger that throws, and a turn that made no model calls all bill zero rather than guessing, and none of them fails the turn. Goals recovered from a transcript written before the field existed restore with zero spend. No limit is introduced here — this only counts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(goal): cover recorder token accounting --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
f829a02896
|
feat(review): validate Aone inline anchors against the captured diff before posting (#9634)
* feat(review): validate Aone inline anchors against the captured diff before posting Aone Code performs no server-side anchor validation — a controlled probe (scratch MR 29427547, a1 v0.2.51) proved any --line integer posts, and an old-side number silently lands on the same-numbered new-side line. The old side cannot be anchored at all, and file-level comments drop their path. Pin the removed-line semantics for the Aone write path: submit's Aone branch now validates every well-formed inline anchor against the review's captured diff before posting. An unanchorable Critical is relocated into the summary body, an unanchorable Suggestion discarded and counted — the GitHub 422-recovery dispose, performed in code — each disclosed in the terminal. A missing captured diff refuses the whole post; malformed shapes (missing path/line, reversed range, renders-as-nothing) keep their consistency-gate refusals, and a garbage state.bodyCriticals stands the gate down so compose's pinned refusal fires. The GitHub path is untouched — its server performs this validation. Issue #9615 * fix(review): reject unpostable anchors and unify the Aone gate's shape refusals - validateNewSideAnchors now rejects the input domain (fractional/zero/negative lines and reversed ranges) before the hunk scan, so its verdict can no longer certify an anchor the zero-validation Aone platform would post silently wrong. - Extract the consistency gate's per-comment shape checks into one shared predicate (commentShapeProblems) read by both the loud refusal and the Aone anchor gate, so a shape the gate disposes is never a refusal the operator misses (open fence, start_line-without-side). The path check becomes a type check, closing truthy non-string paths that reached the write seam unvouched. * fix(review): generalise the Aone gate's stand-down and harden its relocated entries Round-2 review fixes for the Aone anchor gate: - The stand-down now keys on ANY degrade that touches the payload and covers every compose-owned garbage shape: bodyCriticals that is not an array of strings, or a suggestionsDiscarded compose's counter refuses. The countability test reads compose's OWN acceptance table (toCount, exported as the total tryToCount), so the gate's merge and compose's counter can never drift — an integer-but-not-safe count now merges instead of silently dropping the gate's discards. - The relocated entry's claim extraction strips a leading marker RUN (fixpoint, like every other strip) and treats a fence-delimiter claim line as absent — both shapes used to leak raw markers or junk delimiters into the posted summary-body blocker line. - The gate keeps the model-authored comment indices through its removal (and floor enforcement keeps them through its own), so the consistency gate's refusal names the culprit in the model's payload JSON instead of a renumbered position the re-compose loop cannot act on. - A --dry-run with a missing capture no longer exits 3: it writes nothing, so it skips the gate with a disclosure and reports wouldPost: false (reason: aone-diff-missing); the exit-3 refusal stays reserved for the real write. - The MULTI_DIFF fixture's second hunk header becomes byte-exact git output (@@ -20,0 +22,2 @@, probed against git itself). - The design doc gains the gate-relocation doctrine (relocated entries deliberately inherit the model's own tag-exemption treatment), the dry-run carve-out in the failure-shape table, and the corrected fence/one-line-channel claim. * fix(review): close the anchor-gate witness gaps and a footer-leak in the relocated entry Gap-fill on top of the round-2 gate hardening: - The relocated entry's claim extraction strips the canonical footer FIRST: with an empty claim line (a marker-plus-separator-only body), the separator strip eats the newline+colon and the extraction falls THROUGH into the appended footer's first line, posting it as the claim. Witness added for the placeholder shape. - Pin the multi-line relocation entry CONTENT (it must cite the claimed end line, not the start — the start sits inside the hunk and looks fine) and the disclosure naming it. - Witnesses for the remaining mutant-tested gaps: a range whose start sits outside every hunk and end inside (the startLine mapping), the dry-run compose parity (preview composes from the gate-corrected payload), the suggestionsDiscarded 0 merge boundary, the empty-path shape (loud refusal, never a gate disposal), a declared LEFT start_side without a start_line, and the equal-boundary range (start_line === line, a shape GitHub itself produces). - The routing suites run from a per-test fixture cwd, so the captured-diff seeding and its cleanup can no longer overwrite or delete a same-numbered live capture in the real vitest cwd. Issue #9615 * fix(review): sanitise relocated-entry paths and stand down over any compose-refused bodyCriticals * fix(review): keep the anchor gate's captured diff when resetting the receipt state The Aone receipt suite's beforeEach wiped the whole .qwen tree to start from no receipt — deleting the captured diff the anchor gate needs along with it. Every post then died at the gate's missing-capture refusal and no receipt was ever written (ENOENT in the four receipt tests on CI). Remove only the receipt file; the seeded diff survives. * fix(review): close the anchor-gate entrances the review rounds demonstrated Round-3 remediation of the review comments on the Aone anchor gate: - R3-2 (structural): the BUILT relocated entry is now validated against compose's own ingestion (tryIngestBodyCriticals over the single entry) before the relocate is disclosed, and any refusal degrades the entry to the inert constant `finding — (no path):<line>` — the entrance space is unbounded model text and compose's acceptance is the authority, so a shape the enumerated guards never anticipated degrades the entry instead of refusing the whole post mid-degrade. The demonstrated entrance (a lone CR inside the claim: it passes the leading-fence guard, compose's CR normalisation then splits the entry and the second line leads with a fence delimiter) is covered by a witness. - Ledger collision: the relocated entry flips to `<claim> — <path>:<line>` — the claim leads, so a carried id keeps position 0 and the ^-anchored ledger readback matches instead of silently renumbering a carried finding as new. Witness asserts the id survives the readback regex. - R7-1: an explicit JSON null side/startSide reads as ABSENT (defaults to RIGHT), the model's idiom for an omitted optional field — never a declared old side. Unit and gate-level witnesses. - R3-3: witness for the non-identity authoredIndices branch — the gate renumbers the array, floor enforcement keys on the post-gate array, and the remap drops the comment floor enforcement names. - R4-2: the hostile-paths test gains the \r-bearing path (compose's ingestion normalises a bare CR to a line break — the same hostile shape as \n; the guard's \r half was unwitnessed). - R3-5: the design doc states the carve-out — the non-RIGHT degrade runs for single-line comments only; a multi-line non-RIGHT comment keeps the consistency gate's whole-post refusal; null side is absent, not a declaration. The failure-shapes table splits the row accordingly. Issue #9615 --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
2172721405
|
feat(cli): restore each daemon session onto its last selected model (#9687)
* feat(cli): restore each daemon session onto its last selected model Idle detach currently rebuilds Config from settings.model.name, so session A picks up whatever model session B last switched to. Fixes #9686 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): address session-model persistence review findings - reader: always select the last assistant record into the restore read set so the legacy lastAssistantModel fallback still fires when a trailing chat_compression candidate excludes it from the resume read - recorder: assign currentSessionModel before the awaited write so a rewind landing in the pending-write window re-anchors the new binding instead of the stale one - reader/recorder: reject non-string session_model payload fields instead of crashing the restore path on malformed transcripts - protocol doc: describe the session_model append as best-effort, not an unconditional consequence of a successful switch - cli: import RUNTIME_SNAPSHOT_PREFIX/stripRuntimeSnapshotPrefix from core instead of duplicating the prefix algorithm locally - tests: pin the isRuntime/baseUrl payload dimension, the prefix and route-mismatch false arms, the neither-field fallback, and regression coverage for the two fixes above * fix(cli): keep daemon session-model restore from failing load Pre-auth restore skipped the last-assistant fallback, and a recorded qwen-oauth binding could hard-fail load when cached credentials were gone. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): roll session-model auth retry back onto the settings route Same-id baseUrl restores and runtime-only settings models were skipping or breaking the fallback, which made load fail on the recorded credential set. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep empty daemon sessions from creating a transcript Recording the session model on newSession wrote a jsonl file before any user content, so close/delete/child-death left the id occupied and listing still showed the empty session. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): allowlist restored session-model routes against the registry JSONL baseUrl is only a registry selector, so unknown hosts are dropped before switchModel. Restore also keeps the last valid session_model payload instead of falling through a torn trailing record. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): retry session-model auth after same-id snapshot restore The retry gate ignored runtime-snapshot identity, so restoring an implicit registry route off a same-id snapshot looked unchanged and skipped rollback. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6c742ec792
|
feat(review): ask each fix for its test, and rule on non-convergence (#9596)
* feat(review): ask each fix for its test, and rule on non-convergence The review-fix-re-review loop is its own largest customer. Provenance analysis of six multi-round takeover pull requests attributed each post-first-round finding to the commit that introduced the line it anchors on: roughly a third were introduced by the fix round immediately before the review that found them, overwhelmingly as a guard or branch with no test of its own. That measurement produced a fix on the loop side, where it reaches exactly one fixer. Most pull requests are not fixed by a bot the review can configure, and whoever does fix a finding reads only the comment. So the acceptance criterion moves into the finding and into the posted comment. A finding whose suggested fix adds a guard, a branch, or a behavior now names the test that must go red if the fix is removed, and the comment asks for the mutation that proves it. The criterion never gates reporting: a finding whose fix cannot be pinned is filed anyway, because a bar on reporting would trade rounds for missed defects, and the evidence rule that governs what confirms a finding is a separate one. Second, a round stops renumbering its own churn. A new defect the reviewer can trace to the change that answered a previous entry is re-reported under that entry's id rather than taking a fresh one, so the author reads one thread per site instead of a new one every round, and the cross-round work list stops spending an id per round on a site the loop is circling. Attribution is bookkeeping and never a posting decision; it applies only when the new defect is at least as severe and as confident as the entry it carries; and anything it cannot trace takes a fresh id, which is what every round did before. Third, that attribution produces a count, and the count is what ends a loop the review cannot close by filing more findings. Each round hands over how many findings first appeared and how many of those it attributed to the previous round's fixes. The command owns the threshold, carries the streak across rounds, and files its own blocking finding on the second consecutive round in which most of the round's new work was work the previous round created. It reads the attributed count deliberately, not the count of findings on newly pushed lines: a pull request whose author pushed a feature between rounds created none of them out of the review, and a bar built on the looser number would block a pull request for growing. * fix(review): cross-check the churn census and harden the streak's edges Round-1 review findings on the non-convergence mechanism: - Refuse a census whose fresh count exceeds everything the round reports (drafted comments, body Criticals, deferrals). The census is the model-written half of the trigger; this one-sided bound is the cross-check it gets before it can arm the streak, so a round that reported nothing can no longer file the blocker on the model's say-so alone. - persistRecoveredLedger's anonymous-advance branch now drops churnRounds/fresh/induced with the other round-specific facts: a streak re-dated across a round this account never ran would arm the blocker one round early and discard the foreign winner's own streak state. The plain recovery path round-trips them, and both seams are now pinned. - SKILL Step 6's fix-induced rule caps re-reports at one per original id per round — two same-id entries are a duplicate id, and the artifact validator refuses the round's findings whole. - Reword the posted blocker and its docblocks to what the arithmetic actually does: the bar is half-or-more (not "most"), it keys on the attributed count (not findings on new lines), and the streak counts rounds against the bar — rounds that could not measure carry the count — rather than calendar-consecutive rounds. - parseLedger clamps a recovered streak to the marker's own round: the streak counts rounds inside the round it rides, and an unclamped forged streak inflates the posted ordinal past everything the pull request ever ran. - Witness pins for the gaps the reviewers probed: the >= filing condition at streak 3, ordinalSuffix past 2 (rd/teen-th/st), CHURN_MIN_FRESH from both sides, and the full corrected blocker text. * fix(review): strip foreign churn state at the recovery seam Round-2 review findings on the non-convergence mechanism: - recoverLedger now strips churnRounds/fresh/induced from a foreign winner beside the anchor. Left riding, any account that can submit a review could plant a streak through the identity-known write path and trip the blocker one honest census later on a pull request that never churned; the anonymous-advance drop stays as defence in depth. - A below-minimum census carries the streak like an absent one — three findings are rounding, not a trend — so a pull request alternating above-bar rounds with small ones still reaches the filing bar; the filing condition takes back its explicit above-bar guard, which the carry makes reachable again. - Round 1 refuses a census outright: with no predecessor nothing can be fix-induced, symmetric with the round-0 streak guard. - SKILL Step 6 counts fresh over the three reporting channels the module cross-checks — deferrals in, terminal-only and unanchorable drafts out — and Step 4's aggregate template gains the Fix witness slot Step 6 already names. - Witness pins for what the reviewers mutated: the seam strip and its round trip, the three-channel sum on both non-drafted channels, the carry and its guard, the round-1 refusal, the finder brief's fix-witness format, and the aggregate slot. * fix(review): restore own churn state at the union, clamp the side-file streak * fix(review): name the churn group once, and part the two fresh counts The convergence state was hand-enumerated at three production seams while the volume group beside it documents a single shared list as the remedy for a bug that already shipped once there — a field kept at one seam and shed at the other. Nothing reds on a missed enumeration until a fourth field is added, and the two ways to miss it are both silent: left in the side file after the counter advances past the round it describes, or dropped from the restore that protects this account's own data on a merged round. The group now has one name, and the strip, the restore and the counter-advance branch all read it. The other half is a collision of words, not of arithmetic. One posted body carried two counts of what a round did for the first time: this mechanism counts DEFECTS newly identified, and the volume trend counts inline comments POSTED for the first time. They legitimately differ — a fix-induced defect re-reported under the id it came from is new work here and a re-post there — so a round can newly identify six defects while posting two first-time comments, and both numbers are right. Written as "findings first filed" beside "reported for the first time", neither could be trusted. The blocker now says "defects newly identified", and both the module and the skill record why the two must not be reconciled by changing either one: excluding carried-id re-reports from the census would put the attributed count outside it and every such census would be refused as impossible, while counting them as first-time posts would tell the trend that a re-post is new work. Four witness pins close the gaps the last rounds left. The census cross-check summed three reporting channels but every arm populated one at a time, so a non-additive reduction shipped green and would refuse a census on the ordinary shape of a round with body blockers beside inline findings. The same-round union's churn restore had no pin through the persist seam — the existing foreign-winner arm is cross-round, where the state is already gone and a second drop is a no-op. The anonymous recovery walk's churn strip was unpinned, so a refactor gating it on a known identity would let a foreign streak ride an identity blip into the side file. And the finding format's N/A exemption was pinned only up to its prefix, so deleting the clause that keeps the criterion from becoming a bar shipped green in the copy that actually reaches the agents. * fix(review): name fix-induced in the ruling, pin the census clauses, drop the unread marker pair * fix(review): carry the churn streak through unmeasured rounds The carry contract says unmeasured rounds carry the count, and two seams broke it for the cumulative streak while handling it correctly for the per-round volume: - The union restored own churn state only when the own marker described the SAME round as the winner, so a strictly NEWER foreign winner silently zeroed this account's standing streak — on a PR two accounts alternate on, neither ever reached the filing bar. The streak now restores across the round gap; only pickVolume stays same-round gated. No foreign state enters: the winner's streak is stripped at the recovery seam, and the restore spreads only the own marker's state. - The anonymous-advance branch dropped the streak from this machine's own side file, so an identity blip (the gh api user failure the branch already anticipates for the volume) reset a standing claim; repeated blips kept the blocker unreachable on exactly the churning PRs. The drop rationale clauses do not apply — the winner's streak cannot reach this seam, and carrying arms nothing early because filing still needs THIS round's own above-bar census. The streak now carries, matching the sibling recovery-threw state and the filed blocker's own body. Also part duplicate-dropped findings from the census `fresh`: they restate defects an earlier round identified (the duplicates paragraph discloses the confirmation; it is not a fourth reporting channel), and counting them let the module refuse a census the rule as written licensed. The exclusion is now explicit in SKILL's NOT-counted list and pinned by the census contract test. Witnesses: the cross-round persist test now asserts own streak restored (1) and planted streak gone (never 4); the anonymous-advance test asserts the streak survives the counter advance. Mutation probes on each guard red when removed, green when restored. * fix(review): part "reset" from "not recorded" at the churn seams Two blockers landed on the same seam pointing opposite ways: one that the identity-known write drops a standing streak, one that the anonymous-advance branch keeps a stale one. Applying both suggestions as written would have reverted a seam that had already been reversed once, so neither is applied directly; the shared cause is fixed instead. That cause is an ambiguity. A round measuring below the bar resets by stamping no streak at all, so "no churn state" is written by a reset and by a marker that was never read, and the two paths resolved it in opposite directions. Recovery now reports whether an own marker was actually READ — distinct from whether an own review exists, which is the case the corrupted marker falls into — and the seams read that instead of guessing from absence. The identity-known write carries the file's streak only when no own marker was read: nothing authoritative said reset, so the file still holds the last state this account certified. When one was read, it has spoken in whichever direction and the write leaves it alone, so a real reset still lands. The anonymous-advance branch sheds the streak with the volume. The argument for keeping it was that a carried streak arms nothing early because filing still needs the round's own above-bar census; that shows it is only USED where a measured round finds it, not that it is still true there. With no identity this branch cannot tell this account's own reset marker from a stranger's, and carrying one lets a later census reach the bar a round early with the blocker's own body claiming rounds that did not pass. Dropping costs only the outage: the own marker stays on the pull request, so the next identity-known recovery re-establishes the true streak. The rule the two now share is one sentence. Carry while the state is known to be ours and current; drop where it can be neither attributed nor dated. Three smaller things fell out of checking the fix rather than the findings. A carried streak is read through the ledger's own reader and clamped to the round it is written beside, because this is the first path where bytes from the file survive a write instead of being replaced by it. The anonymous whole write now sheds the churn group as it already shed the volume, so that seam defends itself instead of resting on an upstream strip, and the assertion covering it was rewritten over a fixture that actually carries a streak — it had been holding vacuously. And two guards that no mutation could redden were resolved explicitly: one removed as an invariant of the strip above it, one kept as defence in depth with its unreachability and its behavioural pins named, because it sits on the exact axis the second blocker was about. |
||
|
|
079e22a914
|
feat(review): add temporal-reachability and incident-replay lenses (#9708)
* feat(review): add temporal-reachability and incident-replay lenses Two blind-spot fixes measured on PR #9655's escaped P1 (a post-run --capture that could not steer the run it documents, plus brief text telling the witness to quote it as though it had): - Agent 1c: reachability gains a TIME axis. A value produced after every decision it should influence is a record, not a mechanism; when documentation or workflow guidance treats the record as a mechanism, that is the Critical, with 'produced at X, needed at Y, Y precedes X' as the whole trace. - Agent 0: a motivating incident narrated in the PR context is replayed step by step against the post-change workflow, regardless of closing-keyword formality. An unchanged outcome is a Critical even when the diff faithfully implements what its issue prescribed — an issue can prescribe a remedy that never reaches its own observed failure. An empty closing set no longer empties the replay duty. * fix(review): pin the new lenses and give the replay an enforceable contract Round-1 review feedback on this PR, all five findings addressed: - R1-1/2/3: the three added passages were unpinned — a future deletion shipped green. Weld-style pins added in agent-prompt.test.ts (the enumeration-trap precedent), covering the replay duty, its un-gating, the TIME-axis paragraph, the trace format, and the verifier clause. - R1-4: the empty-scope return now carries a fourth evidence item — the replay's outcome (the step that changes, or the reason none does), or an explicit statement that the description narrates no incident — so a skipped replay never reads identically to a performed one. - R1-5: the orchestrator contract buried the lens's product in the exact case it was written for — SKILL.md forbade falling back to the PR description and the verify brief downgraded fidelity findings lacking issue evidence to low confidence (terminal-only). Carve-outs added in critical rule 4, the Step 2 context paragraph, and the verify brief: a replay finding quotes the PR's own narrative as its evidence, judged as the PR's claim about what the change prevents, not as ground truth. * fix(review): route the no-step-changed replay outcome to a finding, never the receipt Round-2 review feedback, all four findings addressed: - R2-1 (Critical): round 1's fourth evidence item routed the replay's no-step-changed outcome INTO the scope-empty receipt while the bullet above mandates it as a Critical — two mutually exclusive return shapes, and a receipt contributes nothing to the verdict, so the mandated Critical could dissolve. The contract now routes explicitly: no step changed = a findings return; the receipt carries only the benign outcomes (the step the replay saw change, or an explicit statement that the description narrates no incident). - R2-2: four load-bearing clauses pinned — the replay's Critical severity, 1c's record-as-mechanism severity condition, the distinguishability sentence, and the verifier's no-downgrade clause. - R2-3: the orchestrator-facing copies of Agent 0's return contract (the whiff-check parenthetical and the roll-call example) updated to the new shape, so a skipped replay cannot pass as the old three-item receipt the prose told the orchestrator to accept without relaunch. - R2-4: SKILL.test.ts revert guards for both SKILL.md copies of the incident-replay carve-out, following the rule-4 guard's pattern. * fix(review): complete the receipt example and pin the last unpinned clauses Round-3 review feedback, all three findings addressed: - The roll-call example restores the 'not a bugfix' evidence item the round-2 rewrite dropped — it now models all four receipt items, so an orchestrator shaping its Step 6 line on it cannot certify scope-empty for a bugfix PR without that determination asserted. - The orchestrator-side copy of the R2-1 routing rule and the roll-call line are pinned in SKILL.test.ts's carve-out guard: reverting either restored the pre-R2-1 receipt standard while every brief-side pin stayed green. - The TIME-axis pins gain the definition clause ('a record, not a mechanism') and the two-moments method — without them the severity rule names a split nothing defines. |
||
|
|
c10143a9c1
|
chore(release): v0.22.0 (#9736)
* chore(release): v0.22.0 * docs(changelog): sync for v0.22.0 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
39378ac0a4
|
feat(serve): restore ask_user_question HITL on session load/resume (#9665)
* feat(serve): restore ask_user_question HITL on session load/resume Keep a trailing unanswered question votable after daemon load/resume when --restore-ask-user-question is on, instead of closing it as a failed tool result. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(daemon): harden ask_user_question restore per review feedback - acpAgent: defensive restore hint (no `!` lookup; accepts undefined session) + normalized id lookup on both cold return paths; session test doubles carry shouldHintAskUserQuestionRestore - bridge: single maybeFireRestoreAskUserQuestionPrompt helper with the full admission-time busy predicate (pendingPromptCount + goalTurnActive), sync-throw try/catch, no-attached-client gate, fork suppression, and hasActivePrompt reflecting an admitted restore prompt; child-bound requests carry a suppress meta when the daemon already knows it will decline, keeping replay skip and re-hang aligned - Session: restore prompt gated on the config flag; early bail before per-turn bookkeeping when history is not restorable; system reminders ride the post-answer message; restore turns no longer burn the active-todo reminder; a permission timeout on a restored question no longer persists the fabricated decline (transcript stays dangling for a later re-hang); continueLastTurn declines a restorable question; restorable detection reads peekLastHistoryEntry instead of cloning the full history - history-replay-page: isInitialized() guard on the skip probe; dead paged-path skip wiring removed - transcript-replay: skip set matches raw ids after dedup renames - core: inline orphan-repair preserves the restored AUQ ids; the compression side query strips a trailing dangling functionCall; the CLI flag is honored only in ACP mode * fix(cli): skip restore hint helper when the switch is off Load/resume used to call shouldHintAskUserQuestionRestore on every Session, including test doubles that do not implement it. Short-circuit on argv first so the default-off path stays independent of the restore-only API. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
af25c45e80
|
fix(cli): Recover sessions across archive races (#9513)
* fix(cli): report a conversation directory deleted mid-inspection as already gone A child deleted between the lstat and the realpath, or a root that vanished mid-inspection, was rewritten as 'identity_changed' and then surfaced as 'Live conversation directory must be an owned direct child' — a plain Error with no .code pointing at permissions and symlinks when the directory was simply deleted. Restore the ENOENT-race -> false contract of discardEmptyConversationDirectory (QwenLM/qwen-code#9489, item 4). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): keep conversation metadata reads race-free and parent ids storage-aligned Item 2 of QwenLM/qwen-code#9489: readExistingMetadata read the location, read the metadata, then re-read the location and returned undefined on mismatch, so an archive landing between the probes made lock-free resolvers report a healthy session as session_not_found. Creation metadata is immutable, so one tolerant read per state (active first, then archived) decides deterministically; the location probes are gone and a path-safety charset gate keeps the joined transcript path a single segment. Item 3: the parent-lineage gate required strict RFC-4122 v1-v5 ids while the store resolves far looser names, so persisted parents written by older builds (nil, v6/v7, agent-suffixed ids) turned loadable children into SessionNotFoundError, and the -agent- allowance could never resolve. Drop the shape gate and let storage resolution decide, keeping only the self-reference rejection. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): let loads resolve both-states sessions and drop the pre-lock restore scans Items 1 and 5 of QwenLM/qwen-code#9489. Item 1: a session persisted in both active and archived states — left behind by a crash inside archiveSessions — hard-failed ACP session/load and session/resume with session_conflict while plain CLI --resume kept loading the active copy. findSessionIdIgnoringCase now resolves the requested spelling first (and a single both-states candidate) instead of throwing, and assertSessionLoadable treats 'conflict' as loadable from the active copy. Mutating surfaces keep refusing: unarchive still conflicts via assertSessionArchived, and multi-runtime ownership arbitration stays strict so a conflicted internal copy cannot claim a session an ordinary workspace serves. Item 5: both restore handlers ran findSessionIdIgnoringCase twice per request — once as a pre-lock guard whose result REST discarded and the ACP twin kept as a stale storageSessionId fallback consumed exactly in the TOCTOU where the in-lock resolve returned undefined. The pre-lock guards are gone (the in-lock resolve is authoritative and both handlers now agree), the exact-spelling fast path removes directory scans from the common case entirely, and the remaining scan uses async readdir so a large chats tree no longer blocks the daemon event loop. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): preserve canonical restore conflicts Canonicalize live task keys before resident bridge operations, and keep known case-conflict responses when the optional storage recheck fails. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): handle case-variant session follow-ups Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): preserve mixed-case live task identity Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): resolve canonical persisted session ids Batch case-insensitive transcript lookups for multi-thread waits and preserve organization metadata when live and persisted session IDs differ only by case. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): preserve canonical session restore state Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): complete canonical session reads Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): address canonical review findings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): preserve aliased session organization Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): close canonical session review gaps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): close canonical task ownership gaps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): keep case twins distinct across session pages Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): isolate alias verification failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(integration): align both-states transcript expectation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): preserve mixed-case session ownership Arbitrate noncanonical live task IDs across workspace runtimes and retain the newest legacy organization alias only for uniquely persisted sessions. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): preserve live task ownership during refresh Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): handle session alias races Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#9513) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(e2e): normalize generated session ids Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): narrow PR 9513 to restore regressions Drop the review-driven mixed-case expansion and retain only the five regressions tracked by #9489. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): complete active transcript conflict recovery Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): align session conflict assertions Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): align transcript conflict e2e Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#9513) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
2a99e84169
|
fix(review): clear the deferred Round-5 findings from the Aone write path (#9604)
* fix(review): clear the deferred Round-5 findings from the Aone write path The full cleanup of #9579 — the 29 Suggestions deferred from round 5 of the /review bot on #9491 under the ~5-round rule (Criticals-only from that round on). One item (the GH_HOST setGhHost assertions) was already landed with the round-5 Critical fixes; the rest are implemented here. Write-path fixes: - A shaped-but-empty --host refuses with its own shape (host-flag-empty) instead of collapsing to the unbound refusal the flag was the remedy for — the agent re-run loop the refusal wording exists to break. - An invalid host (recorded verbatim or flag-typed) refuses in the exit-3 shape naming the offender and its origin, instead of setGhHost's TypeError escaping runSubmit as a failed command. - A flagless gh post whose nothing-bound routing would inherit an ambient GH_HOST pointing at canonical Aone refuses actionably (ambient-gh-host-aone) instead of failing opaquely after compose ran. - The shared authorisation gate no longer reads an absent host as a github.com claim for callers whose routing follows the recorded binding (submit): the ordinary flagless publish of a GHE-recorded review passes, while publish-assets keeps the strict comparison. - Mid-batch drift disclosure rides the partial-post shape too (headMovedDuringPost on AonePartialPostError, warned from submit's partial branch), and the post-batch re-read is tri-state: a failed re-read leaves headMovedDuringPost undefined and submit discloses "could not re-verify" instead of a false all-clear. - The Aone success JSON surfaces postedCommentIds/summaryCommentId — the audit the partial shape carries and the gh receipt records. Docs and contract fixes: - The context-unavailable cap wording now says what it does (keeps an Approve verdict at Comment; a Request-changes verdict still posts) in the user docs and both SKILL.md sites. - The head-drift bullet is qualified by the per-review restart bound — spent on Aone there is no submit-at-reviewed-SHA fallback; report and leave the rest to the user. - Step 9's Posted: contract admits the no-link note the Aone fallback prescribes. - The --host help text spells both canonical Aone hosts out. - The provider design doc's Phase-3 "refuses" sentence is marked superseded. Test hardening (unfalsifiable pins made falsifiable): - ensureAoneAuthenticated ordered before the writes; setGhHost ordered before the gh write; the a1 path never touches the gh host state. - Live-probe cells for the explicit-flag precedence, the unbound refusal, and the fast-path hostless refusal; the recorded-binding- outranks-probe fixture driven through submit's real gitOpt seam. - submit.test.ts mocks ./lib/git.js (no real git spawned in the vitest cwd), isolates the cross-session suite's recording store via chdir, and pins the newest-wins ordering when two recordings of one PR carry different hosts. - Producer-side 'refusing to post:' prefix pins, the RC-Note count source pin, the contextUnavailable:true gh-path pin, and the floor recovery's callerHost pin. * fix(review): address round-1 findings on the Aone write path (#9604) * fix(review): address round-2 findings on the Aone write path (#9604) Extract one refuse helper for submit's seven exit-3 refusal shapes (sibling publish-assets precedent), align the Aone pre-write refusal prefix with the other refusal paths, and pin the invalid-host remedy of the flag/origin arms positively — the recorded arm's absence pin alone let a ternary-collapse mutant ship green. * fix(review): address round-3 findings on the Aone write path (#9604) Make submit's exit-3 refusal terminal: refuse now throws a SubmitRefusal that runSubmit's single catch renders into the refusal shape (stderr line, posted:false JSON, exit 3), so a gate that says no cannot fall through toward the write — the helper previously returned and relied on every call site adding its own `return;`. Also extract the post-batch MR-head re-read, duplicated between submitAoneReview's partial-post and success paths, into one helper. * fix(review): address round-4 findings on the Aone write path (#9604) * fix(review): address round-5 findings on the Aone write path (#9604) * fix(review): address round-6 findings on the Aone write path (#9604) * fix(review): resolve merge-conflict residue in the review skill (#9604) --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
4b014c96e5
|
fix(review): stop shedding the convergence observation first (#9715)
* fix(review): stop shedding the convergence observation first The convergence paragraph is the only thing a posted review says about the SHAPE of the loop rather than about the diff, and it was rank 0 — the second block the body-budget ladder sheds, right after the mechanism-health note. The reasoning was that an advisory paragraph decides nothing, so it should yield before the deferral list and the not-reviewed disclosures. The arithmetic does not support that ordering. Rendered bilingually the paragraph is 603 characters when only the volume signal fired, 1,510 with three recurrence clusters, and 2,372 with the clusters, the evidence caveats and the land reading together — against a body budget of 56,830. Shed second it can pay for at most 4% of an overflow, so any overflow larger than the paragraph itself spent it and then went on to spend the deferral list and the disclosures anyway. On the rounds this fires on — the high-volume ones, which is the whole point — that is the normal case: the author lost the reminder AND the disclosures. It is rank 3 now, the last rank the ladder sheds. Still ranked, not untagged: a body that genuinely cannot hold its blockers must drop an advisory, and being ranked is what makes the trim notice name it when that happens. Ranked last because it is the cheapest block to keep and the only one whose reader is the pull request's author alone — the deferral list has a second durable copy in the findings artifact, the disclosures are restated in the terminal report, and the mechanism-health note above it is written for the operator, who has the `HEALTH:` line. The test that pinned the old order is replaced by two that pin the new one: one sized to the window where the ladder sheds the disclosures and stops, asserting the paragraph survives and the notice names what actually went; one sized a rung further, asserting the paragraph goes last and is named when it does, before the hard cut. Mutation-verified: restoring `trim: 0` reddens both; making the block untagged reddens the last-and-named test (and the existing terminal-copy test); removing the rank's name from RANK_NAMES reddens only the naming assertion. * test(review): finish the rank move in the comments, and re-centre a constant Two hygiene items the review caught on the trim-rank change. A test title and its comment still said the convergence paragraph is the first rank the ladder sheds — the sentence the change makes false. Three other copies of that wording were updated with the move; this one was missed. And `keeps the terminal copy on the round that actually sheds the paragraph` was sized at 55,850. Reaching a body that dropped the paragraph now means sizing past every other rank, and the window that does so without also truncating runs 55,825–56,350 — so the constant sat twenty-five characters above its own floor, with no note saying it was tuned at all. It is 56,100 now, near the middle, and carries the same retuning instruction as the two order tests. It also asserts the body was not truncated, so a future retune that overshoots reads as a failure rather than as a pass for the wrong reason. Measured, not guessed: sweeping the blocker size in 25-character steps puts the shed boundary between 55,800 and 55,825 and the truncation boundary between 56,350 and 56,375. Dropping the constant back below the window reddens the test, so it still reaches the case the terminal copy exists for. * docs(review): finish the rank move where the rationale actually lives Six comments still asserted that the convergence paragraph is the first thing the overflow ladder sheds. The reviewer named five; a sweep found a sixth, and two of the six were already inaccurate before this branch touched anything. Corrected: - the `convergence` result-field doc — "sheds this paragraph first" - the deferral block's own rank comment — "the first thing to yield", which ranks -1 and 0 had both preceded since before this branch - the not-reviewed disclosures — "and before nothing else" - the `CONVERGENCE:` stderr line's rationale — "the first thing the overflow ladder sheds" - `save-artifact.ts`'s allow-list — "the ONE clause the overflow ladder sheds first" - the body-budget suite's own statement of the policy under test, which listed two of the four ranks Left alone, and verified correct: every "sheds first" attached to the mechanism-health note, which is rank -1 and genuinely first (`save-artifact.ts`, `save-artifact.test.ts`, the health-note terminal-copy test), and the `keep`-ordering comment about the tail cut, which is not about `trim` at all. Each corrected site keeps its own reasoning — durability, the artifact, the terminal copy — and states the ordering only as far as that reasoning needs, rather than restating the whole argument a sixth time. Six copies of one fact is what let it drift; the argument for the ordering lives at the convergence block and the others point at it. The durability rationale gets stronger, not weaker, and the comments now say so: a body that sheds rank 3 has already shed every other rank, so the terminal and artifact copies are the only ones left exactly when they fire. |
||
|
|
0c36e5093a
|
feat(review): close Aone residual gaps — composeUrl, test-plan routing, a1 version floor (#9624)
* feat(review): close Aone residual gaps — composeUrl, test-plan routing, a1 version floor The three residuals #9619 tracks together, one pass: - composeUrl joins the platform reader: GitHub composes the PR-page URL from the routed host (deterministic grammar, no API call); Aone is reader-backed — the platform's own detailUrl, never assembled, since the nested-group owner/repo collapse can name a different repo. submit fills a receipt that carries no url through it on both platforms, so the skill's prose fallback shrinks to the coordinates relay for the one case the reader cannot serve. - test-plan's body fetch routes through the platform reader: the MR description on Aone (already in the reader's fetch metadata — no new API surface), so the Test Plan check runs on Aone targets instead of going unchecked on every run. - ensureAoneAuthenticated enforces the a1 version floor design-doc Q1 asked about — 0.1.90, the version the platform facts were probed against — in presence → floor → auth order, each with its own remedy message; unreadable versions are disclosed on stderr and fail open. Verified: ~590 targeted unit tests, tsc/eslint/prettier clean, build + bundle green, and a CLI smoke that refuses a fake stale a1 at the floor while a fake fresh one passes the gate. * fix(review): apply round-2 review on the Aone residuals All six round-2 suggestions on #9624, probe-verified and pinned: - R1-1: the version-probe fail-open now discloses the CAUSE — the extraction mirrors the whoami catch (first non-empty line past the execFileSync preamble), so segfault / unsupported flag / permission failures stay distinguishable instead of one constant preamble line. - R1-2: aoneReader.composeUrl discloses a failed lookup on stderr before degrading to '' — every other fail-open in the provider discloses, and the coordinates-relay case must stay distinguishable from an environment fault. - R1-3: one home for the PR-page host spelling — normalizeGhHostForUrl in lib/gh.ts, shared by compose-review's comment anchors and the reader's composeUrl, so a `--host GHE.Corp:443` run can no longer print two textual spellings of the same PR page; non-default ports survive. - R1-4: submit no longer re-queries the reader when the Aone receipt carries no webUrl — detailUrl is a stable MR attribute and the pre-write drift-gate read already carried it, so the second fetch could only block on the flaky state that lost the field. The empty receipt rides the coordinates relay; the reader keeps composeUrl as the canonical seam. - R1-5: the Aone body-fetch route runs the same ensureAuthenticated gate every other a1-backed flow runs first — a standalone test-plan on a missing/stale/logged-out a1 now fails exit 1 with the install/upgrade/login message instead of exit 0 with the generic note. The GitHub arm keeps its historical degrade. - R1-6: the handler wiring (the Aone fix's integration point) is pinned by handler-level tests — an Aone --host must route the body through the reader with the gate first, and a refused gate fails the command before any fetch. SKILL.md's Posted paragraph, its revert-guard pins, and the design-doc bullet follow the R1-4 semantics. Verified: tsc/eslint/prettier clean, 759 targeted cli tests + 23 SKILL guards green. * fix(review): apply round-3 review on the Aone residuals * fix(review): apply round-4 review on the Aone residuals * fix(review): fail closed on unknowable host in composed receipt link (#9624) * fix(review): route explicit GHE-family hosts to the GitHub reader Platform detection selected Aone on ANY *.alibaba-inc.com host for explicit --host/--remote signals, but a family host that is not the canonical pair (ghe.alibaba-inc.com is the live example) is a GitHub Enterprise instance — such a review authenticated against a1 and read an unrelated same-numbered Aone MR instead of the GitHub PR body (and test-plan additionally gated on a1 auth first). Explicit signals now select Aone only via the canonical pair (code./gitlab. alibaba-inc.com) — the same canonical-only rule the write gate has always applied — while the family predicate survives on the no-explicit-signal cwd-origin fallback. parse-args stops refusing /pull/ URLs on GHE-family hosts (the same predicate misapplied: those are real GHE PR URLs), and the five --host describe texts now name the canonical pair. Pins: registry.test.ts flips the GHE explicit-host expectation to github, adds the explicit-remote and canonical-port arms, and keeps the family cwd fallback; detection-side 592 + write-side 322 tests green. * fix(review): fail closed on family-only /codereview/ URLs AONE_CR_URL_RE captures the whole *.alibaba-inc.com family (shape-first grammar), but a family-only host is a GHE instance that serves no /codereview/ page: accepting its URL as a live target would let detection route the explicit GHE host to GitHub and aim fetch/submit at GHE PR #<id> — a target the supplied URL never named as a valid GHE resource. The classifier now gates the aoneMatch branch on isAoneCanonicalHost, so non-canonical /codereview/ inputs stay invalid-url, mirroring the /pull/-on-canonical-Aone refusal. The mirror arm is pinned too: a /pull/ URL on a family-only host parses as the real GHE PR target it is. --------- Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
b455bad5e5
|
refactor(cli): keep acp-integration off serve internals (#8084) (#9144)
* refactor(cli): keep acp-integration off serve internals (#8084) The dependency direction set in #8084 regressed: native Live Voice ( |
||
|
|
fcc1dfb123
|
feat(review): report the address a drive's service actually bound (#9655)
* feat(review): report the address a drive's service actually bound A port is a request, not a fact. Handed one that is taken, `qwen serve` prints `port 8931 is in use, trying 8932...` and listens on the next. A verifier that goes on addressing the port it asked for then reads a different, stale process for the rest of the run — its readiness probe passes against whatever is squatting there, the drive completes, and every number in the witness is about the wrong daemon. Nothing in the report says so, because nothing in the report knew: `drive` had no port handling at all. Measured during a daemon verification, it cost a full cycle before the readings stopped making sense. `--capture name=<regex>` reads named facts back out of the run's own output into `captured`, the bound address first among them. Four choices in it are the ones that matter: - It reads the UNTRIMMED log. `trimCapture` keeps the tail and a service prints its address at the head, so capturing from the report's `output` would lose exactly the value this exists for, on the loudest runs — the ones most likely to need it. - A pattern that never matched is `null`, never `''`, and the note NAMES it. That is the moment a witness is about to quote a value the run never produced, and the reader has to know which one. - A malformed pattern rejects the whole set before anything starts. Silently dropping the bad entry would leave a missing key beside the good ones, which reads as "the service never printed it" — the one meaning `null` is reserved for. Finding out after a 300-second drive costs the drive. - Captures are taken on every outcome, not only `completed`: a drive that timed out still bound its port, and that address is often what explains where the rest of it went. The verify brief carries it too. A capability taught only where the verifier does not read is inert — the lesson from #9445's first review round — so the brief now says to bind ephemeral where the service allows it and quote the captured address rather than the one on the command line. Refs #9446. * fix(review): correct drive capture examples and bound its outputs (#9655) * fix(review): a declared capture group the match left unfilled is null Round 1/2 review. `m[1] ?? m[0]` reads as "group 1, or else the whole match", and for an OPTIONAL group that is a silent substitution rather than a fallback: `(?:a(x))?b` against `b` matches with group 1 absent, so the caller that asked what `x` matched received the whole match `b` under the same name, with nothing in the report saying a different question had been answered. Whether the pattern DECLARES a group and whether group 1 happened to participate are different questions; conflating them loses a value quietly, which is the one failure mode this command exists to remove. The declaration is now settled when the pattern is parsed — by matching `<source>|` against the empty string, guarded, since it builds a second pattern from the first — and a declared-but-unfilled group is `null`. A pattern with no group still yields the whole match. The docblock's `null`-versus-`''` claim was too strong beside that, and the narrowing belongs to the pattern rather than the service: `''` means the group captured zero characters, which a `*`-quantified group does wherever it is anchored (`pid=(\d*)` returns `''` against `pid=abc`). Said so, and named `+` as what a "printed nothing" test needs. Also from the two rounds: - The head-trim clause and the capture block contradicted each other in the note. `output` is trimmed at the head, `captured` is read before that trim, so a completed noisy drive said "early output is missing" directly beside a value that came out of the missing head — and beside a null the reader would reasonably blame the trim for. A scoping clause now says captures read the untrimmed log, covering the matched case (R1-8) and the missed one (D2-2) together. - Four invariants the docblocks state were pinned by nothing: the unfilled-group rule, the empty-capture-is-a-measurement rule, the first-match rule against a log holding two, and the pattern-length cap's upper bound (only its lower bound was tested). - The CLI seam has a test (D2-1). The handler casts `argv as unknown as DriveArgs`, which type-checks whatever the option is called, so a rename of either side silently disabled `--capture` end to end while every runDrive test stayed green and tsc exited 0. It now drives the real builder with a real flag string and asserts the value reaches the report the handler prints. Each of these was checked by mutation: reverting the group rule, the option name, the first-match rule or the scoping clause reds exactly its own test, and restoring them returns 50 passed. * fix(review): scope drive capture notes to the outcomes they describe (#9655) * fix(review): stop the capture guidance from authorising a misattribution @yiliang114's P1. Captures are extracted after the drive loop has ended, so nothing in `captured` can reach a request the script already made — and the brief told the verifier to quote the captured address in the witness anyway. That combination is worse than not capturing at all: a script that talks to 8931 while the service logs its fallback to 8932 reaches its sentinel, returns `observed: true` beside `captured.baseUrl: …8932` with nothing contradicting it, and the witness then attributes 8931's readings to the daemon on 8932. Before this change the same run would at least have quoted the address it really read. The mechanism that prevents the wrong-process read lives in the script, not in the report, and the brief now says so: derive the address from the service's own output before the first request, bind ephemeral wherever the service allows one so the fallback never fires, and let `--capture` show that the address the script used is the one the service printed. `null` is named as "never measured", explicitly not as permission to fall back to the address on the command line. The recipe is spelled out rather than described, because the failure it replaces is one of omission. The report carries the same caveat where it cannot be missed: `captured` is documented as a RECORD of the run and never an input to it. Verified rather than read: the recipe's `sed` and the `--capture` pattern were run against real `listening on` lines, http and https, and agree on the same value — which is the property that makes the capture corroborate the script instead of competing with it. The rendered brief was printed and re-read after the edit, and again after eslint caught an unnecessary `\$` escape in it, to confirm `$BASE` still survives to the agent verbatim. No behaviour change: 340 passed across drive, agent-prompt and run-skill-parity; tsc 0 errors; eslint clean. * fix(review): make the taught bound-address recipe actually capture Round 5 Critical, and it was mine: the recipe added last round sends the service to a file of its own, and `drive` runs the script as `bash <script> > <the drive log> 2>&1` while `extractCaptures` reads only that log. So the service's `listening on` line never reached the capture. Reproduced against the real `runDrive` before touching anything: a faithful run of the recipe returns `completed`, `observed: true`, and `captured.baseUrl: null`, under a note asserting the value "was never measured" — while the service had printed it all along. A recipe that cannot work is worse than no recipe. The service's output has to reach two places, and the shape that does it is a temp file the script greps plus a `cat` of that file before the first request. Three details in it are load-bearing, and each is a finding from this round: - `mktemp`, not a file beside the code. `--cwd` is the reviewed worktree, and an untracked `svc.log` left there is inlined into the next capture of that tree as though the PR added it. - `cat` BEFORE the request. Captures take the first match, so the service's own line wins over any response body containing one — a status endpoint quoting its own banner cannot forge the address. Verified with a service whose body advertises a different port. - No `trap … EXIT` of the caller's own. The wrapper writes its completion sentinel from an EXIT trap and a second one replaces it; measured, a script with its own trap comes back `timed-out` with a null exit code having run perfectly. I was about to use one for cleanup. Block-buffered stdout gets a line too — it is the one failure this shape cannot fix, and the note's advice to raise `--timeout` cannot touch it. The recipe is now pinned by a test that runs the brief's OWN text: the script body and the capture pattern are extracted from the brief rather than retyped, filled with a real service, and executed under the same redirect contract `runDrive` imposes. No tmux is involved, because what broke was the shell. Reverting the recipe to either broken form — the one shipped last round, or merely dropping the `cat` — reds it, and restoring returns 54 passed. 341 passed across drive, agent-prompt and run-skill-parity; tsc 0 errors; eslint clean, including two problems of my own the linter caught in the new test. * fix(review): re-read the drive log once its sentinel is observed Round 6 Critical. The poll loop reads the log and then the sentinel, and breaks on `completed` without reading again — while the wrapper writes the sentinel from an EXIT trap, strictly after the script's last write to the log. A final write landing between those two back-to-back reads is on disk and not in the snapshot, a window one `readFileSync` of a near-cap log wide. The ordering predates this PR, where it cost a truncated tail in `output` and read as a display artefact. Extracting `captured` from the same snapshot turned it into a machine-readable measurement with a false cause attached: `outcome: completed`, `captured.<name>: null`, and a note asserting the pattern never matched — for a value the log on disk contains. That is the shape this command exists to prevent, so the escalation is the defect even though the loop is older than the diff. Reproduced before changing anything, on a 7.9 MiB log with a real writer process swept across the read window: before trials=70 completed=70 stale-tail hits=1 (delay 248.72 ms) outcome completed, captured null, finalmetric=7 on disk after trials=70 completed=70 stale-tail hits=0 same sweep Every log write happens-before the sentinel write, so a read taken after observing it is complete. Scoped to that branch alone: the other exits stopped the run rather than watching it finish and have no such guarantee to lean on, and the existing `existsSync` guard is kept so a script that wrote nothing still reports an empty capture rather than throwing. No unit test: reproducing this deterministically needs the log mutated between two reads inside one iteration, which wants a `readFile` seam this command does not have, and drive.test.ts states that it never mocks `node:fs`. A widened-race test hits ~1 in 70 and a flaky test is its own defect. The measurement above stands as the evidence. 344 passed across drive, agent-prompt and run-skill-parity; tsc 0 errors; eslint clean. --------- Co-authored-by: wenshao <nigolaschao777@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
7330a022cd
|
perf(review): give review agents their own subagent type (#9678)
* perf(review): give review agents their own subagent type
Review dimension agents were launched as `general-purpose`, which is the
only builtin subagent that declares no `tools` list. That sends it down
`AgentCore.prepareTools`' inherit-everything branch —
`getFunctionDeclarations({ includeDeferred: true })` — so every agent was
handed all 51 tool schemas, deferred ones included, and re-declared them
on every turn.
Measured with a recording endpoint on a 6-file / 115-line diff, driving
one real dimension agent through its four turns: 21,178 prompt tokens of
tool declarations per turn, of which the 35 `computer_use__*` schemas
alone were 11,011. A review names 13-14 such agents.
`review-agent` declares the six tools a dimension actually uses, which
takes the `getFunctionDeclarationsFiltered` branch that `Explore` and
`statusline-setup` already use. Same fixture, same launch prompt,
nothing else changed: 3,447 tokens per turn, and one agent's delivered
prompt fell from 139,013 to 55,733 (-59.9%) — about 1.08M tokens across
one roster.
The alternative — applying the deferral that trims the orchestrator to
subagents too — was measured at 84,537 and rejected: deferral does not
go below the ~14 core tools, and `revealDeferredTool` writes to the
registry the parent session shares, so one subagent's discovery would
rewrite the orchestrator's declarations and void its prompt-cache prefix.
* fix(review): correct the review-agent docs and pin its system prompt
Addresses the review on #9678.
The `REVIEW_BUILTIN_SUBAGENT_TYPE` docstring described a fallback that
does not exist. `AgentTool.execute` substitutes the default only when
`subagent_type` is omitted, and `loadSubagent` ends at `getBuiltinAgent`
with no default, so an unknown non-empty type fails loudly with
`Subagent "<name>" not found` rather than silently reverting. It also
claimed the literal is the only input to the branch choice; resolution
runs session > project > user > extension > builtin and builtin names
are not reserved, so a user-authored `review-agent` shadows this entry —
deliberate, since that is how any builtin is customised, but worth
documenting where the cost is explained. The same wrong mechanism was
repeated in a SKILL.test.ts comment.
The system prompt had no content assertions: blanking it left every
test green while every dimension agent would have launched with no
instructions. Its contract lines are now pinned the way the sibling
builtins pin theirs.
The stale `coverage.ts` comment ("`agentName` is `general-purpose` for
all of them") is updated — it is now the launched type, so it is not a
value to match on.
Web Shell has per-type display labels for every other builtin; adds the
en/zh pair so review agents do not render as a raw kebab-case id.
DESIGN.md now carries the per-turn record the totals decompose from, and
states the second-order share as 12,476 of 83,280 (15%) with its
arithmetic: the skills catalogue lives in the first user message, which
is re-sent every turn, so its 3,119-token saving is charged four times.
The figure was right; the prose put a per-turn number next to a
four-turn ratio and invited the reading that it was not.
* fix(review): make the review-agent prompt role-neutral and its tests bite
Second round on #9678.
The systemPrompt was written for a diff-reading dimension, but the same
type now serves every role the review launches, and it is
`systemInstruction` — it outranks the brief that arrives as a user turn.
Agent 7 reads no diff at all (`readsDiff: false`), `verify` rules on a
findings file, and `reverse-audit` exists to look outside what the first
pass covered, so a frame naming "your diff ranges" and bounding scope to
them contradicted three required roles. A blanket "silence is better
than noise" was worse: the finder briefs carry RECALL, whose whole point
is that a withheld half-believed candidate is unrecoverable, and the
verifier brief withholds RECALL deliberately — a confidence bar from
above breaks both halves. The prompt is now role-neutral and sends the
agent to its brief; the two restraint lines `general-purpose` carried
about a shared tree are restored. Its "absolute paths everywhere" note
also contradicted SKILL.md, which tells worktree-mode agents NOT to
prefix paths for `read_file`/`grep_search`; it now says what it means
about cwd and shell.
The `subagent_type` reminder sat inside the worktree-only `paramNote`,
so the three modes with no worktree were told nothing — and an omitted
type resolves to `general-purpose`, which is the entire cost this change
removes. It is now unconditional, with the non-worktree branch covered.
Three assertions had no teeth: `getBuiltinAgent` returns `null` and
`toBeDefined()` accepts it, `?.tools ?? []` satisfies every
`not.toContain` trivially, and `tools: ['*']` takes the
inherit-everything branch that the "declares a list" filter let through.
A renamed entry failed one test before and fails three now. The SKILL.md
guard was negative-only, so a reworded "Each is a general-purpose
subagent" passed; the set of `subagent_type` literals is now asserted
positively.
The Agent tool splices every type's description into its declaration in
every request of every session, so the description is one clause.
Documented rather than fixed: `agent`, `web_fetch` and MCP tools are
real losses against the inherited surface, not free savings; and a
user-authored `.qwen/agents/review-agent.md` shadows this builtin, which
also traps the file behind `deleteSubagent`'s builtin-name check.
Re-measured end to end on the rebuilt bundle: 55,669 delivered against
139,013 (-60.0%).
* fix(review): scope the cwd rule and correct the tool-search rationale
Third round on #9678. One of these is a regression the previous round
introduced.
The rewritten prompt said "Never `cd`", which is broader than the rule
it was porting: SKILL.md forbids `cd` into the pinned working directory,
not everywhere. The Step 4 verifier is sent to its own scratch tree and
told to work there, and that tree is a SIBLING of the review worktree
(`<worktree>-scratch-<label>`), so `run_shell_command(directory:)` fails
the workspace check and `cd` is the only remaining route. A blanket ban
left the verifier running probes in the shared worktree — the #9207
contamination the scratch tree exists to prevent — or demoting them. The
rule is scoped now, and a test pins the scoping in both directions.
The stated reason for excluding TOOL_SEARCH was wrong, and it was
load-bearing: it was also the argument for rejecting the deferral
alternative in DESIGN.md and in the PR description. A subagent does not
share the parent's registry — `rebuildToolRegistryOnOverride` builds one
per launch and rebinds `getToolRegistry` on the override config — so a
reveal cannot reach the orchestrator's declarations or its cache prefix.
The exclusion still stands on a closed list and on the schema costing
357 tokens/turn, more than two of the tools kept. The DESIGN.md
rejection now rests on the measurement and the blast radius, and records
the argument that did not hold.
Documented, not fixed, both pre-existing and both first surfaced by a
type with a restricted list: `coreToolScheduler`'s skill-activation
reminder gates on the registry rather than on the declared list, so it
is unconditionally true and announces a tool the agent does not have;
and `buildMcpServerInstructionsReminder` has no gate at all beside two
reminders that do. Each changes what every subagent receives, so each
belongs in its own change. The capability the second-order saving buys —
review parts can no longer invoke a project skill — is now named beside
the number rather than left implied.
Re-measured on the rebuilt bundle: 55,789 delivered against 139,013
(-59.9%).
* fix(review): state the launch type on every emission path
Fourth round on #9678, and one item is a gap the third round's own fix
left open.
`typeNote` was hoisted out of the worktree gate but still printed only
by `runRoster`. Step 4's verify shards and Step 5's audit rounds are
built by the other two emission paths, and those are both the most
numerous agents a high-effort review launches and the ones furthest from
SKILL.md's statement of the rule — an omitted `subagent_type` there
resolves to `general-purpose` at full cost, silently. It is now a module
constant carried by all three.
The single-block path carries it on stderr, not stdout. Its stdout IS
the block the orchestrator pastes verbatim, and the delivery check
compares that against the record, so appending to stdout made every
launch differ from its record — five existing tests caught it, which is
the guard working. stderr is the channel this command already uses for
operator-facing lines, and via the ...Safe writer, since #9213 pins that
a broken stderr must not refuse a build.
The SKILL.md paragraph is cut from 816 characters to 457. The skill's
own convention keeps narrative in DESIGN.md because SKILL.md is loaded
on every orchestrator turn — quoting four numbers already verbatim in
DESIGN.md was the opposite of this change's thesis. It also named the
tool set as "read, grep, glob, shell, write, edit", four labels matching
no registered name, while the next sentence asked the orchestrator to
judge what falls outside that set; it now names the registered tools and
a test pins the prose against the registry.
The `subagent_type` assertion is a set, not an ordered array. Pinning
count and order froze the document's shape, so restating the rule at
Steps 4 and 5 — strictly more correct — would have turned it red.
* test(review): give the review-agent pins teeth, and log the monitor loss
Fifth round on #9678.
Four assertions did not hold what they claimed. The tools array was
pinned with `toEqual`, so alphabetising a list whose order carries no
semantics turned the suite red while changing nothing; it is a set plus
a length now. The SKILL.md tool-name pin ran one way only — registry
names must appear in the body — so shrinking the registry left the skill
advertising a capability the agent no longer has, green; it is now set
equality against the sentence itself. The stderr assertion joined every
accumulated mock call in the file and passed whether or not its own
invocation emitted anything, because the enclosing beforeEach clears
only stdout. And the third emission path — the reverse-audit round
header — was asserted nowhere, so dropping its note shipped green while
every Step 5 auditor launched untyped. Each is mutation-verified against
the case that used to pass.
`run_in_background: false` is now pinned too. Dropping it defaulted
every review agent to a background launch, whose findings never return
inline — the review stalls in Step 4 with nothing to aggregate, and no
test was red.
MONITOR joins the deliberately-absent ledger. It is the one removal the
agent is actively pointed at: `shell.ts` answers a blocked foreground
sleep with "use the Monitor tool", and it is not in the subagent
exclusion set, so a `general-purpose` review agent had it. Recorded with
the measurement that qualifies it — across both arms of a real A/B
review, neither the guidance nor the tool ever fired.
* docs(review): record how to re-run the tool-surface measurement
Sixth round on #9678. The figures are load-bearing and quoted in five
places, but a reader had no way to re-derive them — only to re-check the
arithmetic against a table.
No script is committed, because none is needed: both halves of the
measurement run on commands this repository already ships. DESIGN.md now
carries the two-arm build, the per-turn capture, the run-level ledger
read-back, and the environment facts that are part of the result — the
51-tool arm is the product default (35 of them `computer_use__*`, on by
default), not a local quirk.
Two traps are named because both are easy to hit and silent. A second
run of the same PR is an incremental re-review, so an arm sharing a
working tree with the other reuses its findings and the comparison
measures nothing; and `review mock-provider` truncates its record at
8 KB, which is smaller than one tool block, so its log cannot be the
source for per-turn token counts.
Deliberately not a `###` heading: SKILL.test.ts requires every incident
heading under that section to carry a SKILL.md pointer, and a procedure
note is not an incident the orchestrator needs pointed at.
* fix(review): drop the stderr launch note, and stop asserting the brief is a file
Seventh round on #9678. Both items are regressions this PR introduced.
The launch note on the single-block path was moved to stderr last round
because stdout is the block the orchestrator pastes verbatim. stderr is
not a second channel: `ShellExecutionService` returns
`stdout + separator + stderr` as one string and `ShellToolInvocation`
hands that back, so the note arrived inside the very text the caller is
told to copy. It failed the same recorded-prompt equality as stdout
would, except invisibly — the five tests that catch the stdout version
see nothing — and removing it by hand is the edit the delivery gate
forbids, so those launches could enter drift/relaunch repair. The note
now has no channel on that path, and the code says why so it is not
re-added. The rule still reaches those launches: SKILL.md states it for
every `agent` call, and the two paths whose note sits OUTSIDE the ─────
blocks — the roster header and the audit-round header — still carry it.
The role-neutral prompt asserted that the assignment is a brief on disk.
Agent 8 is the reachable exception: `buildWholeDiffBlock` deliberately
writes no brief and SKILL.md appends its domain brief inline, so a
specialist launched that way was told from `systemInstruction` — which
outranks its own launch prompt — to read a file that does not exist. It
is optional and outside `requiredAgents`, so a generic diff walk would
have passed coverage in its place. The instruction is conditional now:
read the brief when the launch names one, otherwise the inline
assignment is the brief.
Both are mutation-verified against the shape that used to pass.
* docs(review): re-measure after the Agent 8 prompt fix
The conditional-brief wording is 27 tokens/turn longer than the sentence
it replaced, so the delivered figure moved: 55,789 to 55,897, and the
gap it is measured against from 83,224 to 83,116. Every citation in the
tree is realigned, including the per-turn table's system-prompt column.
Correctness bought the difference, and it is worth it — the previous
wording sent Agent 8 after a brief that does not exist. Recorded because
a figure quoted in five places is only useful while it matches what the
built bundle actually delivers, and this one is re-measured from that
bundle rather than adjusted on paper.
* docs(review): revert the test file too in the A/B recipe
The re-run recipe's baseline-arm revert list named the review sources but
not `agent-prompt.test.ts`, which imports `REVIEW_BUILTIN_SUBAGENT_TYPE`.
Reverting core removes that export, so `build:packages` fails with
TS2724 — and because the recipe chains with `&&`, the bundle step is
skipped and whatever `dist/` was there before is copied as the baseline
arm.
Reproduced rather than reasoned about: the recipe as written exits 1 on
TS2724, and adding the test file to the list exits 0. That failure is
also what happened when this measurement was first taken; the bundle was
re-run by hand afterwards, which is why the arm was still correct and
the broken recipe went unnoticed.
The note says the general rule, since the specific file will drift:
reverting the sources is not enough, anything that references them has
to go back too.
* test(review): pin the agent-type labels, and fix a comment the delta outran
Both from the round-3 sandboxed verification.
The web-shell `agentType.review-agent` keys were consumed at a live read
site and pinned by nothing. `localizeAgentTypeName` falls back to the raw
id, so a missing key is invisible — the badge renders kebab-case beside
siblings showing "Explore" and "Status Line Setup" and no test fails. The
badge is also newly visible for review agents: both surfaces elide the
type prefix only for the default type, so giving the review its own type
turned a never-rendered label into one shown on every row.
The test goes through `getTranslator`, the lookup the component uses,
because that is what makes the nastiest miss catchable: it falls back to
the English table, so a key added to `en` and forgotten in `zh-CN` still
returns something other than the raw id. Comparing the two locales'
output is what catches it — mutation-verified both ways (zh key removed:
1 red; both removed: 3 red).
The other item is a comment this PR's own previous round outran: the test
that now asserts the single-block path emits the note on NO channel still
opened by saying it emits on stderr, and its name was a beat behind too.
Behaviour and assertions were correct; the prose contradicted them, which
is exactly what would mislead the next reader about what the test pins.
|
||
|
|
6e787aa431
|
i18n(cli): translate Unset in the settings dialog (#9714)
Add the 'Unset' settings-dialog label to all 9 CLI locale dictionaries and register it in MUST_TRANSLATE_KEYS so the normal i18n checks cover it. |
||
|
|
6bbb273a86
|
perf(web-shell): optimize streaming transcript rendering (#9672)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
* perf(web-shell): optimize streaming transcript rendering * test(web-shell): pin streaming fast paths --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> |
||
|
|
4188486419
|
feat(review): give the convergence observation a machine-readable half (#9623)
* feat(review): give the convergence observation a machine-readable half
The diagnosis could tell a human why a loop was not settling and gave a
caller nothing to act on. This adds the two remaining pieces of that work
item: matched recommendations as a closed code set, and the round's own
report on whether its machinery is working.
**`recommendations: [{code, basis}]`** — measurement to advice, with no
constants and no decisions. Four codes are matched from facts this round
already holds: `root-cause-triage` (files that carried findings before and
carry new ones now), `batch-fixes` and `stem-surface` (a trend that is not
falling, the latter only where a floor rung is left to take), and
`land-and-defer` (a round that posts no Critical — a loop whose blockers are
all fixed can end by merging, and a merged pull request cannot diverge).
Every entry carries the deterministic fact it was matched from. The set is
DERIVED from the diagnosis rather than stored on it, and the paragraph's
prose is generated from the same derivation, so the codes a caller wires and
the sentences a human reads cannot describe different rounds. It rides the
composed result and the durable artifact.
The design's menu is larger, and the codes left unimplemented are each
absent for a stated reason rather than forgotten — `split` needs the
diff-topology test, `reset-drift`/`rescope` need `srcDelta`, `fix-pipeline`
needs marker content-hash dedup, `reduce-cadence` is two thresholds, and
`re-anchor` was matched to a cause this repository's measurements did not
bear out. The last one is now DISCLOSED instead: an anchor chain that has
stopped is stated as a fact and prescribes nothing.
**Mechanism health** — a pipeline that has stopped and one with nothing to
do are both silent, so the round says what it can see about itself. Two
checks: a posting floor the reporting reading resolved to critical while the
enforcement backstop failed open (the default configuration's standing gap,
invisible from either side alone — and deliberately not a count of what
posted, since the deterministic `[test]`/`[build]` carve-out is the
mechanism working), and two consecutive rounds withholding the incremental
anchor (every later round re-reads the whole diff until one closes cleanly).
Stated, never acted on. The anchor decision moves to one shared predicate so
the disclosure and the marker cannot describe different rounds.
Also clears the seven Suggestions deferred through rounds 5-7 of #9461: a
dead `floorKnown` parameter and its dead helper, two comments still
describing the fold that was narrowed, the drafted-id path missing the
length bound `idFor` applies (which let the two ends disagree about one
comment over a shortened list), an English cluster clause whose plural form
read the new-finding count as another round, a comment opening the wrong
validator block, and the merged-provenance wiring's missing end-to-end
assertion.
Ten mutations, each verified to turn a named test red.
* fix(review): the posture disclosure needs the manifestation it asserts
Round 1 found two blockers, both in the mechanism-health half.
The posture check held on EVERY default-config round from 6 on — the
reporting and enforcement readings differ only in folding an absent floor to
`auto`, so the gap itself is permanent there — while the sentence it renders
asserts that a Suggestion posted inline because of it. On a Criticals-only
round, or on an APPROVE, that claim was simply false: the clause accused the
posture of failing on rounds where it was not asked to do anything. It now
requires all three conjuncts, because the sentence asserts all three. Gating
on the posted count is safe precisely where the enforcement reading is
false: `floorEnforcedReroute` never ran, so no inline Suggestion can be its
deliberate deterministic carve-out — and when enforcement does engage, the
second conjunct is false and the carve-out can never trip it.
The APPROVE branch also spread the health block twice. Removed — and the
branch's invariant is now written down beside it: neither half can fire
there (the posture half needs a posted Suggestion, which makes the event
COMMENT; the anchor half needs a fail-closed scope, which caps the verdict
off this branch), verified by probe rather than argued. The spread is kept
for symmetry with the convergence block above it, which carries the same
invariant, so a later check that CAN fire here does not have to rediscover
the wiring.
The duplicate has no test, and deliberately so: the branch admits no shape
where the block is non-empty, so any test for it would assert a state the
code cannot reach. The manifestation gate is pinned three ways — the clause
renders once, a Criticals-only round is silent, and a nothing-to-report
round is silent while its anchor-chain disclosure still stands.
* fix(review): close round 2 on the machine-readable half
Two blockers.
The posture check still accused a round the posture was running correctly.
SKILL Step 6 excludes a `[build]`/`[test]`/`[probe]` finding by source at
ANY floor — it is pre-confirmed and stays inline whether or not the floor
engaged — so a fully compliant round that defers every deferrable Suggestion
and posts one such finding satisfied all three conjuncts. My previous
round's argument for the gate was true and beside the point: when the
code-side reroute has failed open, the MODEL-side posture is the layer in
charge, and it carries the same carve-out. The count now excludes
deterministic findings, read off the claim line through the same projection
`floorEnforcedReroute` uses.
The health block shared the convergence paragraph's trim rank while having
no copy outside the posted body — the exact false-record class the
convergence paragraph's own fix exists to close. Worse at the sharpest
corner: with no diagnosis firing, rank 0 held ONLY the health note, so the
trim notice named "the convergence observation" for a section that never
existed. The block gets its own rank with its own name (shed before the
paragraph, because its reader is the operator who has the terminal line),
and the note now rides `ComposeReviewResult`, the `HEALTH:` terminal line
and the durable artifact.
Also: the anchor-chain check states what it measures and names what it
cannot see (the scope is the only withholding leg visible from the body
composer; a plan with no fetched sha, an unreadable plan, and a model
identity drift also withhold, and are decided where the marker is built);
`recommendations[].code` is checked against the closed set instead of cast
into it; the absent-code list now covers the whole eleven-code menu; and the
comment claiming an absent floor reads as `auto` "throughout this module"
now says which reading folds and which does not.
Nine mutations, each verified to turn a named test red. Two of them needed
the fixtures fixed first: the health field and the codes are spread into
three separately-maintained result constructions, and the tests only reached
one of them.
* fix(review): close round 3 on the machine-readable half
The blocker: `land-and-defer` matched on a zero that was not a confirmed
zero. A round capped `cannot-tell-existing-critical` posts no Criticals
precisely BECAUSE existing ones could not be ruled on — the entries ride
their own channel, are never counted, and were never shown fixed — and
`findings-unverified-at-compose` is the same shape. The body would then say
"Unresolved, please confirm:" and "no Critical is open" at once, and the
artifact would tell a machine consumer to merge. The count is passed only
when the blocker state is established, and the module's own "absent is not
zero" rule withholds the code otherwise.
Four of the seven suggestions are corrections to things this branch itself
introduced, three of them last round:
- the health note's own rank made the convergence block's "shed before every
other" comment false; it now says which one goes first and why;
- the accusation counted pathless Suggestions, which the floor structurally
cannot defer — no deferral entry can be built without a path — so it
accused the posture of failing to move something it has nowhere to move;
- the anchor-chain sentence attributed the withhold to the round's SCOPE
while the predicate it reads also fires on a dimension gap and on verdict
caps; it now says only that the round did not close cleanly, and the
docstring names all three legs;
- and the comment misplacement I claimed to have cleared was reintroduced
one line up, by an insert anchored on the statement below its own comment.
Also: the closed code vocabulary is declared once and the type derived from
it, rather than a union and a runtime set kept in step by hand; the `HEALTH:`
terminal line and the health validator's refusal path are pinned.
Six mutations, each verified to turn a named test red. One needed the
fixture fixed first — every test drove a Suggestion that had a path.
* fix(review): close round 4 — land-and-defer needs the scope established too
The blocker: last round's gate covered two of the three unestablished
shapes. An unproven scope — a chunk nobody read, an uncoverable chunk, an
idle agent, context unavailable — also posts zero Criticals while
prior-round blockers sit unread, and the non-repost inference then reads
them as fixed. Such a round carried "cannot show that any of the diff was
read" and "no Critical is open" in one body, and the artifact told a machine
consumer to merge over the unreviewed chunk. The diagnosis moves below
`scopeUnproven` so the gate can read it.
Also: the anchor-chain sentence claimed a clean close ENDS the full-diff
re-reads, which the suite's own passing tests contradict — the marker also
withholds on a missing fetched sha and on a model-identity drift, both of
which a cleanly-closed round can carry. It now says "until a round's marker
carries an anchor again", matching the docstring beside it.
And the duplicate comment I reported fixed last round was only half-fixed:
the file carried the sentence twice, and I deleted the copy above the health
block while leaving the one that opens the recommendations block. Removed.
Six mutations. Three needed the fixtures fixed first, all the same shape:
the `cannot-tell` leg was measured against a bare plan whose unproven scope
withheld the code anyway, and the round's own `land-and-defer` test likewise
never had an established scope. A test that cannot fail without the line it
names is not a test of that line.
* fix(review): one gate for land-and-defer, with every leg named once
Three rounds added one leg at a time to the same gate, and each addition
left the previous rationale describing a gate that no longer existed. Round
5 found three more legs missing. This replaces the stack with one condition
and one comment listing every leg, so the next one cannot be added in a
place the others do not mention.
The legs round 5 named:
- a whiffed dimension withholds the anchor but was not withheld here, so
the artifact told a machine consumer to merge over lines nobody re-read.
The gate now reads the marker's OWN `anchorFailsClosed` predicate rather
than enumerating its legs — which also SUBSUMES the two blocker states
the gate listed separately, since both are caps and neither is
`unreviewed-dimension`. Those conjuncts are removed rather than left as
dead code that reads like extra protection.
- a truncated work list makes the non-repost inference unsound: a shed
Critical is neither re-posted nor ruled on. The same `complete` flag the
freshness rule already reads.
- a pure-foreign list holds none of this account's entries, so its own open
Criticals cannot be re-posted at all.
Also the three stale comments this branch left behind: the superseded
"until one closes cleanly" wording in the `PrevRound.anchored` docstring and
in a test comment (the rendered text and its pin say "until a round's marker
carries an anchor again", and the negative pin is widened to a regex that
catches both spellings); and the health computation's claim that `cappedBy`
is still being appended below it, which the previous round's move made
false — it now names the real constraint, `dimensionGapsAreDepthOnly`.
And the fixture whose own numbers proved the list incomplete (`fresh: 9`
over a one-entry work list) no longer blesses an inference conditioned on
completeness; it uses a shape the pipeline's writer can produce.
Six mutations, one per leg, each verified to turn a named test red. The new
arms are table-driven from the shape that DOES offer the ending, flipping
exactly one leg per arm — the fixture failure that let three of these legs
ship unpinned was arms that would have withheld the code anyway.
* test(review): pin the two land-and-defer gate legs the mutants survived
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(review): pin the draft projections directly, one ledger setup helper
Round 6's two actionable suggestions, both test-side:
- `deferrableSuggestionsInline` and `draftedFindingsOf` were pinned only
through composeReview. Both are exported and unit-tested directly: the
severity gate, the claim-line-only deterministic-tag window, the
pathless exclusion, the LEDGER_MAX_ID bound, the first-keeps dedupe,
and a parity test asserting the count equals the exact set
`floorEnforcedReroute` moves when the floor engages — the divergence
the suggestion names.
- The identical covered-plan + prev-ledger setup pasted at 12 sites
collapses to one `coveredWithLedger()` beside `coveredPlan()`,
deriving the side-file name from the same prNumber. A typo in a
hand-coupled name failed nothing — the reader swallows ENOENT and the
test silently measured round 1; broken at the one place now, 13 tests
redden.
Five mutation probes (helper name, pathless gate, deterministic gate,
id bound, dedupe), each verified red and restored to green.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
|
||
|
|
7bc0d80998
|
fix(review): audit Aone targets in cleanup's bypass tripwire (#9633)
* fix(review): audit Aone targets in cleanup's bypass tripwire Step 9's bypass audit already flags same-account writes on GitHub that bypassed `qwen review submit`, but Aone targets had no tripwire at all — cleanup audited them against GitHub (a hostless report hit github.com's same-named repo; a recorded Aone host pointed gh at a host it has no auth on). Route the audit by the fetch report's recorded host with the registry's cwd-origin fall-through, list the MR's comments through the a1 CLI (default + --resolved union — the default listing hides resolved comments), and flag any comment the authenticated account posted — or edited — inside the window that the submit receipt does not vouch for. Submit now records a commentIds receipt axis (Aone's sanctioned write posts comments, not a review) on success and on a partial post. Closes #9617 * fix(review): preserve both receipt axes on the submit receipt rewrite The submit receipt is keyed by PR number alone but carries an axis per platform — review ids on GitHub, comment ids on Aone — and each writer rebuilt the whole file from only its own axis. A submit on one platform silently erased the ids the other platform's submit vouched for a same-numbered target, and that platform's cleanup audit then flagged submit's own sanctioned writes as bypasses. Merge the whole prior receipt into the rewrite so both axes survive. Also flatten a1's message-less JSON error object in the audit's skip note instead of paging its opening brace, tag an unparseable `a1 auth whoami` answer with the failing command, name the audit's third disclosed residual (an edit of an unvouched pre-window comment is invisible once its discussion is resolved), and pin the previously unwitnessed audit contracts: the receipt vouch's edited-arm exclusion, the Aone auditSince window boundary, the --resolved union's dedupe, the header shape, and both footer platform nouns. * fix(review): tag null whoami answers and disclose audit residuals (#9633) --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
e2de7d2884
|
feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search (#9543)
* feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search
When a PR is created from the Web Shell Git dialog, bind its number and
URL to the current session. The daemon accepts the binding through the
session metadata routes (validated at the route, bridge, SDK, and sidecar
layers, with the URL restricted to http(s) since it is rendered as a link
target), keeps it in live memory, and persists it as a per-session sidecar
file so the binding survives daemon restarts and follows the session
through archive/unarchive/delete.
The sidebar renders a #N badge next to the session title (opening the PR
via the desktop-aware external-link opener, shows the PR in the details
tooltip, and the session search now also matches PR number, branch name,
and worktree slug — so with many concurrent sessions, the one that
produced a given PR is one search away.
EOF
)
* feat(web-shell): Support multiple PR bindings per session
A session can produce several PRs (stacked or follow-up work), and
keeping only the latest binding would defeat the sidebar's
search-by-PR-number flow for every earlier one. The binding is now a
bounded list (10, oldest dropped) ordered by binding time: re-binding
the same number refreshes it and moves it to latest, the badge shows
the newest number with a +N overflow, the tooltip lists every bound
PR, and search matches any of them. The write API stays single-binding
per call; reads, SSE events, and responses carry the full list, with
the sidecar as the complete history merged over the live entry's
daemon-lifetime bindings.
* feat(web-shell): Show PR badges in the session overview and picker dialogs
The mission-control overview panel and the shared session picker row
(resume / delete / release dialogs) now show the same PR badge as the
sidebar — latest number with a +N overflow, opening the PR via the
desktop-aware opener — and the resume dialog's search matches bound PR
numbers, branch names, and worktree slugs through the shared
sessionMatchesGitQuery helper.
* fix(web-shell): Match the overview PR badge color to the sidebar accent
The overview card badge used the panel's neutral --primary tint while
the sidebar and picker badges use the accent violet; one element should
read the same on every surface.
* fix(web-shell): Address review findings on PR bindings
Read/display correctness (verified by ytahdn and the R1 review):
- mergeLiveSessionSummary merged {..existing, ..live} wholesale, so a
live entry's this-daemon-lifetime prs overwrote the sidecar-enriched
full history after a restart; prs is now merged by PR number (live
url wins, history kept), and the dead merge branch in
enrichPrSidecars is gone.
- The pr-only session_metadata_updated event carried no displayName,
which SDK folds treat as "cleared" — the title blanked on every PR
bind. The producer now echoes the current name.
- GitDialog synced sessionIdRef from the prop on every render, so the
fresh session id the dialog resolves for its own side queries was
clobbered before the binding read it; the sync now runs only when
the prop changes.
Robustness:
- upsertSessionPr's read-modify-write is serialized per sidecar path,
closing the concurrent-bind drop race under runSharedMany.
- The REST routes persist the sidecar before mutating the bridge, so a
failure on either side leaves the binding durable.
- The ACP dispatch only upserts when the call actually binds a PR (a
displayName-only rename no longer rewrites createdAt/order).
- pr.url is capped at 2048 chars across all four validation layers.
Structure & a11y:
- The three badge copies (sidebar / overview / picker) are now one
SessionPrBadge component: shared CSS, count-aware aria-label,
non-http(s) entries filtered defensively, and tabIndex=-1 inside
listbox options.
- The SDK's duplicated PR validator is a single session-pr module used
by both DaemonClient and events.
- Delete/Release dialogs' search matches bound PR numbers like Resume.
Tests: list-level live+sidecar prs merge, sidecar-vs-bridge echo
authority, route tests made order-independent, bridge
atomicity/cap/catalog-revision/displayName-echo cases, concurrent
upsert serialization, SDK fold keeps the name, GitDialog bind-failure
degradation, url-cap rejections at every layer.
* fix(core): harden session pr sidecar persistence and moves (#9543)
* fix(serve): align session pr echoes with the persisted sidecar (#9543)
Address round-4 review findings:
- R4-1 (Critical): bridge entries are re-created without prs on daemon
restart / close / archive-restore, so ACP and REST metadata updates
replied and broadcast only this daemon lifetime's bindings, silently
dropping persisted history. Hydrate the entry from the sidecar before
the mutation (new optional bridge seedSessionPrs) and make the ACP
handler reply with the authoritative persisted list like the REST
routes, fixing both the response and the session_metadata_updated
event on all three surfaces.
- R4-2 (Critical): the non-live metadata fallback persisted the rename
before the PR sidecar while bumping the catalog revision only after
both writes succeeded; a failed sidecar write stranded a durable,
unannounced rename behind a total-failure response. Persist the
sidecar first so a failed write leaves nothing durable behind.
- R4-3: map InvalidSessionMetadataError in toRpcError to the REST
invalid_metadata contract instead of an opaque -32603 Internal error.
- R1-5: add the stderr audit record for pr binding mutations, mirroring
the displayName branch (accepted in the round-2/3 thread).
- R2-4 (source part): make enrichPrSidecars' archiveState required so a
future archived-listing call site cannot silently enrich from the
active chats dir.
- R2-16: filter non-openable URL schemes in the session details tooltip
exactly like SessionPrBadge.
* chore(desktop): regenerate bun.lock to match workspace versions
Main's desktop lockfile drifted: @craft-agent/electron and
@craft-agent/shared are 0.0.5 in the workspaces but 0.0.1 in the
lockfile, and the @qwen-code/live-host workspace entry is missing.
bun install --frozen-lockfile (the Live Host CI gate) fails on any
PR touching packages/sdk-typescript/src/daemon/types.ts because of
this. Regenerated with bun 1.3.x.
* fix(web-shell): address R2 review findings on PR bindings
- SessionPrBadge: narrow onKeyDown to Enter only so the badge no longer
blocks roving-listbox navigation keys in picker dialogs (R2-15).
- SDK updateSessionMetadata: per-entry prs shape gate so a hostile or
buggy daemon response cannot surface javascript: urls or malformed
numbers downstream; valid entries survive (R1-17).
- Tests: bridge mirror atomicity (valid pr + invalid displayName),
GitDialog stale-id retry binding, list-level merge dedupe by number,
organized + archived listing paths keep PR sidecars, DaemonClient pr
request/parse + gate, Delete/Release dialog PR-number search (R2-6,
R2-13, R2-14, R1-15, R1-17, R2-20).
* fix(serve): address R5 review findings on PR bindings
Best-effort hydration (R5-1/R5-2/R5-3): the sidecar hydration read at
all three metadata-mutation sites (ACP dispatch, primary and workspace
REST routes) now absorbs non-ENOENT I/O errors as "no sidecar" instead
of failing the whole call — a squatted sidecar path no longer turns a
pr-less rename into a 500/-32603. This also makes the R4-2 fallback
ordering test reach the branch it names.
Validation hardening (R5-4): pr.url rejects control characters at all
four layers (bridge via hasControlCharacter, route, SDK guard, sidecar
reader) — the url is interpolated into the stderr audit line, so a
newline-bearing url could forge audit records.
Traversal parity (R5-5, R4-5): the ACP session/update_metadata handler
now gates on isValidSessionId before any sidecar I/O, and the primary
REST route's gate moved ahead of runtime resolution so traversal ids
get 400 invalid_session_id identically on single- and multi-workspace
daemons (previously 404 on multi-entry registries).
Tooltip (R5-6): PR rows key on index+number — a hand-edited sidecar
with duplicate numbers no longer risks cross-row reconciliation.
Tests: FakeBridge callLog pins seed-before-mutate order (R5-9);
cross-workspace pr sidecar lands in the owning workspace's chats dir
(R4-4); multi-workspace traversal test; metadata-filtered listing
keeps prs (R1-15); Resume dialog PR-number search (R2-20); dialog
fixtures annotated DaemonSessionSummary[] (R5-7/R5-8); control-char
rejection cases at bridge and sidecar layers.
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
|
||
|
|
e44a16d8b5
|
fix(cli): keep slash menu selection stable during streaming (#9494) (#9508)
While a response streams, session stats / pending-item updates rebuild commandContext. useCommandSuggestions listed commandContext in its effect deps even though only the async argument-completion callback consumes it, so every context rebuild re-ran the search and replaced the suggestions array with an identical-content copy. useCommandCompletion resets the active index to 0 whenever the suggestions array identity changes, which snapped the user's menu selection back to the first item mid-navigation. Read the context through a ref (same pattern as historyRef in slashCommandProcessor) and drop it from the deps so only real inputs — query, command list, recent commands — rebuild the suggestions. Adds hook-level regression tests wiring real useCommandCompletion + useSlashCompletion: selection survives context churn (red before the fix), query/command-list changes still reset it, and argument completion still observes the latest context. |
||
|
|
f1d05b79fc
|
feat(review): detect self-MR on Aone targets in presubmit (#9629)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(review): detect self-MR on Aone targets in presubmit The self-PR verdict downgrade existed only for GitHub targets: the Aone read path skipped presubmit entirely, so a review of one's own MR silently carried the weight of an independent review (#9616) — exactly the wrong direction for the most common local Aone flow, re-reviewing one's own CR before the next amend. presubmit now routes by platform. On an Aone target it compares the authenticated account (a1 auth whoami) against the MR author from one mr view fetch — case-insensitive, fail-soft on a deleted author, fail-closed on an unreadable MR — and emits the same report shape with the unbacked slices neutral (CI classification and comment dedup have no Aone backing yet). The same fetch backs head drift via sourceBranch, and a malformed pr_number/owner_repo stays a usage error rather than a metadata blip. SKILL.md runs presubmit on Aone targets instead of skipping it, and the "self-PR detection has no Aone backing" caveat is gone from the skill and the user docs. * fix(review): unify Aone live-head reads and the presubmit whoami gate The round-1 review of the Aone presubmit found four seams the new path had hand-derived a second time; each is now stated once: - The self-PR comparison (including the load-bearing `author !== ''` guard) existed as two inline copies in presubmit.ts; isSelfReview states it once for both platform paths so a future normalization rule cannot diverge one platform silently. - "An Aone MR's live head is mr view's sourceBranch" was hand-derived in five places in aone.ts, two of them untrimmed: a padded server value then manufactured a phantom "PR head advanced during review" (and a submit-time refusal) against the trimmed reads, for an MR that never moved. aoneHeadSha states the fact once; all five sites route through it, repairing the two untrimmed copies. - The "same report shape as GitHub" invariant was convention only; both presubmit result literals are now typed against one PresubmitReport interface, so a field added to the envelope is a compile error on the path that forgets it instead of a silent toBool(false) at the consumer. - The Aone path spawned `a1 auth whoami` twice per run (plain gate + JSON account read). The gate now runs the JSON whoami once and returns the account: one spawn per run, and no account fetch remains after the MR fetch that could throw uncaught and orphan the graceful metaUnavailable report — the fail-closed path pays no a1 work after a thrown mr view. The padded-head regression cells for getPrMeta/getFetchMeta fail on the pre-round code; the empty-guard, single-spawn, and report-shape witnesses each fail under mutation probes. 4143 review tests green. * test(review): pin Aone presubmit auth-gate throw path (#9629) --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
04886c4354
|
fix(review): make the incremental cache work for Aone AGit-Flow CRs (#9630)
* fix(review): make the incremental cache work for Aone AGit-Flow CRs * docs(review): qualify the Aone no-ancestry claims in comments and docs The D7 comments described the ancestry gate as unconditional and both ancestry tests as failing for every AGit-Flow update; the head test alone fails for every amend (the clamp fires only on amend-plus-rebase), and the narrowing join never lets a drift byte reach the published scope. Qualify the ledger.ts SHA_RE block, the resolveIncrementalAnchor docstring, the clamp-skip and call-site comments, the test-block comments, and the design/user docs accordingly. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
0dd518f950
|
feat(review): disclose that Aone posts join the discussion gate only (#9625)
* feat(review): disclose that Aone posts join the discussion gate only Aone has a dedicated ai_comment merge gate for AI-posted review comments. A controlled probe on a scratch CR (issue #9614) resolved the design doc's open question Q4: `a1 repo mr comment create` does NOT auto-set isAiComment for the posting identity (a general and an inline probe both read back false, re-checked against an async classifier), and a1 v0.1.90 exposes no flag to request it — so qwen-posted comments sit in the generic discussion gate only, and the ai_comment gate never tracks them. The same probe re-confirmed Q3: still no native reject/request-changes on the a1 mr surface. Until a1 ships a marking flag (feature request to the a1 CLI), the write path discloses the gate split instead of silently implying participation: the Aone REQUEST_CHANGES note names the posted comments as unflagged and the discussion gate as the only mechanical block, and SKILL.md / the user docs carry the same fact. createMrComment documents the constraint and is named as the seam where a future marking flag wires. * test(review): pin the directional ai_comment gate claim in the Aone disclosure note Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(review): bind the Aone gate-disclosure pins to content, call, and count source --------- Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
e40263ee55
|
chore(deps): Clear high-severity CVE baseline and harden the security gate (#9584)
* chore(deps): Clear high-severity CVE baseline and harden the security gate - Bump OpenTelemetry stack to 0.221.x (fixes @opentelemetry/core advisories) - Bump @larksuiteoapi/node-sdk to ^1.73.0 and override axios to ^1.19.0 - Bump mobilewright to ^0.0.53 (drops vulnerable sharp 0.34.x) - Bump markdown-it to ^15.0.0 (drops vulnerable linkify-it 5.x) - Update undici/fast-uri/brace-expansion/ip-address within range - Adapt telemetry code to OTel API changes (forceFlush, processor options) - Make security-checks a hard gate now that the high baseline is clean * chore(deps): Refresh mobile-mcp vendored lockfile to drop vulnerable sharp * fix(telemetry): stub sdk-node 0.221 env auto-config helper packages sdk-node 0.221 extracted its env-based auto-configuration into @opentelemetry/configuration, otlp-exporter-base, and otlp-grpc-exporter-base, which it now requires eagerly. The existing esbuild stub only covered the exporter-* packages, so the OTLP protocol chain (grpc-js, protobufjs, otlp-transformer) re-entered the sdk-impl static closure and tripped the serve fast-path bundle guard. Stub the three helper packages when imported by sdk-node only; our own protocol modules keep resolving the real packages. qwen-code never reaches these helpers at runtime (explicit exporters + env scrub). * fix(telemetry): disable metrics fallback without reader * fix(vscode): restore nested dependency notices * fix(deps): declare bundled punycode so its notice survives regeneration The CLI esbuild config aliases punycode to the userland package (esbuild.config.js), so the shipped CLI bundle contains MIT-licensed punycode@2.3.1. Its NOTICES.txt section was lost because the only lockfile paths reaching punycode were dev-only; the notice walker (rooted at vscode-ide-companion) never sees a production declaration. Declare punycode as a direct production dependency of the CLI (the bundle input) and of vscode-ide-companion (which packages the bundled CLI into the VSIX and owns NOTICES.txt), then regenerate the lockfile and notices so the MIT notice is restored. |
||
|
|
d0d68c0e8b
|
feat(cli): extend non-blocking slash commands to more builtins (#9495)
* feat(cli): extend non-blocking slash commands to more builtins #8130 opted /about, /help, and /settings in to run immediately while a response streams. Apply the same criteria to eleven more builtins so local UI controls no longer wait for the active turn: - UI-preference commands whose saved changes apply through the existing settings hooks: /theme, /editor, /vim, /voice, and /terminal-setup. - Read-only status commands: /tools, /lsp, /tasks, /hooks, /docs, and /bug. Commands that submit model turns, mutate conversation state, or read state the active turn is writing remain serialized, as documented in the non-blocking slash commands design doc. Each opt-in is pinned by a unit test. * docs(cli): scope extended-command claim to the eleven opted-in builtins |
||
|
|
e09399e0fb
|
feat(core): make list_directory opt-in (disabled by default) (#9424)
* feat(core): make list_directory opt-in (disabled by default) glob covers directory listing in most cases, so list_directory is now disabled by default to keep the tool surface lean. Enable it with tools.listDirectory.enabled=true or by listing it in the coreTools allowlist (--core-tools / tools.core). The plan-mode reminder no longer steers the model toward the tool. * fix(core): align list_directory opt-in gate with prompts and allowlist parsing Round-1 review findings on the opt-in gate: - Normalise coreTools entries with parseRule so specifier forms such as list_directory(/src) still re-enable the tool. isLsToolEnabled used resolveToolName, which does not strip specifiers, while PermissionManager admits the same entry via parseRule — so the allowlist accepted the tool and the registry never got it (R1-1). - Drop list_directory from the plan-mode block error. That message is returned to the model as the tool result of every blocked call, so it kept steering the model at an unregistered tool even after the system prompt stopped doing so (R1-2). - Remove the now-inert ToolNames.LS entries and the prompt text that advertised the tool to built-in agents. The gate also guards the forked-registry rebuild path, so these agents were silently stripped of a tool their own prompts told them to use (R1-3). - Say why the tool is missing and how to enable it in the tool-not-found message, instead of offering unrelated Levenshtein suggestions (R1-6). - Cover the alias and specifier forms in the registration tests (R1-4), and drop the stale examples from the sub-agents and Java SDK docs, where the listed entry can no longer register the tool (R1-5, R1-7). * fix(core): address list_directory opt-in round-2 review findings - Reword the skill-review agent task prompt so the inspection guidance only references read_file; the run's tool filter has no list_directory, glob, or shell, so the old `ls` instruction was unsatisfiable for the turn-budgeted background agent. - Resolve tool-name aliases (ListFiles, ListFilesTool, ReadFolder) in the list_directory not-found message so aliased calls get the enablement explanation instead of a Levenshtein suggestion. - Attribute a missing list_directory to the workspace tools toggle when the workspace disabledTools set blocks it, since the opt-in setting cannot lift that state. - Drop the coreTools allowlist advice from the enablement message: setting tools.core to ["list_directory"] alone would exclude every other tool. - Switch the two remaining sdk-java runTransportOptionsExample copies from list_directory to glob; setAllowedTools only adds auto-approve rules and never registers a tool. - Extract the shared fake-server scaffolding in the list_directory integration tests into a local helper, keeping the load-bearing CLI-flag comment. - Pin the skill-review agent tools array and assert the learn-skill prompt steers to read_file / glob, matching the sibling planners. * fix(core): only claim list_directory is opt-in-disabled when it is unregistered The not-found explanation resolved aliases (ListFiles, ReadFolder) before checking whether the tool was actually absent. The registry is keyed by canonical names while the lookup that reaches this path resolves legacy migrations only, so an alias call missed even when list_directory was enabled — and the message then told the user to switch on a setting that was already on, hiding the generic path's "Did you mean list_directory" self-correction. Gate the branch on the canonical name being absent from the registry, and pin the two alias combinations that were unpinned: an alias against a non-empty workspace disabledTools set, and an alias against a registered tool. |
||
|
|
98854a38d4
|
feat(review): fold the one-hop import widening into fetch-pr --since (#9332)
* fix(review): gate the recovered incremental anchor on the model that certified it Incremental scoping is a same-model contract: "clean up to this commit" is one model's verdict. The cache path has always enforced it through lastModelId, but the anchor recovered from the posted review's ledger marker shipped bare, so a round run under a different model would scope sha..HEAD past code the current model never reviewed — permanently, since each clean round re-anchors past the last. The marker now carries the certifying model beside the anchor, riding and falling with it: withheld on fail-closed and truncated rounds, and dropped by the parser when the sha beside it did not survive. The recovered-ledger context section names the model and instructs the gate (absent counts as a mismatch — markers predating the field), and the skill's incremental check requires a model match on both the cache path and the marker-recovery path before scoping to the interdiff. The findings work list still carries across models — every entry is re-asserted against the code — only the anchor does not. * feat(review): rescope — deterministic incremental plans, widened one import hop Incremental review existed only as prose: Step 1 said "compute git diff <lastCommitSha>..HEAD and use it as the review scope" and left the mechanics to improvisation. The improvisable route — re-run plan-diff over a hand-captured interdiff — silently degrades the plan (no worktreePath, no PR identity, no heaviness), dropping Agent 0, the modeled-system lens and every invariant agent from the roster. `qwen review rescope --plan <plan> --anchor <sha>` moves the scope decision into code: it re-validates the anchor against the history, captures the interdiff with the pinned flags, widens it by one import hop — every still-clean source file that imports a changed file re-enters the scope with its full-range hunks — and rewrites the plan in place with the same builders fetch-pr used, identity fields riding through and post-image line counts intact. The plan gains an `incremental` block; chunk briefs annotate each file's class (changed = review in full, interaction = review the seam only), and whole-diff briefs carry the frame once. Failure is directional: any refusal leaves the plan untouched, so the fallback is the full-range review, never a skip; an empty interdiff exits 3 and maps to the same-SHA outcomes. The widening exists because "clean" was certified against the code as it stood: a fix that moves a contract can break an unchanged caller, and an interdiff-only scope never re-opens it. Dependents only, source only, one hop; the scan is a documented heuristic whose misses keep exactly the pre-widening floor. * fix(review): harden rescope and the widening against review findings Findings from the PR's own review rounds, each verified before fixing: - Scoped files now carry FULL-RANGE hunks; the interdiff only chooses which files are in scope. Since-anchor hunks broke inline-comment anchoring: a fix round that restores lines the previous round changed produces hunks that exist nowhere in the PR's own diff, and one such anchor 422s the whole posted review, all-or-nothing. - EXT_MAP maps .js to BOTH .ts and .tsx — under react-jsx a .tsx file emits .js, and 921 of 6,200 relative .js specifiers in this repo named .tsx targets no edge could reach. Root-escape guard is segment-exact (a '..config' directory is not an escape), and the documented dist/ deep-import remap now actually strips the dist/ segment. - rescope refuses an already-rescoped plan (a second pass derived candidates from the shrunk file list and repointed fullDiffPath at the file it was about to overwrite) and a plan with missing or malformed files[] (normalising to [] silently dropped every widening candidate). All git calls are pinned with -C to the plan's worktree: pathspecs resolve against git's cwd, and from a subdirectory an unmatched pathspec exits 0 with empty output instead of failing. - incrementalScopeOf honours its degrade contract: interaction entries whose edges failed validation are dropped, and a block with no surviving scope renders no incremental frame at all. - Whole-diff briefs name each file with its scope class (capped list); chunk briefs state that scope classes override the generic duties for interaction files; heavy INTERACTION files get no invariant agents — their full-range slice is exactly the code the previous round cleared. - incremental.contextFiles (23 KB measured on a 300-file plan, with no reader) is now a count; fullDiffPath is named in the skill prose. SKILL.md states rescope runs from the main checkout, not the worktree. - Test batch from the mutation findings: exit-code literals pinned, --out exercised, diffPathAbsolute asserted, one-hop limit gated, same-sha refusal byte-compared, heaviness preservation asserted, test-file dependents excluded, cross-package widening exercised, fileLineCount covered at the git layer. * fix(review): round-2 findings — slice the fetched diff, cap and reconcile the frames Round-2 review findings, each reproduced before fixing: - The composite is now a BYTE-SLICE of the fetched full-range diff, not a pathspec-scoped re-capture: a scoped re-capture cannot see a rename source, un-pairs the rename, and renders a whole-file add whose hunks exist nowhere in the PR's own diff — the second entrance of the same 422 anchor class the round-1 redesign closed. Slicing also keeps the subset invariant byte-exact. sliceDiffByLines moves to lib/diff-plan. - deltaFiles is reconciled with the sections the composite actually holds (a file restored to its merge-base state names no phantom scope; its importers still widen), a files[] whose entries carry no usable path refuses like an empty one (zero-compared must never read as nothing-changed), and an unwritable --out exits 2 instead of throwing. - The whole-diff frame carries the same scope-class-WINS reconciliation as chunk briefs (agents 1a/1b sweep duties re-opened round-1 findings over interaction hunks), scope lists cap edges per entry (8) as well as entries (30), anchors render inertly, empty-string edges degrade, a chunk with no classed files gets no frame, and the frame wording is flow-neutral (review's base, not PR base). - Roster: interaction paths subtract deltaFiles (a path in both lists is live delta — widening wins), the field is declared on RosterPlan, and heavyFiles' doc is re-attached. - import-graph: dist deep-imports resolve under BOTH emit layouts (dist/src/… and flat dist/…), and the header now states the honest wrong-edge cost of unparsed exports maps (one extra widened file, never a narrowed scope). - Tests: rename-preserving slice, restored-file reconciliation, empty and zero-usable files[], out-of-worktree cwd run, unwritable --out, exact contextFileCount, head-distinct heaviness oracle, .cjs resolution, both dist layouts, list/edge caps, both-lists roster widening, no-frame-for-unclassed-chunks. * fix(review): round-3 findings — follow the lineage, absolute full-diff path Round-3 review findings on the rescope layer: - R3-1 (Critical): a file renamed BEFORE the anchor and deleted in the fix round carries two names — the post-image name in the interdiff, the left-side name on the PR diff's deletion section — so the section holding its unreviewed hunks matched no scoped name and silently vanished (or exited 3 as 'nothing new'). An unmatched delta file is now dropped only when a cheap per-file probe proves it a genuine RESTORATION (identical blobs on both sides of the PR range); any other lineage break refuses to the full range, and the check runs before the empty-sections exit so the refusal wins. - incremental.fullDiffPath is absolute: a cwd-relative path is meaningless to the later step the field exists for (R3-9), and the exit-3 contract in the header now names both of its causes (R3-8). - The roster's interaction-path reader applies the same validation the brief renderer does — anchor present, every entry carrying a surviving edge — and a malformed deltaFiles disables the narrowing entirely rather than just its delta-wins subtraction: with no trustworthy delta list there is no way to tell a seam-only file from a live one, and every malformation here must widen (R3-2, R3-10). - Tests: rename-then-delete refusal, restored-only exit 3, unwritable --out leaves the plan byte-identical, sliceDiffByLines gets a direct suite (parse → slice → parse round-trip, byte-exactness over invalid UTF-8 and lone CR, range ordering and clamping), and the resolver's literal-form candidate is pinned. * fix(review): certify the ledger anchor with the runtime model identity * fix(review): pin the posted marker's model wiring and tighten the anchor-gate spec * fix(review): round-4 findings — chunk-scoped role briefs, resolver gaps Round-4 review findings: - R4-1 (Critical): a chunk-scoped ROLE brief (the reverse auditors, the one role accepting a chunk) received per-file scope classes only from the globally capped list, so on a wide round its own files could be elided past entry 30 — the sole reviewer of that territory left without their class and with no way to recover the tail. Its own chunk's files are now listed in full, and the chunk brief's seam bullet drops the display cap for the same reason (R4-2); the cap stays where it belongs, on the whole-diff frame. - The resolver gains the `.jsx` emit row (a JSX source emits `.js` under the same convention as `.tsx`, R4-4) and normalises bare-package subpaths through the same POSIX rules relative specifiers already get, refusing escapes (R4-5). - A plan file that parses to JSON `null` now refuses instead of throwing a TypeError past the catch (R4-7), `fileLineCount` is `-C`-pinned like every other git call in the module (R4-9), and a `deltaFiles` array of non-string junk disables the roster narrowing exactly as a missing list does (R4-11). - Tests: chunk-scoped role brief listing, `.jsx` and subpath normalisation, junk-deltaFiles widening, JSON-null plan refusal. R4-8 declined with rationale, recorded in the code: a file absent at BOTH ends of the PR range is either a net-zero add-then-delete (safe to drop) or a rename-before-anchor whose deletion hunks sit under its pre-rename name (dropping loses them). This layer cannot tell them apart, and dropping re-opens the round-3 Critical, so the refusal stands. * fix(review): shed the anchor pair first and pin the round-3 findings The marker's byte-cap loop dropped a finding before the anchor pair; `dropped` then withheld the pair in the same render, so a capped clean round lost a ruling it was owed. Shed the pair first — the work list survives and recovery degrades to the full diff. Plus the round's pins: attribution-off withholding of the runtime-injected model, the submit fixture's production filename encoding, the skill's same-model gate clauses, and the differing-SHA gate in the user docs. * fix(review): scope the identity-channel claims and pin the branch-1 gate The boundary comments and DESIGN.md claimed the runtime identity channel delivers what the mechanism cannot: a model-authored command prefixes its env, and the override reaches the child (measured in this repo's bash -c spawn shape), so "the model the session ACTUALLY runs, not the id the state JSON typed" overstated the guarantee. Scope every PR-owned claim to what the wiring delivers — the runtime id supersedes the typed one, and the channel stays forgeable, same posture as the cache path. Plus the revert-guard's missing pin: branch 1's `If SHAs differ **and** model matches` clause was unpinned, so a partial revert dropping only it left every suite green (measured); the pin makes that revert fail and does not misfire on the PR state. * fix(review): shed the dead anchor tie-break and pin the reprieve clauses * fix(review): round-5/6 Criticals — readers for restored files, honest exits Per the posture announced last round, this lands Criticals only. - R6-10: a delta file the fix round RESTORED to its merge-base state fell between both reader classes — no PR-diff section, so no full review, and inside `delta`, so the widening skipped it as a candidate. Its imports of files that are still changing therefore had zero readers. The restoration probe now runs BEFORE the widening and splits the set: every changed file (restored included) still pulls its importers in, because a revert moves their seam too — round 1 cleared them against the pre-revert callee, and (importer@head x callee@base) is a pairing no round has seen — while the restored files themselves become candidates in a second pass keyed on the LIVE delta, since a restored file importing another restored file has no moving side to check. - R5-14: nothing past the plan write may throw. "Only exit 0 rewrites the plan" needs its contrapositive to hold, and a dead stdout (`qwen … | head`, a daemon redirect) made the courtesy reporting raise EPIPE — exit 1 over an already-rewritten plan, sending the caller down the "full-range plan untouched" branch against an incremental one. - R6-16: `fetchedSha`/`mergeBaseSha` were taken on type-check faith. Both ends of the PR range must be object ids: a clobbered plan naming a moving ref would resolve at call time, so the interdiff describes one tree and the worktree reads another while the exit-0 plan claims incremental scope. Each of the three tests was mutation-checked: reverting the fix it pins turns it red. * fix(review): round-6/7 Criticals — whole tree entries, and an async-proof exit Criticals only. - `restored()` compared blob oids (`rev-parse <ref>:<path>` yields nothing else), so a fix round that reverts the content and KEEPS `chmod +x` — or swaps a file for a symlink with the same text — was misclassified as restored and dropped from scope. Its mode-only section is in the PR's own diff (parseDiff emits one, planChunks gives it a chunk), so the incremental path narrowed BELOW the full-range floor it is documented to hold and exited 3 "nothing new" over a change nobody reviewed. The probe now compares the whole tree entry, mode included, via a pathspec-pinned `ls-tree`. - The round-5 EPIPE guard caught only the synchronous throw. A dead stdout also surfaces as an ASYNC 'error' event on the stream, which no try/catch around the write can intercept and which terminates the process with exit 1 — over an already-rewritten plan, sending the orchestrator down the "full-range plan untouched" branch against an incremental one. A persistent no-op 'error' listener makes that shape inert; the test now pins both. Both tests were mutation-checked: restoring the blob-only probe, or removing the listeners, turns them red. * fix(review): stamp the round's model at capture, qualify it by provider Two ways the same-model gate could certify a range under a model that did not review it. 1. Deferred post. compose/submit read QWEN_CODE_MODEL at POST time, which tracks the session's CURRENT model — review under A, /model to B, "post comments" and the marker said B. The next round under B then scoped sha..HEAD past code B never saw. fetch-pr now stamps reviewModelId into its report when the diff is captured, and compose withholds the sha/model pair outright when that stamp disagrees with the runtime posting it: the round cannot name who reviewed the range, so it certifies nobody and the next round reviews in full. The findings still post. 2. One model id, two providers. A bare id is unique only inside one provider configuration; two of them exposing 'qwen3-coder-plus' would pass each other's gate. Config now publishes QWEN_CODE_MODEL_IDENTITY — <model>@<8-hex of authType+baseUrl> — beside the bare id, and the review flow prefers it. A runtime that publishes neither yields '', which reads as a mismatch, not as agreement. The identity slot is process-global while the model is per-session, so shellContextEnv hands it down only while it still describes the model resolved for THIS session; a daemon side-session gets the bare id rather than another session's qualification, since a confidently wrong identity passes a gate the coarse one would have failed. Every new test mutation-checked. * docs(review): correct the absent-stamp and model-cap notes The reviewModelId doc claimed compose reads an absent stamp as "unknown"; it reads it as today's behaviour, and the reason is worth stating — the report is written at the start of a round and read at its end, so a missing stamp means an upgrade landed between the two, and a runtime that publishes no model id empties the other side of the comparison anyway. The ledger cap's note predates the provider qualifier, which adds nine characters to every id it bounds. * style(review): prettier the reapplied round-model helper * fix(review): drop the duplicate `incremental` field the merge left behind main's #9100 declared `incremental?: unknown` on agent-prompt's local PlanReport, and this branch already had one for the rescoped plan; the merge kept both, which is TS2300 and failed the build for every PR in the stack. Kept the documented one. Missed locally because vitest transpiles through esbuild, which drops types without checking them — a duplicate interface member is invisible to the test run and only `tsc --build` sees it. * fix(review): rule the same-model gate in the CLI, key the identity per session Four blockers from round 9, all in the identity plumbing this PR adds. R9-1: the recovery path's gate could never fire. The marker's `model` is the provider-qualified identity (`<model>@<digest>`), but SKILL.md told the orchestrator to compare it against `{{model}}`, which BundledSkillLoader substitutes with the BARE `config.getModel()` — two identity spaces that are never equal, so every same-model continuation round silently re-reviewed the full diff, which is the whole payoff this PR exists for. Read loosely instead, a prefix match would have accepted another provider's same-named model and re-opened the scope-skip the digest closes. The comparison now happens in the process holding both values: `pr-context` renders the verdict — "the same-model contract HOLDS" or "**Do NOT pass the reviewed-at sha as `--since`**", naming both identities either way — and the skill obeys that sentence instead of comparing strings. A section with no verdict is a mismatch. The cache path keeps its bare-`{{model}}` gate: Step 8 writes `lastModelId` from the same bare value, so that path is self-consistent. R9-2: in daemon mode the identity leaked across sessions. The slot is process-global and first-writer-wins, and withholding by OMITTING the key is not withholding at all — every spawn site composes the child env as `{...process.env, ...getShellContextEnvVars()}`, so the stale global rode the spread and session B stamped its marker under A's identity. Now registered per session beside the model (dropped together on unregister) and written as `''` on a miss, the precedent the agent and prompt ids in that file already set. The global slot stays the single-session CLI's fallback, guarded so one that describes another model is dropped rather than mis-qualifying this one. R9-3 (×2): the two wiring tests never cleared QWEN_CODE_MODEL_IDENTITY, which the boundary under test prefers — so an ambient value, which this PR's own Config now publishes into every subprocess, overrode the model they set. Running the suites inside a Qwen Code session is the dogfooding path, so that was the normal case, not the exotic one. Also folds the four inline `?? ` chains into lib/round-model.ts: `roundModelIdFrom` and `certifierMatchesRound`, the latter pinning whole-string equality and every unknown — absent certifier, unpublished runtime, two blanks — as a mismatch. Every new test mutation-checked. * feat(review): fold the one-hop widening into `fetch-pr --since`, drop `rescope` main's #9100 landed anchor validation and scoping inside `fetch-pr`, which is where this work belongs — so the `rescope` subcommand it was built as is gone (612 lines of command, 728 of test), and what was unique to it now runs on the `--since` path. Two changes to what an incremental round reviews. The scoped diff is a SLICE of the PR's own diff, not a re-capture of `since..head`. The delta decides WHICH files are in scope; their hunks come from the full range. Every hunk an agent can anchor a comment on is therefore byte-identical to one GitHub renders, and an inline-comment 422 takes the whole Create Review call with it. It also dissolves a refusal: an "undo per feedback" commit reverts lines back to base content, so a re-captured delta carries hunks the PR's diff does not contain — `hunks-outside-pr-diff`, which cost the round its whole scope. Sliced, that file is simply reviewed at the shape GitHub shows. And the file set is widened by one import hop. A still-clean source file that imports a changed one re-enters: round 1 cleared it against the callee's OLD shape, and (importer@head × callee@head) is a pairing no round has seen. This is only expressible under slicing — an importer is unchanged by definition, so no delta capture can show it. `incremental.scope` names each file's class (deltaFiles, interaction with the edges that pulled each one in, contextFileCount, restoredFileCount) and the superseded full range stays at `incremental.fullDiffPath`. A file restored to its merge-base state owes no review — mode-aware, so a content revert that keeps `chmod +x` is not a restoration — but still pulls its importers in. New refusal `lineage-unfollowable`: a delta file with no section of the PR's own diff under that name (a rename before the anchor) cannot be sliced, and refusing costs a full review where guessing loses hunks. An unparseable delta is `containment-unverified`, never `upToDate` — the empty file list is the parser's, not the tree's, and reading it as 'nothing changed' would stop the round over a failed capture. The scope logic is a pure module with injected readers, so it is unit -testable without a repository. Four new fetch-pr cases cover slicing, widening, restoration and the nothing-new stop; every one mutation-checked, and each of the four mutants (no slice, no widening, no restoration probe, widen on the live delta) turns the suite red. * fix(review): make the blanked identity fall back, and drop the anchor pair whole Round 10 filed no Criticals; these are the deferred items that were defects rather than coverage gaps. The R9-2 blanking silently disabled the bare-id fallback. `??` falls back on ABSENT, not on empty — and the identity slot is deliberately written as '' when a session has none to publish, because an omitted key is not withheld (the spawn-site env spread leaks the parent's stale one). So a blanked slot meant 'this round has no identity at all' rather than 'no qualification, use the bare id': the round certified nobody and every round after it re-reviewed the full diff. Both comments claimed the opposite. Blanking must cost the qualification, never the identity. `stripAnchor` dropped a foreign ledger's `sha` and left its `model` behind — an identity certifying a range that is gone, which every reader would have to know to ignore. They are written together, withheld together by compose-review, and serialized only as a pair; they are dropped as one now. SKILL.md's recovery path is reached from a cache-path WITHHOLD too, not only from an absent or refused anchor: a cache holding another model's anchor stops the round at the cache, and the marker it never looks at may hold one this model certified. Five new tests, each mutation-checked: the blank-slot fallback, the pair-drop, buildMarkdown's identity wiring, the per-session identity registry (write and mid-session re-key), and `certifierMatchesRound`'s engage case — every other case there is a refusal, so `return false` survived them all. * fix(review): repair the build, the retry class, and two import-graph edges R1-1 broke `npm run build --workspace=packages/cli` outright: `mergeBaseSha` is `string | null` and reached `treeEntryUnchanged` un-narrowed (TS2345), because the guard above tested only `fullBytes` / `fullText` and the compiler cannot see that a non-null capture implies a base. Naming the null base in the rejecting conjunct narrows it — and it is the same conjunct R1-2 needs, so the two fixes are one edit. I missed this locally twice, and the reason is worth recording: in a fresh worktree `tsc` bails with TS6305 before checking anything, so the `grep commands/review` I judged by came back empty and read as clean. Building core in the worktree first reproduces it immediately. R1-2: a base-fetch failure was demoted `containment-unverified`, which this skill's own taxonomy files under "deterministic for the same sha and must NOT be retried" — so a CI checkout with a flappy base fetch would pay a full review every round from then on, under a reason that also misnames the cause (the delta read fine). The three causes are now split by what a re-run would repeat: `base-untrusted` for a failed fetch, `capture-failed` for a base that existed and would not read, `containment-unverified` only for a successful merge-base that found no common ancestor. SKILL.md's reason list says so too, and the test that conflated the first and third is split in two. R1-4: `candidatesFor` tried every extension remap BEFORE the literal specifier, and `resolveSpecifier` takes the first membership hit — so in a mixed JS/TS directory where both siblings changed, `./util.js` resolved to `util.ts`. That is not one extra widened file, the cost this module budgets for a wrong edge; it DISPLACES the true one, so the seam brief names a pairing that does not exist while caller × util.js is named nowhere and retires unreviewed under a `scope.interaction` entry claiming the caller was covered. Every existing test used a single-element membership, so none could tell precedence apart. R1-3: the package-subpath escape check was `startsWith('..')` — the exact misclassification `repoJoin`'s comment eight lines above names and avoids segment-exactly. `@q/core/..config/mod.js` is a legal directory, and reading it as an escape drops the edge silently. Four new tests, each mutation-checked. * fix(review): rule the anchor verdict on the sha the side file actually holds R11-3: the section's RULED-FOR-YOU verdict was rendered from the ledger this run RECOVERED, while the sha Step 1 passes comes from the side file — and `persistRecoveredLedger`'s never-lower-round guard deliberately keeps a HIGHER-round file when the recovery walk comes back short (a concurrent lane, a paginated fetch that returned less than it should, a latest review deleted or edited). In that state a HOLDS about the recovered sha is obeyed against a different one, certified by whichever model ran THAT round — so the round scopes past a range only that model reviewed, permanently, since its own clean verdict re-anchors past it. Compose's drift gate cannot catch it: the re-run re-stamps under the running model, so the stamp agrees with the runtime and nothing looks wrong. The verdict now rules on what the file HOLDS, read back off disk after the persist decision rather than inferred from it — the guard's outcome is exactly the thing a caller would get wrong by reasoning about it. A divergence is a no-verdict state: both shas are named and the round reviews the full range, because nothing available here can say who reviewed the span between them. The findings still carry. Two new tests, both mutation-checked: the renderer's divergence refusal (and that agreement, and a file holding no anchor, still rule normally), and `persistedAnchorSha` reading back what the guard actually kept — the second is what fails when the read-back is stubbed out, which the renderer test alone could not see. * fix(review): move the last identity comparison out of prompt text R12-1 and R12-2 are the sixth and seventh findings in one class — two boundaries meaning different strings by the round's identity — so these close the class rather than the two instances. R12-1: the cache-path gate compared BARE ids on both sides. Step 8 writes `lastModelId: "{{model}}"` and the gate compared it to `{{model}}`, both the bare `config.getModel()`, so two provider configurations exposing one model name passed each other's gate — the exact case the recovery path in this PR rejects. Self-consistent is not sound; it was consistently wrong across providers, and I deferred it last round as an asymmetry when it was a hole. The gate moves into `fetch-pr`, beside the one the anchor already goes through: `--since-model` carries WHO certified the anchor, the skill copies both fields verbatim, and `certifierMatchesRound` — the same function the marker-recovery ruling uses — decides. A mismatch reports `cross-model-anchor` and reviews the full range, refused before the history is consulted at all. That leaves ZERO identity comparisons in prompt text. Six rounds have each closed one channel and the next round found another; the reason the class kept regenerating is that a comparison written in prompt text cannot share the CLI's notion of the string, and `{{model}}` is structurally the wrong one — it interpolates the bare id where everything the CLI records is provider-qualified. The SKILL guard now asserts the absence, not just the presence: no `lastModelId equals`, no `model matches`/`model differs`. R12-2: the drift gate disengaged whenever the post-time runtime channel was blank, even with the plan's stamp proving the round STARTED under a published identity — so `certifying` fell back to the model-written `input.modelId`, the channel these docstrings retire. The recovery side already rules an empty running identity a mismatch; the certifying side does now too. An UNSTAMPED round still keeps its old behaviour, because it cannot prove disagreement either. Two new tests, both mutation-checked. * fix(review): keep the merge-base probe's exit status, not just its answer R2-1: `mergeBaseSha === null` conflated the definitive "these histories share no ancestor" (git exit 1) with a probe that could not ANSWER — exit 128, or a kill, which is the 120s timeout a large long-lived PR under CI load reaches. The probe was wired through `gitOpt`, which discards the status, and `lib/git.ts`'s own `gitProbe` doc condemns exactly that collapse. The consequence is the retry class again: the round reported `containment-unverified`, which the taxonomy files under "deterministic for the same sha and must NOT be retried", so a transient merge-base failure cost the PR its incremental scope permanently and named a cause that had not happened. `GitProbe.mergeBase` now returns `{sha, status}`, `resolveMergeBase` reports `probeUnavailable`, and the reason keys on it. The flag is STICKY across candidates: the tracking ref can fail to probe while the local fallback answers a definitive no-ancestor, and a round that heard one unanswerable probe has not established determinism. `probeUnavailable` is required rather than optional on the result type, so a future producer cannot omit it and have the absence read as benign — the shape of the last three findings in this class. Three new tests, all mutation-checked: dropping the status split, and dropping it from the reason, each turn the suite red. * refactor(review): retire what slicing made dead, and pin what the caps cut Suggestions from round 1, all mutation-verified by the reviewer and re-verified here. Two are defects the slicing change introduced. R1-8: `diffBase` still carried the ANCHOR while the published bytes had become sections of `merge-base..head`. Agent 7 welds it into `--base` and recomputes its own diff, so the probe would run over hunks the round never reviewed and miss the ones it did — the exact error the field was added to prevent, arrived at from the other side. The producer stops writing it on a sliced round; the consumer's fallback to `mergeBaseSha` is the correct answer there, and it still honours the field on a plan an older CLI wrote, where a delta-range publish made it true. The seam-crossing test now asserts the published range instead of the anchor. R1-7: `fullDiffPath` was cwd-relative while every agent reads through `read_file`, which rejects relative paths, from inside `worktreePath` where `.qwen/tmp/…` resolves to nothing. Absolute now, and the docstring says NOTHING READS IT rather than naming consumers — the same over-claim #9191's R10-1 caught in the sibling field. R1-5: `containmentRuling` and its ~200 lines of helpers had no production caller left — containment is structural once the published diff is a slice of the PR's own — while a comment still claimed it "runs on every incremental capture" and `hunks-outside-pr-diff` sat in the reason union and the SKILL enumeration with no emitter. All gone, including the integration file that existed only to exercise it. R1-6: `fileLineCount`'s `repoRoot?` was a dead switch no caller set, documented for `rescope`, which no longer exists. R1-10: the capped-lists doc block sat above `chunkScopeBullets`, the function that is explicitly UNCAPPED, so hover read the cap rationale as documentation of its own contradiction. Moved to `scopeFileLists`. R1-11 through R1-15 are test gaps, each named with the mutant that survived. The caps are now pinned by what they CUT (the `(+N more)` arithmetic is independent of the `.slice()`, so both markers stayed correct with the truncation deleted); the malformed-block fixtures reach the field validators, and one carries a bad anchor with VALID lists — the only shape the anchor guard alone can reject; the restoration probe is steered per REF, which pins both "entries differ ⇒ not restored" and the mode half (a `chmod +x` with unchanged bytes is not a restoration); the plan⇔slice pairing is asserted where the slice is genuinely smaller; and the uncapped chunk-scoped path has a fixture that reaches the cap. Every one of those mutants was re-run here and turns the suite red. * fix(review): reconcile the incremental docs with slicing, pin the killed probe * fix(review): keep the scope ruling honest — probe status, rename lineage, two-flag re-run The restoration probe kept its exit status (an unanswerable ls-tree is retryable infrastructure, not a deterministic lineage refusal), a restored rename target carries its deleted source into the lineage check, a lossy capture fails the scope ruling closed, the side-file re-run passes both --since flags, a resolved base sheds the probe taint, and the brief renderer agrees with the roster on what a corrupt delta list means. * fix(review): brief the seams that have no other surface first R4-1: an interaction file that carries a section of the PR's diff is named twice — in `scope.interaction` and, uncapped, in the chunk brief of whichever chunk holds that section. One that carries NONE is named once. Those are the restored files the second pass pulls in: their own content is base content, so no chunk holds them, and the capped whole-diff list is the only place their seam is briefed at all. Insertion order appended them LAST, so on any round past `SCOPE_LIST_CAP` they were the first elided into `(+N more)` — the seam went unbriefed while `scope.interaction` still recorded it as covered. Coverage claimed and not delivered, which is the failure direction this module's header says it does not have. The cap now bites the redundantly-named entries first. It still bites: a round with more sectionless entries than the cap elides some, and that is the honest degradation rather than the silent one. The module is pure but for two injected readers — the property its docstring claims to make the whole decision testable without a repository — and nothing exercised it directly until now; every existing case reached it through `fetch-pr`. This adds that file, with the ordering as its first property. Mutation-checked: restoring insertion order turns it red. * fix(review): measure the decode, and ask the FULL range about a rename R5-2: the lossy-decode guard scanned the decoded TEXT for U+FFFD, which cannot tell a substitution from the code point itself. The code point is ordinary content — this repository carries four literal ones in source — so a delta touching any of them, even as context, demoted the round to `containment-unverified`. That reason sits in the recovery contract's "deterministic for the same sha and must NOT be retried" class, so the affected PR paid a full review every round from then on, under a cause that had not happened. Both documented causes of that reason are false for this arm. Measured on the DECODE now: re-encode and compare byte lengths. A substitution replaces an invalid sequence with three bytes and changes the length; a buffer that legitimately holds U+FFFD round-trips unchanged. Only invalid bytes can collide two names onto one, which is the hazard this guards — the character never could. R6-1: the rename-source ride-along fired only when the TARGET was restored, and that is the wrong question. Rename detection is a similarity threshold and the two ranges compare different pairs of blobs, so the delta can pair a rename the full range renders as a plain deletion beside a plain addition. With a live target and that straddle, nothing rode along, the lineage check passed on the new name alone, and the source's deletion hunks — content no round had seen — dropped out of the slice with the anchor advancing past them. The rule is the direct one: does the FULL range carry a section under the source's name? If it does, that section is unreviewed content the slice would drop, so the source rides and the lineage check keeps it. If it does not, both ranges paired the rename, the net hunks already sit under the new-side section, and riding the source would demand a section that does not exist and refuse the round. Three new tests, each mutation-checked — including an END-TO-END U+FFFD case, because the unit test of the helper alone left the arm that calls it free to revert. * fix(review): ask the decoder whether a capture is valid UTF-8 The byte-length round-trip missed every LENGTH-PRESERVING substitution, which is the shape a truncated capture actually produces: Node emits one U+FFFD per maximal ill-formed subpart, and a 3-byte subpart substitutes to a 3-byte replacement character. `F0 9F 98` — a cut-off 4-byte sequence — decoded to one U+FFFD of exactly the length it replaced, so the guard passed it as clean and scope membership was then decided on collided path strings: a live delta file conflated with a cleared sibling, its sections dropped from the slice, the anchor advancing past hunks no round had read. Asked of the decoder now — `TextDecoder('utf-8', {fatal: true})` — which is the only thing that knows. A literal U+FFFD in ordinary content still decodes cleanly, which is the distinction the guard exists to draw. Four of the reviewer's byte sequences are pinned directly; the byte-length heuristic calls every one of them clean. * fix(review): ride the section the full range paired a deletion under The ride-along asked whether the full range carries a section under the rename SOURCE's name. The two ranges can also pair the same deletion with DIFFERENT targets: base has `a.ts = A`; the anchor round rewrites `a.ts` to `A'` and adds `r.ts ~ A`; the fix round deletes `a.ts` and adds `q.ts` as an exact copy of `A'`. `anchor..head` pairs `a.ts->q.ts` (100% similarity, zero hunks); `merge-base..head` pairs `a.ts->r.ts` and renders `q.ts` as a plain addition. Nothing names `a.ts` in the full range, so nothing rode along, the lineage check passed on `q.ts` alone, and the published slice retired the source's net hunks — which sit under the section labelled `r.ts` — at the next re-anchor. Content no round had seen, gone from every later delta by construction. The rule now asks where the full range put the deletion. A section under the source's name rides as before. Otherwise, when the full range paired the source with a different target, that carrier section rides instead; a rename target of the full range is absent at the base by construction, so the restoration probe cannot misread it as restored and drop it. Otherwise both ranges paired the delta's own rename, the net hunks already sit under the new-side section, and riding anything would refuse the round for nothing — the pinned control for that shape still scopes by the new name alone. One battery test built from the two-range rendering: delta pairs, full does not. It fails at the parent commit (the slice publishes `q.ts` alone) and passes here. * fix(review): widen the restored-file hop in both directions R9-1. A file the fix round reverted plays both parts, and only one was wired. As a change it pulls its importers in; as an importer, its own base-era calls now face whatever the PR still moves — and that second direction is the one a revert makes load-bearing. Round 1 changes `i.ts` (`foo(x)` → `foo(x, y)`) together with its caller `r.ts` and clears both at the anchor. The fix round reverts only `r.ts`. The delta is `{r.ts}`, restored, so `deltaLive` is EMPTY — and the callee it strands was changed BEFORE the anchor and is unchanged since, so it is not in `deltaFiles` at all. Two layers then stopped the round: the second pass resolved `r.ts`'s import against that empty membership and found no edge, and even with the edge `scoped` took only the importer side, so the section that actually moves was never kept and `kept.length === 0` ruled `nothing-new` anyway. `upToDate` does not advance the anchor, so every re-run rules the same and `r.ts@base × i.ts@head` — the base-era call against the new contract — retires reviewed by no round. The membership is now every file the PR still changes, and the edges' targets are scoped with their importers. Restored×restored pairs stay excluded for free: a restored file carries no section, so it is never a candidate. `contextFileCount` follows the same move — "considered and not scoped in" is no longer "not an interaction key", now that a seam can scope a candidate as a target. One regression test in the pure module; each of the three edits is killed by it independently. It also trips one existing fixture, which declared `a.ts` restored while serving a full range that carried a hunk for it — a state git cannot produce, since a file identical at both ends of the PR has no section there. That fixture now serves the honest range, which makes its `not.toContain('a/a.ts')` structural rather than load-bearing; the comment says so. * fix(review): gate the widening's worktree reads on lstat before opening The readWorktree closure fetch-pr hands to widenScope is fed paths the PR's own file list determines — candidate files and every ancestor package.json discoverWorkspacePackages walks up. readFileSync follows symlinks and opens whatever sits there: a planted fifo blocks the synchronous read forever and a device like /dev/zero grows the buffer until SIGKILL. Neither death mode throws, so the catch that releases the worktree lease never runs and every later review of that PR refuses or re-hangs identically. lstat first and treat anything that is not a regular file as unreadable — null already means that to the widening, so an irregular path contributes no edge and the round keeps the unwidened floor. Same gate the pipeline already applies to this hazard class in script-lint's firstLineOf and run-ledger's ledgerOccupant. Witnessed by probe before the fix: the verbatim closure against a modeled worktree whose src/package.json is a fifo blocked the full timeout budget (exit 124); gated, it returns in milliseconds. Regression test pins the gate through runFetchPr — served content carrying a real edge is never read when lstat says the path is not a regular file — and removing the gate fails it. * fix(review): contain the widening's reads, and stop skipping invariants Two Criticals on the widening, one of them a hole in the lstat gate that answered the last one. **The gate defended only the FINAL path component.** `widenScope` hands the reader paths derived from the diff, and an INTERMEDIATE component can be a symlink the PR itself planted — ordinary git content that a standard checkout materializes, needing no platform cooperation. The path stays lexically inside the worktree while the kernel resolves it outside. That is an arbitrary-file read AND a channel out, because what the reader returns is content-derived and reaches `scope.interaction` in the published report. Containment is now by filesystem reality: `realpathSync(abs)` must sit under `realpathSync(resolve(root))`. Same class and same defence as `script-lint`'s `firstLineOf`, whose comment already names it — "SYMLINKED ANCESTOR … lstatSync only spares the final component". The reader moves out of the `fetch-pr` closure into `lib/worktree-reader.ts` so it can be tested against a real filesystem, where the kernel does the resolving; a mocked `fs` would have passed against a fiction, which is how the lexically-inside form got through the first gate. Six cases there, and `fetch-pr` keeps one mocked test for the WIRING — that it reaches the worktree through this reader at all. **A heavy interaction file keeps its invariant agents.** The skip rested on the premise that an interaction file's full-range slice is code the previous round already cleared, which holds only while the merge base holds still. Nothing enforces that: the anchor gate validates `--since` against head history, and neither the round cache nor the posted ledger carries a base identity, so a BACKWARD base move — the author retargets the PR to an older base, an ordinary GitHub operation — is accepted. `newBase..anchor` then carries hunks no round has read, they arrive inside a heavy interaction file's full-range slice, and these three agents are the only ones that would walk them; the chunk agent for the same file is briefed for the seam alone. So the skip is off until an anchor can prove base continuity — recording the base beside `lastCommitSha` and refusing on a change is a ledger schema change, and it belongs in its own PR. Removing it costs three agents on a rare shape (heavy, unchanged since the anchor, importing something that moved) and gives back the direction this design refuses to lose in. `incrementalInteractionPaths` and the `incremental` field on `RosterPlan` go with it. Both fixes are mutation-checked: dropping the realpath containment turns the reader's two escape cases red with the canary content, and turns the `fetch-pr` wiring test red; restoring the interaction skip turns the roster test red. * test(review): unit-test the seam narrowToDelta now composes Round feedback, one of three. `narrowToDelta` is a thin wrapper over `selectNarrowing` + `assembleSections` since the reshape, and the two halves are the surface the widening uses — it runs between them and asks `assembleSections` for a set LARGER than `selection.touched`. Every scenario in this file drives the wrapper, so a change correct for `touched` and wrong for any wider set was invisible here. Two cases, against captures real git produced: the selection reports only the touched paths while carrying every section the full capture does (the state the widening needs in order to consider anything), and the emit answers for whatever subset it is handed — reproducing the wrapper's own bytes for `touched`, adding the other section whole for a wider set, and answering null for a set the capture carries nothing for. Mutation-checked with a mutant shaped like the gap: gating the emit on `selection.touched` as well as `paths` leaves the wrapper's behaviour exactly right, and turns exactly one test — the new one — red. Also rewords a comment in `agent-prompt.test.ts` that read as a standing admission ("deleting the anchor guard left the suite green") when it describes the state BEFORE the case beneath it was added. Deleting `typeof raw.anchor !== 'string'` today is a one-test failure; the comment now says so. --------- 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> |
||
|
|
575e62ee46
|
fix(autofix): bind the sandbox image to its pulled digest (#9527)
* fix(autofix): bind the sandbox image to its pulled digest The sandbox image was exported as a mutable tag. `docker run <tag>` resolves against the local store without re-pulling, so a co-resident process with daemon access can `docker tag` different content under the same name between the resolve step and the consumer. Export the `<repo>@sha256:...` RepoDigests entry that matches both the pulled repository and the digest the pull itself reported: RepoDigests is shared by every tag of the same content, so index 0 can move off the pulled repo under a same-content retag, and retagged foreign content keeps its own repo — only the pair binds the export to what the pull fetched. Pin the daemon endpoint for both spawns. The docker CLI resolves its endpoint from DOCKER_HOST, then --context, then DOCKER_CONTEXT, then `currentContext` in the pool-shared config.json; clearing DOCKER_CONTEXT falls through to that last one, so the context is named explicitly and DOCKER_HOST is dropped from the child environment. An inspect answered by someone else's daemon hands back any digest it likes. Write the step files through a non-blocking, type-checked append. $GITHUB_ENV and $GITHUB_OUTPUT live under the runner-writable temp tree, where a planted FIFO turns a plain append into a block until the step timeout. Extracted from #9214, which is frozen; these were R11-1 and R11-2 there. The inspect timeout is now injectable so the tests can pin it, and the suite covers the endpoint pin on both spawns, the FIFO and directory refusals, cross-chunk stdout accumulation, and the timeout itself. Each new test was checked against a mutant of the code it pins. Refs #9089, #9524. * fix(autofix): bind gate image inputs to the resolver step output (#9527) * fix(autofix): revert repo-hygiene binding outside PR footprint (#9527) The deterministic gate rejected the previous commit because repo-hygiene.yml is CI machinery this PR never touched; review feedback alone cannot authorize changes there. Restore the file byte-for-byte and scope the workflow contract test to the two autofix workflows this PR binds. The repo-hygiene binding is real and is deferred to the review-findings follow-up queue for a maintainer-owned change. * fix(autofix): harden sandbox image consumers per review round (#9527) - R1-2: extract the duplicated spawn guard (endpoint pin, settle-once finish, SIGKILL timer, stdout capture, error/close wiring) into one spawnDockerCapture helper; pullImage and repoDigestOf share it. - R2-1: contract test fails when a workflow detects zero sandbox consumers instead of passing vacuously. - R2-2: success-path e2e test for the digest-bound export; verified it kills the exportImage(image) mutant. - R2-3: pin the daemon endpoint (DOCKER_HOST: '', DOCKER_CONTEXT: default) on every sandbox-consuming step, closing the $GITHUB_ENV and pool-shared currentContext channels past the resolver; contract test enforces the pin. - R2-4: gate the repair step on the resolver outcome so a failed resolver can never relaunch the agent unsandboxed. Also updates the workflow source pin in scripts/tests to the shared helper's literals (required by the R1-2 refactor). * test(autofix): pin repair outcome gate, derive contract set (#9527) - R3-1: the contract test now requires every always()-gated consumer to also gate on the resolver step outcome, pinning the R2-4 fail-closed clause; verified that deleting the guard from the repair step now fails the suite (the mutant shipped green before). - R3-2: route both main() e2e tests through withDockerStub; the refusal test's untouched-file asserts move before the temp-dir cleanup — they previously ran after rmSync, so they passed no matter what the resolver wrote. - R3-3: derive the contract test's protected workflow set from the tree instead of a hand-enumerated list, so a new resolver step cannot land untested; repo-hygiene.yml stays in an explicit, staleness-checked exception set until its deferred binding lands. * fix(autofix): pin resolver binary, make image check digest-aware (#9527) * test(autofix): share resolver e2e scaffold, tripwire stale exemptions (#9527) --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |