Commit graph

1464 commits

Author SHA1 Message Date
samuelhsin
b6901ee0fd
feat(web-shell): refresh composer skills incrementally after toggles (#9131)
* feat(daemon): attach skill-toggle mutation metadata to settings_changed

Hosts can apply Skill toggles incrementally without a full task reload or suppressing skills.* events.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(review): fit skill-toggle mutation metadata in the SDK bundle budget

The new normalizer parser pushed the browser daemon bundle over the 186KB cap. Raise it to 187KB and pin the review gaps that were cheap to close.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(daemon): pin skill-toggle mutation event count and parser edges

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web-shell): refresh skills incrementally after toggles

* fix(sdk): raise daemon browser bundle budget for skill-toggle metadata

The 190KB cap overflowed by 491 bytes after merging main, so the SDK build fails before tests run.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): retry cancelled skill-toggle refresh per session

Marking the mutation handled before the workspace reload settled dropped the fallback when the user switched sessions mid-flight.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): keep skill fallback until session snapshot reflects toggle

Reference-identity snapshot checks dropped the workspace fallback on unrelated command updates, so a disabled Skill stayed in composer autocomplete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): scope skill-toggle fallback to workspace and pending toggles

A partial toggle from another workspace, a superseded later mutation, or an unknown session skill list could leave a stale Skill in composer autocomplete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): pass initial value to skill-mutation origin ref

React 19's useRef types require an argument; the missing initializer broke the web-shell build.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): restore skill fallback after workspace round-trip

Dropping the fallback on a workspace switch left the handled mark in place, so returning to the origin session never reinstalled it. An unknown session skill list now also uses the ready workspace snapshot instead of an empty composer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): keep skill mutations per workspace after toggles

An intervening toggle in another workspace overwrote the only mutation
slot, so a failed live refresh could not restore that workspace's
composer snapshot. Also read nested _meta.availableSkills on live
command updates so the session skill list is not wiped to empty.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): retain batched skill mutations instead of the latest only

Two distinct toggles in one replay or live commit used to keep only the
last mutation, so a later applied toggle could drop an earlier partial
fallback and leave a disabled skill in composer autocomplete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): keep Skill composer source correct after toggles

Skip the applied fast path while session skills are unknown, and drop leaked pending toggles when the workspace fallback is cleared.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): keep skill-toggle refresh scoped to #9123

Drop multi-workspace mutation books, pending-toggle merging, and the nested skills mapper so Web Shell only consumes skill_toggle metadata and refreshes the composer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): drop unused skill-refresh eslint disable

The scoped effect already lists its deps, so the leftover exhaustive-deps suppression failed lint:ci.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): revalidate partial skill mutations

* fix(web-shell): reconcile queued skill mutations

* fix(web-shell): reconcile zero-session skill refreshes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: YungSen Hsin <yungsenhsin@U-G0HXNQM1-2052.local>
2026-08-23 04:13:51 +00:00
Shaojin Wen
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>
2026-08-23 01:10:32 +00:00
qqqys
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>
2026-08-23 00:33:51 +00:00
Shaojin Wen
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>
2026-08-22 17:45:27 +00:00
jinye
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>
2026-08-22 16:33:36 +00:00
Yan Shen
867ded5bd5
fix(sdk): support "auto" permission mode (#9003)
* fix(sdk-python): support "auto" permission mode

* refactor(sdk-python): derive permission-mode validation from PermissionMode type

* fix(sdk-java): support "auto" permission mode

* refactor(sdk-python): derive auth-type and effort validation from type aliases

* chore: rerun CI
2026-08-22 14:25:40 +00:00
jinye
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>
2026-08-22 14:19:18 +00:00
jinye
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>
2026-08-22 14:01:59 +00:00
Shaojin Wen
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>
2026-08-22 13:27:51 +00:00
Shaojin Wen
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>
2026-08-22 12:55:25 +00:00
ytahdn
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>
2026-08-22 06:12:54 +00:00
易良
7703d1c310
docs: classify architecture invariants by enforcement mechanism (#9152) (#9689)
* docs: classify architecture invariants by enforcement mechanism (#9152)

Record the policy decision asked by #9152: which architectural invariants
are enforced mechanically, which are left to review, and which are not
worth enforcing. Covers every invariant asserted in AGENTS.md and every
open architecture issue (#8084, #9145, #9146, #9151, #4063).

The drift-guard decision: do not extract a reusable framework from
check-voice-guard-sync.js. The two new drift guards
(cross-package-contracts.test.js and approval-mode-drift.test.ts) share
a pattern but not enough structure to justify abstraction. The pattern
is documented for copy-when-needed.

* docs: correct guard references and complete the invariant inventory (#9152)

* docs: fix #9145 attribution and drift-guard line counts (#9152)

* docs: classify Web Shell UI conventions and reclassify Node engines (#9152)

* docs: cover the full Web Shell convention set without a count claim (#9152)
2026-08-22 02:47:53 +00:00
Shaojin Wen
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>
2026-08-22 01:48:51 +00:00
Shaojin Wen
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>
2026-08-22 01:30:31 +00:00
易良
7a4566cb3b
fix(core): clarify Git requirement for public extensions (#9680)
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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(core): clarify Git requirement for public extensions

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

* test(core): preserve secure Git version boundary

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-21 17:05:02 +00:00
Shaojin Wen
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>
2026-08-21 14:15:27 +00:00
Shaojin Wen
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>
2026-08-21 11:06:59 +00:00
Shaojin Wen
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>
2026-08-21 08:17:23 +00:00
zjgzx1988
07405e669c
fix(artifacts): expand recorded directories into per-file artifacts (#9395)
* fix(artifacts): expand recorded directories into per-file artifacts

A generated folder was stored as one file artifact, so Open and Download hit file APIs on a directory and returned 400. Record each file instead, and treat office documents as downloadable artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): address review on directory expansion

Report depth truncation, keep later files on their own turn, and stop one bad filename or a full-content hash from breaking directory ingest.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): tighten directory expansion after review

Use the locator's realpath workspace root, map unlistable directories to validation errors, skip junk roots, and keep walk caps/warnings aligned with what was actually recorded.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): drop unused recorded counter

Directory expansion warnings are always surfaced, so the counter tripped no-unused-vars in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): close remaining directory-expansion review gaps

Reject non-object metadata and overlong titles before fan-out, wrap symlink-target lstat as validation, skip junk-only over-deep peeks, and stop offering Download for missing files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): follow up on the latest directory-expansion suggestions

Trim derived child titles, hash expanded files like direct records, only flag over-deep trees that actually contain recordable files, and show missing status on download-only cards.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): reject worktree-root expansion and close remaining review gaps

Directory expansion treated a worktree cwd as a normal subdirectory, silently dropped unrecordable names, and could overflow the metadata budget when stamping expandedFromDirectory. Align the walker, tool, and store so those cases fail or disclose cleanly, and keep the pathless download test covering the collapsed 3-card cap.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): close junk-path, coalesce, and depth-disclosure review gaps

Reject directory expansion when any path component is skip-listed, including symlink aliases into those trees. Treat unreadable over-depth directories as depth-limited, reject trim-unstable child names, and strip expandedFromDirectory when a curated same-batch record wins coalescing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): use bracket access for expandedFromDirectory metadata

acp-bridge tsc fails under noPropertyAccessFromIndexSignature when the test reads the marker via dot access.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): keep worktree directory expansion and close round-7 gaps

Strip the bound-root worktree prefix before the skip-directory gate so ordinary subdirectories still expand in worktree sessions. Realpath every directory walk root to catch intermediate symlink aliases into skipped trees, validate parent title/description before fan-out, and make the new fixtures Windows-safe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): refresh desktop bun.lock for Live Host frozen install

Qwen Live Host CI runs `bun install --frozen-lockfile` with Bun 1.3.9; the checked-in lockfile drifted and fails on main and any PR that touches Live Host path filters. Regenerate with the CI Bun version so install stays reproducible.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 07:46:18 +00:00
Shaojin Wen
94a8c40364
docs(autofix): pin the publish-side checks to the patch text (#9652)
Review feedback on #9525 arrived after that PR was merged, so the notes it
raised never made it into the doc. Both are things an implementer could
get wrong from the text as it stands.

Say what makes the re-derived checks sound in their new position: they are
computed from the patch text on the trusted side and may never re-read a
value the execute job wrote — not a line count it reported, not a
footprint list it declared, not a base SHA it echoed back. This is the
check class that has repeatedly failed to be forgery-proof here, and an
implementation that shortcuts to reading the count the gate already
computed reintroduces exactly that.

Record the sequencing constraint as a constraint: the issue-autofix
follow-up has to land before the deletion pass removes machinery it still
relies on, which is why that pass is per cluster.

Cross-reference #9556, which asks the same question on the review side.

Refs #9089, #9525.
2026-08-21 07:39:16 +00:00
Dragon
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
2026-08-21 07:24:49 +00:00
Dragon
459c04adaa
docs: document Session Workflow setting (#8554) 2026-08-21 07:24:28 +00:00
Dragon
fad5ae0bab
docs: document inline terminal image previews (#8656)
* docs: document inline terminal images

* docs: clarify inline image behavior

* docs: scope terminal image limits

* docs: clarify IM image delivery support

* docs: separate terminal image guidance
2026-08-21 07:24:20 +00:00
Dragon
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.
2026-08-21 07:24:05 +00:00
Shaojin Wen
7142a4fd93
docs(autofix): design runner-level isolation for PAT-bearing steps (#9525)
PR #9214 tried to close issue #9089 from inside the job by running the
verification gate in an ephemeral container. Eleven review rounds took its
diff from 329 to 2,700 lines while the per-round Critical count rose
8 -> 9 -> 15 -> 19 -> 29; rounds 10 and 11 changed no code and round 11
still produced 14 new Criticals on byte-identical files, six of them
reporting that a site the diff added lacked the discipline the same diff
installed elsewhere. That PR is frozen; its findings are inventoried in
issue #9524.

Two of its results shape this design. The container cannot help, because
the gate's root of trust sits outside it: the digest chain is rooted in
step outputs that the runner parses last-wins at step end, from a file
under attacker-writable RUNNER_TEMP, with no sweep before the staging
step. And the wrapper could never have run post-merge at all, which the
pre-merge skip path hid from CI.

The design moves the boundary from inside a step to between jobs: an
execute job that runs branch code and holds no credentials, and a publish
job that runs no branch code, re-derives its own decision from the patch
instead of trusting the gate verdict, and holds the PAT. Agent process
trees are killed by cgroup lineage rather than by an env marker, and the
in-step enumeration machinery the split makes redundant is deleted rather
than kept.

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-21 06:49:05 +00:00
Shaojin Wen
2c64ebe980
feat(autofix): audit the approach instead of stopping on growth-budget breach (#9262)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(autofix): audit the approach instead of stopping on growth-budget breach

A growth-budget breach no longer escalates to a maintainer handoff that
stops the takeover. The breach now makes the round a growth-audit round:
the agent audits the PR's approach on two axes — KISS (name a simpler
alternative or prove each piece load-bearing) and minimal change (every
hunk traces to the problem, an accepted finding, or a failing check) —
and records a machine-readable verdict that the verification gate
requires. sound re-arms the counting window at the current size and the
loop keeps solving; drift simplifies first, then continues; conflict is
the only growth path to a human, parked idempotently until a trusted
human responds.

The old divergence ladder (over budget for N rounds and not shrinking →
stop) terminated takeovers whose remaining work could still fit: the
growth it punished was protocol-mandated pinned tests (#9213 stalled at
round 5 with two small Criticals left). A size signal now triggers a
judgment, never a stop.

Design: docs/design/autofix-growth-audit.md

* fix(autofix): update the artifact-list pin for the growth-audit.json upload entry

* fix(autofix): surface conflict verdicts past the failure.md exits and strip verdict forgery channels (#9262)

* fix(autofix): harden the growth-audit verdict pipeline and park wake set (#9262)

* fix(autofix): close the verdict-pipeline forgeries and loop-generated wake entrances (#9262)

* fix(ci): drop the retired divergence rationale records (af-046/af-047) from qwen-autofix.md

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-21 04:54:07 +00:00
Shaojin Wen
5715782279
feat(review): post --comment reviews to Aone Code via the a1 CLI (#9491)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(review): post --comment reviews to Aone Code via the a1 CLI

The Aone chain of /review was read-only: a review of an Aone MR ran
fully but `--comment` refused. This lands the Phase 3 submit slice —
an authorised run now posts through `a1`: one comment per inline
finding, then the summary comment, and `a1 repo mr approve` on an
APPROVE. Aone has no native request-changes state, so that verdict
posts a blocking summary header and leans on the discussion merge
gate; the terminal names the difference.

Writes ride a no-retry transport (a transient retry after an accepted
write would double-post). The commit_id gate GitHub enforces
server-side lives in the provider as a pre-write head-drift refusal,
and a mid-batch failure reports exactly what landed with exit-3
do-not-re-run advice instead of a retryable throw. The
recorded-but-hostless refusal stays fail-closed, now between two
writable platforms.

* fix(review): count an accepted-but-unreadable Aone answer as posted

The triage review of #9491 flagged the gap: if `a1` ACCEPTED a comment
but answered unparseable JSON, the read-back threw before the post was
counted — the partial-post report undercounted by exactly that comment,
and if it was the first, the do-not-re-run advisory did not fire at
all, so a retry would double-post it.

Split the read-back semantics: an exec failure still propagates (the
write genuinely failed), but a succeeded exec whose answer fails to
parse now degrades to "landed, result unreadable" — counted as posted,
only the id dropped. The fail-open empty-sourceBranch drift gate is
left as the deliberate, tested trade-off it was reviewed as; it is now
named in the Phase 3 design-doc note beside the Q4 follow-ups.

* docs(review): record the Aone write-path trade-offs in the Phase 3 note

Names the two deliberate trade-offs the triage review of #9491 flagged
for the Q4 era: the head-drift gate is fail-open on an empty
sourceBranch, and the created-comment id read-back is best-effort —
plus the tolerant read-back semantics the follow-up fix introduced.

* fix(review): close five write-safety holes in the Aone submit path

The maintainer review of #9491 found five ways the new Aone write path
could post to the WRONG platform or double-post; all five closed:

1. The target-platform-unbound refusal never read the --host flag it
   names as the remedy, so the --user-authorized re-run refused again
   forever. An explicit flag on the re-run is platform proof and now
   lifts the refusal.
2. The write gate compared hosts with raw equality, but Aone is one
   platform under two names (the CR URL records the web host, the
   skill's --host rule carries the git host). Hosts now bind through
   hostsEquivalent, like every other host comparison in remote-match.
3. Platform routing keyed on the family wildcard and the ambient
   GH_HOST, so a ghe.alibaba-inc.com GHE export selected a1, and a
   recorded Aone host outranked an explicit --host github.com (the
   opposite of the registry's documented precedence). Write routing now
   keys on the canonical Aone pair (isAoneCanonicalHost), never consults
   the ambient GH_HOST (reads never detect from it), and lets an
   explicit --host outrank the recorded binding in both directions.
4. a1 takes a comment body as one argv element; Linux caps that at
   131072 bytes while compose-review's cap counts characters, so a long
   bilingual CJK summary died with E2BIG only after every inline had
   landed. A size gate now refuses the whole batch before any write.
5. An accepted-then-died write (timeout after the POST committed, a
   reset mid-response) read back as not-landed, suppressing the
   do-not-re-run advisory and inviting the double-post a1Once exists to
   prevent. Exec failures now count as possibly-landed (ambiguous), so
   the advisory fires even when the count is zero.

* fix(review): harden the Aone submit path per the verify-lane review

The sandboxed-verification review of #9491 (8 Critical, 24 Suggestion)
caught the next layer of the write path; every finding addressed:

Platform selection:
- The unbound refusal now also fires when NO recording exists at all:
  a --user-authorized publish from another directory finds nothing, and
  the cwd probe alone must not pick the platform of an irreversible
  write. Tests that modeled the old cwd-fallback now model evidence:
  args() seeds a recording, session-scoped recordings are seeded where a
  session id is set.
- The gh write rebinds its routing host to the evidence that selected it
  (explicitHost ?? recordedHost) — a recorded GHE host no longer posts
  wherever the ambient env pointed.
- Host comparison is normalised ONCE (case/port/trailing-dot) and shared
  by hostsEquivalent and isAoneCanonicalHost, so the write gate and the
  router cannot normalise differently; a port-bearing CR-URL host no
  longer dies at the gate after the whole review ran.
- The fast-path repo axis binds case-insensitively (GitHub resolves
  owner/repo case-insensitively server-side).
- The cross-session recording scan is last-writer-wins by mtime; the
  NEWEST same-PR recording decides (its host, or unbound) instead of an
  older session's stale host masking the newest recording's hostlessness.
- recordedSeverityFloor binds through hostsEquivalent too — the floor
  recovery no longer silently discards the operator's floor on the
  web-host/git-host alias.

Reporting:
- a1Cause reads the captured stderr, not the execFileSync message: the
  message embeds the full argv — the entire multi-line comment body — so
  parsing it surfaced the operator's own review text, never a1's error.
- The REQUEST_CHANGES terminal note is conditioned on the inline
  Criticals actually posted — a body-only Critical posts no discussion
  threads, so nothing mechanically blocks the merge and the note says so.
- The summary skip-guard keys on the posted summaryMessage, not the raw
  body: an empty-body REQUEST_CHANGES still posts its blocking header,
  the verdict's sole carrier on Aone; the size gate measures the same
  message.

SKILL.md: the mid-batch bullet no longer commands hand-posting the
remainder (it contradicted the write-monopoly rule); it names the
oversized-comment refusal as the third Aone-specific shape; the
relay-the-link fallback stops assembling Aone links from meta's collapsed
owner/repo (a nested-group hazard) and relays the target's coordinates.

Tests: 24 mutation-driven hardening cells (ordering via
invocationCallOrder, exact argv pins, boundary cells at 131071/131072,
RC-header accounting, stderr-over-message, summary-create failure,
accepted-then-unreadable counts, attribution-off passthrough, url-absence
arm, positive read-retry). 3861 review tests green.

* test(review): pin hostsEquivalent's alias equivalence across spelling variants

* fix(review): close the third-layer holes in the Aone submit path

The third review round of #9491 found the layer under the last one;
every finding addressed:

Platform selection:
- The cwd arm of the write gate probes the origin through the CANONICAL
  predicate itself instead of delegating to the registry's family-wildcard
  detection: a ghe.alibaba-inc.com origin no longer takes the a1 path.
- submit FORCES context-unavailable into the compose input on the Aone
  path: the cap no longer rides the model-written state, so an omitted
  contextUnavailable cannot compose an APPROVE that the a1 path turns
  into a real platform approval. The docs now say the native approve
  does not fire this phase.
- The floor recovery's host axis binds to the host the write routes at
  (explicit ?? recorded ?? gh fallback): a flagless Aone post no longer
  drops the operator's recorded severity floor.

Failure shapes:
- A mid-batch failure emits "partial": true with the landed counts and
  comment ids: posted:false alone invited a wrapper retry that
  double-posts what landed. The summary's fate is stated when it was the
  write that died.
- A deliberate pre-write refusal (drift, oversized) reads as
  aone-post-refused; an UNEXPECTED pre-write error rethrows, gh-parity —
  nothing landed, a re-run is safe, a recoverable blip no longer loses
  the authorised review.
- The batch re-reads the head once after posting and discloses a
  mid-batch amend (headMovedDuringPost) instead of claiming the pins held.

Text:
- The approve-failure WARNING and the oversized refusal name the USER as
  the manual actor — "by hand" is never an agent action, and Step 7's
  ban now says so.
- SKILL.md: restored the exact subcommand enumeration, qualified the
  cleanup tripwire as GitHub-only, taught the completion contract the
  partial/approved shapes, and documented the repeat-round caveats (no
  dedup backing yet, no self-PR detection on Aone).

Tests: cwd canonical-arm cells (GHE family origin falls to gh), the
forced-cap wiring, structured partial JSON, refusal-vs-failure reasons,
the rethrow cell, missing-sourceBranch key, mid-batch drift disclosure,
and the a1JsonOnce transient-no-retry invariant. 3870 review tests green.

* fix(review): close the residual holes the sandboxed verify report found

The follow-up-2 sandboxed verification of #9491 (155 scripted
assertions, 153 pass / 2 fail) re-measured everything at the new head
and surfaced three findings; all three closed:

- F1R: a failed write exiting with EMPTY stderr (the 120 s deadline
  kill, SIGKILL/OOM, an a1 crash before writing) made a1Cause fall back
  to parsing the exec message — and Node embeds the FULL argv in it, so
  the "cause" quoted a line of the operator's own review body. The
  fallback now reports exit facts only ("a1 failed without stderr (exit
  N) / (signal X)"), never the argv-bearing message. The dominant shape
  (real a1 error on stderr) is untouched.
- F3: the forced context-unavailable wiring on the Aone path survived
  3870 tests unpinned — submit's compose now has a cell asserting the
  compose input carries contextUnavailable: true on the Aone path and
  false on the gh path.
- F2: createdCommentId's result/data nestings were correct but unpinned
  (a key-drop mutation survived). A new cell pins ids read back from
  {result:{id}} and {data:{id}}.

* fix(review): close the round-5 platform-binding holes in the Aone write path

- Order the cross-session recorded-args scan by each recording FILE's
  mtime (writeSkillArgs rewrites in place, so the directory mtime never
  advances); fold the publishing session's own recording and the
  sessionless root recording into the same newest-wins ordering instead
  of pinning them ahead of (or behind) the sorted siblings.
- Refuse an explicit --host that contradicts the recorded host
  (target-platform-conflict): the flag fills a gap in the recorded
  evidence, it does not retarget the recorded review at another
  platform's same-named repo. The Aone web/git alias still passes
  through hostsEquivalent.
- Fail closed on a hostless recording read via the --skill-args
  override: the submission cwd's origin probe names submit's clone, not
  the review's, and must not stand in for the missing platform
  evidence; the --host remedy lifts the refusal.
- Bind the gh routing host to the cwd origin when the cwd arm selected
  the platform (and mirror the chain in the floor recovery's host
  axis), so a cwd-selected post no longer restores ambient env
  inheritance and routes past the clone that chose the platform.
- Hand the GitHub path's contextUnavailable claim through raw so
  compose-review's deliberate shape check still refuses a malformed
  non-boolean instead of silently coercing the cap away.
- Serialize the partial-post ambiguous flag in the stdout JSON and give
  the partial shape its own completion-line disposition in the skill —
  never the `not posted` form a retry-on-not-posted wrapper acts on.
2026-08-20 16:27:16 +00:00
callmeYe
4839935e55
feat: register toggle-only Qwen reasoning (#9574) 2026-08-20 11:31:35 +00:00
ytahdn
bf0dbb1ae1
feat(web-shell): support mid-turn file attachments (#9570)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-20 10:58:12 +00:00
jinye
2e6151aa15
fix(serve): Harden standalone conversation primitives (#9512)
* fix(cli): pass ConversationDirectoryIdentityError cause through native Error options

* fix(cli): re-inspect raced standalone directories and report first creation as created

* fix(core): measure JSONL head integrity against a line budget

* fix(core): Preserve plain JSONL record budgets

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-20 08:09:44 +00:00
Shaojin Wen
a8a855914b
fix(review): run verifier probes in a private scratch worktree (#9207) (#9221)
* fix(review): run verifier probes in a private scratch worktree (#9207)

Step 4's verifier is the review's one writing agent: it writes a probe, runs
it, applies the one-line fix its flip-check needs, and restores. All of that
landed in the shared review worktree — the tree `working_dir` pins every other
agent to — and the pipelined loop launches a round's verifiers alongside the
NEXT round's reverse auditors, so those writes are live exactly while the
auditors read. "Leave the tree as you found it", which the brief has always
said and verifiers do obey, cannot close that window: the exposure is *during*
the probe. Measured on a live run, a round-5 auditor read a probe's mutant plus
a leftover probe test and came within a step of filing a Critical against code
no commit contains; it recovered only by improvising `git show HEAD:`, a
fallback no brief mentions.

Three parts, because isolation alone is a guarantee one regression away from
being false:

- `qwen review scratch-tree` gives each verifier shard a throwaway sibling
  worktree at the commit under review, with the review worktree's node_modules
  linked in so a unit harness starts without an install. Every call hands back
  a pristine tree (a previous finding's mutant surviving into the next probe
  would be a wrong verdict carrying a deterministic source tag); the label is
  the shard's record key, because the shards of one round run concurrently; and
  a tree it cannot create makes the probe inconclusive rather than falling back
  to the shared worktree. This is the isolation the test-efficacy probe has had
  since #6832 and the A/B's base tree has, extended to the last step that
  writes.
- Every code-reading brief — dimension agents, chunk agents, reverse auditors
  and the verifier itself — now carries the rule that auditor had to invent:
  the worktree is shared, code that is not in the diff and not in the commit is
  not a finding, and anything surprising is judged against `git show HEAD:`.
- `agent-prompt` reads the tree once per call, and every wave of agents is
  built by it immediately before launch: residue is named in each brief it
  builds and warned about on stderr, so a contaminated tree is announced to the
  agents about to read it instead of being discovered as a phantom Critical.

`exposeDependencies` moves to `lib/worktree.ts` beside the other disposable-tree
machinery, and now farms each workspace member's own node_modules as well as the
root's. Measured on this repo, a tree with 1560 root packages linked still could
not resolve `@testing-library/react` for a UI probe, because npm could not hoist
that copy out of `packages/cli` — which also silently cost the efficacy probe the
same class of test.

`cleanup` sweeps the scratch family by prefix; the label half is the shard's key
and cannot be reconstructed by the sweeper.

* fix(review): address the scratch-tree review — two criticals and the honesty gaps

Both Criticals were real, and both had the same shape: a claim the code made
that one machine class or one leftover state falsified.

- The welded `scratch-tree` command interpolated `--worktree` unquoted, so on
  any checkout under a path with a space or an apostrophe every shard's
  isolation was silently unavailable and every probe fell back to a reading.
  Quoted with `shellQuotePath`, like every other path this file prints into a
  command.
- The reuse gate checked only that the scratch path EXISTS. A bare directory
  there — the leftover of a crashed `worktree add`, or of a cleanup whose
  `rmSync` failed — has no `.git`, so git walked up and ran
  `checkout --force --detach` against the user's own checkout: their
  uncommitted work discarded, their HEAD detached onto the PR's commit, and
  `rev-parse HEAD` then returning the sha that made the reset report success.
  Gated on the tree being a registered worktree, with the regression pinned by
  a test that fails on the un-gated code.

The rest are the same defect class at lower stakes — something stated more
broadly than it holds:

- The residue probe hand-parsed porcelain's rendered form, so a path with a
  space, a non-ASCII byte, or a literal ` -> ` came back as a name matching
  nothing on disk, `--untracked-files=normal` collapsed a whole probe directory
  to one unactionable `dir/` entry, the 1 MB `maxBuffer` default answered the
  dirtiest trees with "clean", and the cap truncated silently while both
  renderers presented the list as complete. Now `-z --untracked-files=all` with
  a 64 MB buffer, and a `{paths, total}` result both renderers disclose.
- `git checkout -- <path>` restores from the INDEX, so the advised recovery
  left staged residue in the tree; it now says `git checkout HEAD --`.
- `git show HEAD:<path>` cannot produce an untracked path — the prototypical
  residue — so the rule now says what that answer means instead of handing the
  reader a command that exits 128.
- Agent 8's `--whole-diff` block reads the same shared worktree and got neither
  the rule nor the residue paths; it is built outside `buildLaunch`, which is
  exactly how it was missed.
- The stderr warning claimed every agent had been told; the block is gated on
  `reviewsCode`, and Agent 7 — which builds and tests that tree — is not.
- A label that flattens to no path-safe character fell back to a shared
  `agent` tree, `git clean -fd` left a nested repo standing while the report
  said the tree was pristine, `dependencies: null` said "no node_modules"
  about a farm that had failed to link, `{0,0}` read as "already in place" for
  a `node_modules` holding nothing linkable, and `--out` was validated after
  the tree and its farm already existed.
- The docs stated the isolation unconditionally: local-diff and file-path
  reviews have no worktree and no scratch tree, and SKILL.md/DESIGN.md and the
  user page now say so.

Tests moved with the code (`exposeDependencies` and `worktreeCreateFailureDetail`
now live beside `lib/worktree.ts`), and the plumbing that was pinned at both
ends but not in the middle — that a verify shard's recorded brief carries ITS
record key as the scratch label — is pinned by a test that fails when the key
is dropped.

* fix(review): close the round-2 review — Windows guard, Agent 7's blind spot, unpinned wording

The Critical is a test that would only fail where this PR's CI does not look:
the new chmod-based case guards on `process.getuid`, which is undefined on
Windows, and `chmodSync` on a directory there sets a read-only attribute that
does not stop `git worktree add` from creating a subdirectory — so the
merge_group-only Windows leg would go red for every PR carrying the file.
Sixteen of the seventeen chmod-permission tests in this repo already skip
win32; this one now does too.

The rest are the same class as round 1 — a claim wider than the code:

- **Agent 7 had no protection for residue that PREDATES the round.** The
  exclusion's justification ("its own commands are the writes it sees") is only
  true for residue a round creates. A tree that starts dirty reaches Agent 7's
  compile and test run, where a `[build]`/`[test]` finding is pre-confirmed and
  skips verification — a merge-blocking phantom Critical of exactly the class
  this machinery exists to prevent. The residue paragraph now goes to EVERY
  brief (with "a defect confined to these paths is not a finding"); only the
  reader rule stays scoped to the roles that review code. The stderr warning
  says to restore before launching the wave, not before the next round.
- **Residue paths reached two sinks unflattened.** `inertPath` moved to
  `lib/paths.ts` and now covers the scratch-tree note and the orchestrator's
  stderr line as well as the briefs — the `-z` format this PR introduced is
  precisely what lets a control byte in a filename arrive intact.
- **The recovery wording could not clear two shapes the probe reports.** A path
  staged as NEW and a rename destination are in the index but not in HEAD, so
  `git checkout HEAD --` cannot match either; both renderers now name `git rm
  --cached` for those and `rm -rf` for untracked residue (including the nested-
  repo directory entry `--untracked-files=all` still cannot expand).
- **The "full set" command was the one this PR calls unusable.** Both notes now
  say `git status --porcelain --untracked-files=all`, since the default
  collapses the probe directory whose files the count came from.
- **`alreadyPresent` believed an empty farm dir.** The dir a previous call
  creates when the source holds nothing linkable is gitignored, so the reset
  spares it, and the second call flipped "no harness will start here" into
  "already in place" with nothing changed. It now requires a non-empty farm.
- Doc corrections: `resetScratchTree`'s header still said `clean -fd`, and the
  residue probe's comment claimed `--untracked-files=all` ends directory-shaped
  entries — it does not for a directory holding its own `.git`. The invalid-
  UTF-8 limit (`encoding: 'utf8'` maps a bad byte to U+FFFD, and no string form
  of that name resolves) is documented rather than papered over.

Pinned, each verified by reverting the fix and watching the test go red: the
apostrophe ESCAPE in the welded command (the fixture had no apostrophe, so a
naive `'…'` wrap passed), the `git checkout HEAD --` wording in both places,
the capped-note arithmetic, the farm-failure note, stdout-before-side-file
ordering and the exit-1 arm it also covers, and `sharedTreeResidueTotal` on the
creation-failure return.

* fix(review): close the round-3 review — hooks, hidden mutants, planted farms, suppression after restore

Four of the seven Criticals are the same discovery from four directions: a
scratch tree is a LINKED worktree and a `clean`/`checkout` reset is not the
guarantee it reads as.

- **Hooks resolve to the user's repository.** `git worktree add` and
  `checkout --force` both fire `post-checkout` from the common dir — the user's
  own `.git/hooks` — so creating or resetting a scratch tree executed whatever
  that repository holds. Every git call this command makes now runs with
  `core.hooksPath` pointed at a path holding no hooks, and the report says
  plainly that hooks, config and refs are shared rather than isolated.
- **skip-worktree hid a mutant through the reset.** `checkout --force` silently
  skips a file carrying the bit and `clean` never touches tracked files, so a
  probe that set it (directly or via `sparse-checkout`) left a mutant that
  survived with `git status` reading empty and the sha still matching. The reset
  now refuses when `ls-files -v` still shows a hidden entry, which routes the
  caller to discard-and-rebuild.
- **The farm was certified by existence.** `clean -ffd` spares ignored paths to
  keep the dependency farm — and equally spares whatever a probe installed or
  planted there. `node_modules` is now the one ignored path a reuse does not
  inherit: it is cleared and re-linked, and `exposeDependencies` marks the farms
  it builds so a directory it did not build is never certified as one.
- **A broken leftover could not be rebuilt over.** A `.git` gitfile whose admin
  entry survives makes `worktree remove` fail and the next `worktree add`
  refuse "missing but already registered"; `discardWorktree` now prunes.

The fifth is about the instruction rather than the tree: the residue paragraph
is baked into every brief at build time, so restoring the paths and then
launching the already-built wave tells every agent to drop findings in a file
that is by then exactly the PR's code. Both the stderr warning and SKILL.md now
say to rebuild the wave after restoring. The remaining two are the three new
real-git fixtures missing `isolateHostGitConfig()`, which a polluted host
gitconfig (`commit.gpgsign` with no key, a `core.hooksPath` hook) turns into
suites that fail for reasons the branch never touched.

The suggestions, in one line each: a rename now reports BOTH of its names (the
restore needs the one that is gone); a `git status` that dies is reported as
UNMEASURED rather than clean, in both renderers; `inertPath` covers `\p{Cf}`
and `\p{Zl}`/`\p{Zp}` — bidi overrides and zero-width characters passed
through the sanitizer that exists to defuse hostile filenames; residue paths
and tree paths are shell-quoted where the notes prescribe commands over them
(`rm -rf my probe.ts` deleted `probe.ts`); the verifier brief no longer says
"then move on" after applying a candidate fix, because one tree serves every
finding in a shard; `scratchLabel` strips a leading dash, which yargs read as a
flag; the member-farm loop guards per member rather than around the loop; the
unguarded `mkdirSync`/`readdirSync` in the farm now count as failures instead of
throwing out of a best-effort contract; cleanup discloses a `readdir` failure
instead of reporting "nothing to clean", and sweeps a dangling symlink
`releaseWorktree` cannot see; the briefs' restore recipe gained the staged-only
branch the scratch-tree note already had; and both "pristine" claims now say
that gitignored paths survive.

Also corrected: "the one agent whose job requires writing" — Agent 7's
efficacy probe writes too, and has had its own tree since #6832.

New tests, each verified by reverting its fix: the hooks suppression, the
skip-worktree refusal, the planted-farm replacement, the prune, staged-residue
detection, the unmeasured state, `inertPath`'s character class, and the
residue reaching every launch class rather than the one role the earlier test
inspected.

* fix(review): close the round-4 review — untrusted workspace paths, member farms, locked leftovers

The sharpest two are about treating the reviewed PR's own manifest as data
rather than as input, in code that DELETES:

- `exposeDependencies` fed workspace dirs from the root manifest of the code
  under review straight into `join()` and then into the farm's opening
  `rmSync`. A PR setting `"workspaces": [".."]` — or a committed SYMLINK at a
  workspace path, which `readWorkspacePackages` follows deliberately because
  npm does — pointed that delete at a directory outside both trees; in this
  pipeline's layout, at the reviewer's own checkout. Every member is now
  resolved through `realpathSync` and required to be contained in the tree it
  belongs to, which closes the string and the symlink vector together, and a
  member that escapes is counted as failed rather than silently skipped.
- The reuse path wiped only the ROOT farm, so `<tree>/packages/<member>/
  node_modules` survived with its marker and was certified as-is — the same
  hole round 3 closed at the root, one level down, where Node resolves a
  member's imports FIRST. `exposeDependencies` now takes `rebuild`, and the
  reuse path distrusts every farm rather than the top one.

Two more that would have wedged a review:

- `git worktree prune` never drops a LOCKED admin entry, and probe code has a
  shell inside these trees: one `touch` in the admin dir the tree's own gitfile
  names, and every later `worktree add` for that PR fatals "missing but locked"
  — permanently, since cleanup prunes too. `discardWorktree` now unlocks and
  retries with the second `--force`.
- `ls-files -v` and `clean` ran under `spawnSync`'s default 1 MiB buffer, which
  a large repo passes; Node kills the child, the reset reads that as failure,
  and every reuse rebuilds forever.

And a Windows one of the same class as round 2's: the new fixture wrote a file
named `a -> b.ts`, and `>` is reserved on NTFS — the merge-queue-only Windows
leg would have failed at fixture setup. Split, with the arrow half skipped
there (the shape it pins cannot exist on NTFS).

The rest: the residue restore recipe was WRONG for a staged rename's original
name (`git rm --cached` stages a deletion there; `git checkout HEAD --` is
what clears it) and the test that certified it never staged anything — both
fixed, and the test now builds all four real shapes; the reader rule was gated
on `reviewsCode`, which left Agent 0 and the test matrix reading worktree
source without it (the gate is now "every role that judges code", i.e. all but
Agent 7); the stderr warning did not actually carry the rebuild-after-restore
instruction SKILL.md attributes to it; `ScratchTreeReport` dropped the
`unmeasured` state, so a failed `git status` read as clean to a script; the
scoped-package branch linked non-directory entries the top-level branch skips;
`farmDependencies`' catch and its note branch were unreachable once round 3
guarded the fs calls, so they are gone rather than pretending to be a net;
cleanup's family-read failure now fails the run instead of letting it announce
"Nothing to clean", and its dangling-symlink branch no longer swallows a
throwing `rmSync`; the residue note says the names are flattened for display
and where the exact bytes are.

Disclosed rather than enforced, with the reason in the code: the farm's links
are read-write and point into the shared worktree, so a probe that writes
THROUGH one (an `npm rebuild`, a package that writes into its own directory)
lands outside its tree where the residue check cannot see it. Copying the farm
would cost the minutes it exists to save; the verifier's block now says to
replace a link with a copy before modifying a dependency.

New coverage: workspace escape and symlinked-member containment, the self-farm
guard, rebuild-on-reuse at member level, assume-unchanged beside skip-worktree,
the unmeasured state through its renderers, the dangling-symlink sweep, the
family-read failure, and the cleanup mock gaps that made two of those branches
unreachable by construction.

* fix(review): close the round-5 review — scoped prune, real pristine, submodules, symlink gadgets

The findings that hold without assuming an attacker already has a shell:

- **The prune added in round 4 was repo-wide.** `git worktree prune` drops any
  admin entry whose directory is momentarily absent — another shard's
  `worktree add` mid-flight (this pipeline runs discards and adds concurrently
  against one common dir), or the user's own worktree on a volume that happens
  to be unmounted. It now removes the one entry whose `gitdir` file names this
  path, and nothing else.
- **"Pristine" spared ignored paths, and a probe's state lives there.** Its own
  `node_modules` at any depth, a `.tsbuildinfo`, a `dist/` it built and then
  mutated — all survived a reset the report called pristine, and the farm-level
  wipe could not reach them (a member farm whose source has none was skipped
  before the target was touched; the root rebuild was skipped entirely when the
  review worktree had no `node_modules`). The reset is now `clean -ffdx` and the
  farm is re-linked, so pristine means pristine.
- **An initialized submodule was untouched by all of it**: `checkout --force`
  without `--recurse-submodules` leaves its working tree, `clean` never touches
  a tracked gitlink, and `rev-parse HEAD` is the superproject's. A tree holding
  one is now rebuilt rather than reset — a fresh `worktree add` leaves
  submodules uninitialized, so the rebuild is both correct and cheap.
- **test-efficacy reuses one probe tree across every suite run**, and the code
  running in it is the PR's own test code. Round 4 answered this with the
  positive control; the control only covers the green-forcing direction, so the
  farm is now re-linked before each run instead — about a second against a
  540-second budget.

The remaining Criticals all presuppose code execution that this pipeline grants
before any of this code runs (Agent 7 installs and tests the PR; a verifier's
probe is arbitrary code by design), so they do not change what an attacker can
do — but three of them describe gadgets that cost nothing to remove, and they
are removed:

- `cleanup` no longer hands ANY symlink at a family path to
  `git worktree remove`, which would follow it and delete whichever registered
  worktree it points at. It unlinks the link and reports that.
- `resetScratchTree` validates that the tree IS the tree — not a symlink, and
  `rev-parse --show-toplevel` resolving to itself — before running a reset that
  would otherwise land wherever the path resolves.
- `farmNodeModules` lstats its SOURCE root, so a symlink at the review
  worktree's `node_modules` cannot redirect every probe tree's dependencies.

Also: the `dependencies: null` contract now says what it means (no
`node_modules` to link from; a link failure arrives counted, not as null), the
`alreadyPresent` branch of the note is gone with the reuse path that could
reach it, and the residue note carries the flattened-names disclosure its
sibling renderer already had.

New tests: the symlinked scratch path, ignored-state clearing, the submodule
rebuild, and the unmeasured pass-through through the command's own report.

* fix(review): close the round-6 review — committed farms, foreign repos, deinit'd submodules

One finding crosses the line the round-5 reply drew, and it is the important
one: `node_modules` is gitignored by convention, not by rule, so a pull request
can force-add `node_modules/.qwen-review-farm` beside its own module stubs and
`git worktree add` checks both out. The fresh-create path then found a marker
and certified the PR's own modules as the farm this code built — reachable from
PR CONTENT, with nothing executed. Two changes: the marker records the
dependency root it was built from and is only believed when it names this one,
and both scratch-tree paths (fresh and reuse) now rebuild rather than trust
anything found at that path.

The other three that hold on their own:

- **The identity check added in round 5 was incomplete.** `rev-parse
  --show-toplevel` prints the directory the `.git` file sits in, whatever that
  file points at — so a gitfile naming another repository, or a whole repo
  planted at the predictable scratch path, passed it while every command below
  would have run against someone else's objects, refs, hooks and config. The
  tree must now share a common dir with the review worktree.
- **`git submodule deinit` walked through the round-5 submodule gate**: the
  status line goes back to `-` while the submodule's gitdir — its hooks, its
  config, its objects — survives under the common dir and is resurrected by the
  next `update --init`. The gate now reads the commit's gitlinks instead of the
  submodule's state, so a repo with submodules rebuilds rather than resets.
- **The round-5 anti-planting rebuild never reached the positive control or the
  hunk probes.** They defaulted `dependencyRoot` to the probe tree, where
  `exposeDependencies` returns before clearing anything — so the control, the
  run that decides whether ANY mutant verdict is trusted, resolved through
  whatever the baseline's test code had planted.

Smaller, from the same review: a dangling symlink at the target skipped the
rebuild wipe (`existsSync` follows links); a dangling top-level link in the
source was dropped without counting; a stray file under a scope directory was
linked as a package where the top-level branch skips one.

Disclosed rather than fixed, with the count now in the report: the farm mirrors
npm workspace SELF-links, so an import that goes through a package NAME
resolves back into the review worktree's copy and a mutation made in the
disposable tree is invisible to it. Re-pointing them would break resolution
outright for any package whose entry point is a build artifact a fresh checkout
does not have, so `DependencyFarm.selfLinked` carries the number instead of the
layout carrying it silently.

Not fixed, and this is the third round of stating it: the remaining findings
need a prior arbitrary-code-execution foothold (a background process racing the
containment check between resolve and use). The pipeline grants that capability
several steps earlier and by other code — Agent 7 runs `npm ci` and the PR's
test suite — so hardening this command changes which line appears in the trace,
not what is possible. The place to change it is sandboxing the review's command
execution, which is its own design.

Tests: the locked-leftover recovery, the scoped registration drop (a sibling
worktree whose directory is absent survives it), the foreign-repository gitfile,
the deinit-proof submodule gate, and the scope-directory stray file. The
foreign-repo fixture is a CLONE on purpose — with an unrelated repo the reset
fails for the wrong reason and the test passes without the guard it pins.

* fix(review): close the round-7 review — walk-up escapes, identity gates, farm wipe

Five Criticals, each probe-verified on real git before the fix and
mutation-checked after:

- worktreeResidue verifies the path IS a worktree before measuring: with
  the .git file gone, git discovery walks up into the user's checkout and
  answers with the user's own dirty state — fail closed instead (R7-4).
- runScratchTree applies the same --show-toplevel check to the trusted
  --worktree argument before resolving HEAD (R7-3).
- The reuse identity gate rejects a tree whose gitdir equals the common
  dir: a .git symlinked or hand-edited to name it passed every prior
  check while checkout --force detached the user's main HEAD onto the
  PR sha and rewrote the main index (R7-2).
- farmNodeModules detects the rebuild target with lstatSync: a dangling
  symlink a PR committed as node_modules read as absent, skipped the
  wipe, and died EEXIST on every rebuild (R7-1).
- The residue probe's node_modules/dist exclusion is enforced by a
  pipeline-controlled excludes file rather than borrowed from the
  commit's .gitignore (R7-5).

Plus the production-shaped UNMEASURED fixture (R7-19) and the three
remaining stale contract texts on the reuse reset (R6-7).

* fix(review): close the round-8 review — tripwire identity, GIT_DIR redirects, symlink writes

Round-8 Criticals, each probe-confirmed and pinned by a test that goes
red when the fix is reverted:

- The residue tripwire certified a CLEAN status for a repository planted
  over the contamination (`rm .git && git init && commit`): no local
  check can tell a planted repo from the tree it replaced. It now fails
  closed unless `.git` is a gitfile, and reports unmeasured for what
  `git status` cannot see inside committed gitlinks (a non-empty
  submodule directory) instead of clean.
- An inherited GIT_DIR (or GIT_WORK_TREE/GIT_INDEX_FILE/...) redirected
  every identity check at once — both sides of each comparison see the
  same override, so none could detect it. Every git call in the
  scratch-tree lifecycle and the tripwire now drops the redirect
  variables.
- The scratch-tree reuse gate accepted a planted gitfile naming a
  SIBLING worktree's admin entry and reset the sibling: the gate now
  requires the admin entry's gitdir backpointer to resolve to this tree.
- Probe writes followed PR-committed leaf symlinks (mode 120000) into
  the shared review worktree — the positive-control injection, every
  mutant and every hunk restore. Each write site refuses a symlinked
  target as inconclusive.
- discardWorktree handed a symlink at the tree path to
  `git worktree remove --force`, which resolved it and force-deleted the
  registered victim it pointed at; a symlink is unlinked instead.

Plus the record-key comments (chunk id separates --all-chunks shards)
and the orphaned marksOurFarm doc block.

* fix(review): close the round-9 review — gitlink residue, env redirects, probe escapes

Round-9 Criticals, each probe-confirmed on real git before the fix and
pinned by a test that goes red when the fix is reverted:

- worktreeResidue's gitlink blind set was parsed from RENDERED
  `ls-files -s`: default core.quotepath quotes a non-ASCII gitlink
  name into a spelling that never resolves on disk, the entry dropped
  from `blind`, and a contaminated gitlink was certified CLEAN. It now
  reads `-z` (R9-1).
- An exported GIT_DIR redirected every inheriting git call at once:
  test-efficacy's head-sha read, probe resets and revert checkout, and
  base-tree's add and reuse check. Every git spawn in both files now
  drops the redirect variables (R9-16).
- cleanup's symlink guard covered only the scratch family; the three
  named family paths still reached releaseWorktree, whose existsSync
  follows a LIVE link and whose `worktree remove --force` resolves it
  — deleting the registered victim while reporting success, and
  silently skipping a dangling link. The guard moved into the shared
  report path all four families go through (R9-18).
- exposeDependencies' rebuild wiped only the farm-owned node_modules;
  anything planted at another path — packages/node_modules resolves
  before the root farm in a packages/* workspace — survived between
  probe runs and decided their verdicts. Rebuild now wipes every
  node_modules the farm does not recreate, following no links (R9-37).
- NO_HOOKS covered hooks only, not config-driven content FILTERS: a
  planted `filter.<name>.smudge|clean` plus one attributes line — both
  writable into the common dir — executed on every reset and rebuild
  checkout. runScratchTree detects repo-local filter config and
  refuses rather than run it (R9-38).
- The harness never WRITES probe test files, so no write guard ever
  saw one committed as mode 120000 — vitest collected it through the
  link and scored mutants against code the tree never mutated. Probe
  files are checked against the committed index and dropped as
  not-run before any suite collects them (R9-40).
- The probe write guards lstat-checked the LEAF only; lstat resolves
  intermediate components, so a relinked ancestor in a reused tree
  read ordinary and every later write followed it out of the tree.
  The guard walks every component (as safeRmWithin does for deletes),
  and each restore write re-validates immediately before writing —
  a mid-run relink stops the phase instead of writing through the
  link (R9-17).

* fix(review): close the round-10 Criticals — env config injection, sweep scope, probe-set holds

Taken back from the autofix loop, and scoped to Criticals from here: the
findings that hold without assuming a foothold, plus the ones whose fix is a
few lines and removes a step from a chain.

Reachable from the environment or from PR content, with nothing executed:

- **`sanitizedGitEnv` dropped only the discovery redirects.** `GIT_CONFIG_COUNT`
  with `GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` sets any config key for the
  run — `core.fsmonitor` and the `filter.*` pair are command execution — and
  `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM`/`GIT_CONFIG_PARAMETERS` reach the same
  place by other routes. A gate on the front door with the window open.
- **`lib/git.ts` never had the gate at all**: `gitOpts()` spread `process.env`
  into every shared helper, `releaseWorktree`'s `worktree remove --force`
  included — the commands that run against the user's own repository, so they
  need it more than the disposable-tree ones that got it first.
- **The residue probe could be steered by the tree it measures**: `core.fsmonitor`
  runs a command on `status`, so the tripwire was an executor. Emptied for that
  invocation, the same discipline the checkouts' `core.hooksPath` already had.
- **`probeTargetEscapes` treated a backslash as a separator on POSIX**, where it
  is an ordinary name character: `x\y.test.ts` split into phantom components,
  the first `lstat` died ENOENT, and the catch reported "no escape" for a leaf
  nobody looked at. It also never checked the tree ROOT, which defeats every
  per-component check at once.
- **The symlink drop reassigned the probe list before the collocated-test hold
  ran**, so a mutant whose own test was dropped answered "there is no such test"
  and was scored `survived` on the strength of the other probes passing — the
  hold exists to refuse exactly that.
- **The rebuild sweep matched `node_modules` case-sensitively** on the two
  filesystems this file already special-cases: `Node_Modules` resolves like the
  real one on APFS and NTFS and survived every rebuild.
- **A tracked path in the residue list suppressed real findings.** The brief
  said "none of it is the PR's code, so a defect confined to those paths is not
  a finding" — false for a file the commit contains and this PR changes: only
  the uncommitted DIFFERENCE is foreign. It now separates the two, and a defect
  present in the committed version stays a finding.

Cheap chain removal, though each needs a prior foothold:

- The excludes file moved from a constant name in the shared temp dir to a
  `mkdtemp` directory written `wx` — git re-reads its CONTENTS on every later
  `status`, so a writable fixed path could blind the tripwire.
- `discardWorktree` now clears the registration named by the TREE's own `.git`
  pointer, read before anything is removed, instead of scanning
  `<common>/worktrees/<id>/gitdir` files — which any same-user process can
  rewrite to aim this cleanup at a live sibling. The scan remains only as the
  fallback for a tree whose pointer is unreadable, which is the case the step
  exists for, and it now refuses entries whose tree still stands.
- The scratch sweep refuses to run at all when its parent is not a directory: a
  symlinked `.qwen/tmp` redirects every delete below it, and no per-leaf check
  can see an ancestor.
- `removeUnownedNodeModules` counts a wipe it could not perform, instead of
  letting a planted directory survive every rebuild undisclosed.

Left as recorded decisions, not re-litigated: R4-5, R6-2 and the R6-5/R9-14/
R9-38/R9-40/R10-25/R10-27 class, which need probe code already running as the
user — a capability Agent 7's install and test step grants several steps
earlier. The remaining Suggestions stay on the deferred queue for a follow-up,
per the triage gate's own call that seven rounds was the moment to land this.

* fix(review): close the round-11 Criticals — the half-fixes from round 10

Five of the eight are residue from the previous round's own fixes, which is the
honest reason to take them:

- **The fsmonitor neutralization covered one spawn of two.** `git status` got
  `-c core.fsmonitor=`; the sibling `git ls-files -s -z` that builds the gitlink
  blind set did not, so the same planted command still fired during the
  measurement.
- **The gitlink blind-set filter failed OPEN.** An absent directory is the clean
  shape `worktree add` leaves; an UNREADABLE one is a place neither `git status`
  nor this probe can see, and both answered "nothing to hide there". Only ENOENT
  is clean now.
- **The wipe walk skipped symlinked directories entirely**, so a `node_modules`
  behind a PR-COMMITTED symlink directory survived every rebuild with nothing
  said. The walk still refuses to follow a link — the farm's own entries point
  out of the tree — but a link resolving back INSIDE the tree that hides
  dependencies is now counted as a failure the caller reports.
- **The sweep's ancestor check stopped at its own argument.** `lstat` refuses to
  dereference only the last component, so a symlink one hop above (`.qwen` over
  `.qwen/tmp`) resolved silently and redirected the same deletes. Every ancestor
  up to the filesystem root is checked.
- **Detection was followed by the damage it detected.** When the mutation phase
  aborts because the probe tree was relinked mid-run, the revert phase ran
  anyway — `git checkout base -- …` and `safeRmWithin` with a cwd resolving
  through the link into the shared review worktree. The phase now re-validates
  the tree first, and `safeRmWithin` lstats its own ROOT, which its docstring
  always promised and its loop never did (a root link resolves the whole prefix
  in the kernel, so every component below it looks ordinary).

Left as recorded decisions: R11-1 (`refs/replace` in the shared common dir),
R11-2's residual fallback path, and R11-3 (content filters during `status`) —
each needs probe code already running as the user, the capability Agent 7's
install and test step grants several steps earlier, and the scan fallback exists
precisely for the corrupt-pointer recovery the earlier rounds pinned.

* fix(review): close the round-12 Criticals — sanitizer gaps, sweep scope, false farm failures

Nine of the twenty-four hold without a foothold, or are defects in the previous
rounds' own fixes:

- **`residue.unmeasured` rode raw into two sinks** while the paths beside it went
  through `inertPath` — and that string is built from `ls-files -z` gitlink
  names, so a PR that commits a gitlink with ESC bytes puts a control sequence
  on the orchestrator's terminal and in the verifier's note.
- **The branch delete never got the env the check did.** `refExists` resolves the
  real repository through the sanitized helpers, while `git branch -D` in
  cleanup and fetch-pr still inherited `process.env`: with `GIT_DIR` exported
  the pipeline verified a branch in one repository and deleted it in another.
- **The ancestor refusal guarded one sweep of three.** It announced that
  `.qwen/tmp` hangs off a symlink and the same function went on to delete the
  base-tree lock and every side file underneath it. The check moved to the top
  of `runCleanup` and now refuses the whole clean.
- **A family symlink that would not unlink released the lease**, unlike the three
  sibling failures that hold it, leaving the next `fetch-pr` to pass a gate over
  an occupied path.
- **The symlink disclosure added last round produced FALSE failures**: it never
  consulted the `owned` set, so a PR-committed `alias → .` counted a failure for
  the farm this very call had just re-linked, and it judged before the walk had
  wiped the target it was complaining about. It now runs after the walk and
  skips owned farms.
- **Probe names were passed to `git ls-files` as raw pathspecs**, so a probe
  committed as `:(literal)x.test.ts` — a legal filename — was parsed as magic
  and its symlink never found. Every pathspec is `:(literal)`-prefixed now, and
  a refused pathspec drops every probe rather than answering "no symlinks".
- **The residue oracle could be blinded by index bits.** `skip-worktree` and
  `assume-unchanged` make `status` answer clean for an edited tracked file — the
  hazard the scratch tree's reset already refuses to certify around, missing
  from the reader-side probe that tells auditors the tree is pristine.
- The excludes file is created 0600 rather than 0644.

Left on the recorded boundary: the filter-gate coverage findings (R12-6,
R12-24), the hardlink and runtime-relink shapes (R12-31, R12-32, R12-72). Each
needs the PR's own test code running as the user — the capability Agent 7's
install and test step grants several steps earlier — and the last three are
properties of executing untrusted tests at all, not of this command.

* fix(review): close the round-13 Criticals — spelling, empty probe sets, raw error text

Five hold without a foothold, and four of them are defects in the previous
round's own fixes:

- **`owned` mixes path spellings by construction** — the tree root as the caller
  spelled it, each member as `containedIn` resolved it — and last round's
  disclosure compared only one of them, so a farm reached through a link
  counted a failure for a directory that same call had just re-linked. Both
  spellings are asked now, through one helper the wipe and the disclosure share.
- **A suite run with an EMPTY probe list runs vitest with no filter**, which
  collects whatever the repository holds and scores it as this probe's evidence.
  After the committed-symlink drop empties the set, the mutation and revert
  gates now skip rather than run.
- **Git's own stderr rode raw into the verifier-facing note** through
  `Error.message`, as did the filter KEY NAMES in the refusal (a git subsection
  name carries any byte but newline and NUL). Same sink class as last round's
  `unmeasured`, flattened the same way.
- **Every refusal that fires before the residue is measured answered with the
  empty list a measured-clean tree produces.** A consumer could not tell "the
  tree is clean" from "this call never looked"; the refusals now carry the
  unmeasured reason.

Left on the recorded boundary, re-checked and unchanged: R12-6, R12-22, R12-24
(config planting), and the R13-9/R13-10/R13-13 family, which need probe code
already running as the user.

* fix(review): close the round-14 Criticals — farm containment, residue blinds, release guard

Five hold without a foothold, and three of them are channels a pull request
controls outright — no same-user foothold anywhere:

- **Farm entries were mirrored link-or-not.** A committed symlink under
  `node_modules` (force-add defeats gitignore) pointing out of it became a
  write channel from the disposable tree to wherever it points — into the
  shared worktree's tracked files — recreated on every rebuild. Entries now
  resolve through realpath before linking; only a borrowed `node_modules`
  or an npm workspace self-link passes, everything else is counted and
  disclosed. (R10-18)
- **The residue probe's untracked view came from `status` alone**, which
  honors ignore rules the contaminator controls: a committed whitelist-form
  `.gitignore` (`*` with `!`-negations) blinded it to contamination. The
  probe now merges `ls-files --others` without `--exclude-standard` and
  filters the pipeline's build artifacts in code. (R10-19)
- **A gitlink named in bytes UTF-8 cannot decode dropped from the blind
  set**: the mangled spelling never resolves on disk, the readdir read
  absent, and a contaminated gitlink certified clean. Such names now fail
  closed into unmeasured. (R11-4, second entrance)
- **The `owned` set the rebuild disclosure asks held one spelling** while
  the disclosure loop presented another — on a host whose tree path carries
  a symlinked ancestor (macOS's `/var` vs `/private/var`) every rebuild
  counted a phantom failure for the farm the same call had just re-linked.
  The set is normalized once, at build time. (R13-1 remainder)
- **`releaseWorktree` followed a symlink standing at the path**:
  `existsSync` resolved it and `git worktree remove --force` deleted
  whichever registered worktree it named — and a dangling one wedged the
  next `worktree add` while invisible. The lstat-first guard cleanup's
  family sweep applies to every path now lives at this choke point, which
  is where fetch-pr's `cleanStale` releases. (R13-3)

Pinned against real git and real filesystems; all seven new tests fail on
the pre-round sources.

* fix(review): skip the invalid-UTF-8 gitlink fixture off Linux

APFS and NTFS both refuse a filename that is not valid UTF-8, so the fixture
cannot be created there — `mkdir` fails ENOENT on macOS, and the round-14 test
reddened for every maintainer on a Mac while the repo's `pull_request` CI, which
runs the unit legs on Linux only, stayed green. The shape it pins cannot exist
on those filesystems either, so the skip loses no coverage.

Same class as the round-2 and round-6 guards (`chmod` semantics, NTFS reserved
characters): a fixture that cannot exist on a platform is skipped there rather
than left to fail in a leg nobody watches.

* fix(review): close the round-15 Criticals — a tripwire that was firing on every healthy run

The important one is a regression this PR introduced two rounds ago, and it is
measurable rather than hypothetical. Round 14 answered "a `.gitignore` can hide
a probe's leftovers from `status`" by merging the ignore-INDEPENDENT untracked
listing into the residue set. Measured on a healthy review worktree of this
repo, after the `npm ci` and build the pipeline itself runs there: `git status`
reported NOTHING and the tripwire reported 3 957 paths — coverage HTML,
`.tsbuildinfo`, husky's installed hooks. Every one of them reached every
verifier and every reverse auditor as contamination, with `rm` and `git
checkout` recipes attached, and real residue would have been three lines inside
that. A tripwire that fires on every healthy run is not read.

What separates the two sets is not the pattern, which is unbounded, but WHO
WROTE THE RULE. An ignore file the commit under review carries is ordinary repo
hygiene that every reader of that tree sees identically; a rule from anywhere
else — `info/exclude`, a `.gitignore` written after the checkout — is the
plant. `check-ignore -v` names the source file per path, so the reconciliation
asks git that question instead of inferring it from the name. One exception,
because it is the whitelist form's whole trick: a rule from the commit that
hides EVERYTHING (`*` with `!`-negations) vouches for nothing and is not
believed. Measured again through the real function afterwards: 3 957 → 0, with
the two files that were genuinely modified at that moment still named.

Also:

- **The index-bit oracle failed OPEN.** Its guard asked for `status === 0`, and
  a spawn that dies leaves `status` null — so the one tree it could not read
  the skip-worktree/assume-unchanged bits of was certified clean, which is
  precisely the tree that can be carrying a mutant `status` cannot see. It now
  fails closed like its three siblings, and takes `-z` so `core.quotepath`
  cannot octal-expand a non-ASCII name past the buffer.
- **The efficacy probe tree carried tracked writes between runs.** Re-linking
  `node_modules` before each run (round 5) covers half of what a run leaves
  behind; tracked files are the other half and the more direct one — a suite
  that rewrites a probe file after vitest has collected it stays green for its
  own run and hands every later run a file of its choosing, and the verdict
  that buys is `killed`: "a test catches this", asserted for statements no test
  covers. Every run now opens by putting the tree back to its commit, guarded
  by the same identity gate the residue probe uses so a directory that is not a
  checkout is skipped rather than having the enclosing repository checked out
  into it.

Two fixtures had to change with it, and both were relying on the hole. The
control-setup test made its point by having the fake runner DELETE the probe
file; a deleted probe file now comes straight back, so it relinks it instead
(the read-failure path itself is pinned directly in the unit suite). The
wrong-line-mutant test wrote its mismatched content without committing it,
which is not a state a probe tree can be in — a probe tree is a detached
checkout — so it commits it.

Each fix was verified by reverting it and watching the new test go red: the
provenance filter, its trust direction, the catch-all exception, the
`.husky/_` footprint entry, the fail-closed oracle, and the tracked-file
restore.

* chore: keep main's package-lock.json

A local `npm install` during the merge dropped six `"peer": true` markers
from the lock. Unrelated to this branch, so it goes back to main's copy.

* fix(review): close the round-16 Criticals — catch-alls, the exec env family, untracked plants

Two of these are holes in last round's own fix, which is the right way round
for them to be found:

- **`hidesEverything` recognised only the `*`/`**` spellings.** `?` matches any
  single character, so `?*` is `*` with extra steps — a committed `.gitignore`
  whitelist written that way vouched for everything it hid, with no execution
  anywhere: it is committed content. The check is now shape-based rather than a
  list of spellings: every segment wildcard-only, at least one `*`. Enumerating
  the spellings one at a time is how the hole got made.
- **The probe tree's pre-run restore failed OPEN on an unresolvable `.git`.**
  The gate skips a directory with no `.git` because there is no commit to
  restore FROM — but `.git` is untracked, so nothing ever restores IT, and a
  guest that overwrites it once bought "proceed, nothing to put back" from
  every later phase. Present-but-unresolvable is now a failure, which is the
  same discipline round 15 applied to the residue probe's index-bit oracle.

And three that stand on their own:

- **`safeRmWithin` split the PR-controlled path on backslashes unconditionally.**
  On POSIX a backslash is an ordinary filename character, so ONE committed name
  — `\..\review-pr-42\package.json`, legal on POSIX — became three components,
  two of them `..`, which `join` normalises away silently. The revert phase's
  delete then reached the sibling shared review worktree. `probeTargetEscapes`
  already split platform-dependently with a comment naming this hazard: the
  write side had the fix, the delete side did not. Both now, and a `..`
  component is refused outright rather than resolved.
- **`sanitizedGitEnv` closed redirection and config injection and left
  execution open.** `GIT_SSH_COMMAND` and `GIT_EXTERNAL_DIFF` are a command,
  `GIT_EXEC_PATH` moves git's own subcommand and remote-helper lookup,
  `GIT_TEMPLATE_DIR` plants hooks for the next `init`. Not a new judgement
  call: `config/shared-env-keys.ts` blocks exactly this family for session
  subprocesses, with the rationale written out there. The setter need not be an
  attacker — a reviewer's shell profile exporting `GIT_EXEC_PATH` silently
  changes which `git-remote-https` every fetch in this pipeline runs.
- **Nothing removed UNTRACKED files between probe runs.** Round 15 restored the
  tracked half; a `vitest.config.ts` — untracked, because no zero-config
  project commits one — is what a suite reaches for to decide the next run's
  collection. `clean -fd` and not `-fdx`, so the borrowed farm and the ignored
  build output the probes need survive.

Also, the live-symlink test in `git.integration.test.ts` compared git's
CANONICAL worktree path against an un-canonicalized `mkdtemp` path. It passes
on macOS by accident — `/private/var/…` contains `/var/…` as a substring, so
`toContain` succeeds — and would not on a fixture reached through a symlinked
ancestor. It now realpaths, so it passes on purpose.

Each fix was verified by reverting it alone and watching its test go red.

* fix(review): close the round-17 Criticals — provenance, absent .git, ignored plants, ancestor links

Three are holes in the two previous rounds' own fixes:

- **"Tracked" is not "unchanged".** The round-15 provenance test asked
  `ls-files` whether the ignore file's PATH is in the index, so a `.gitignore`
  the commit carries went on vouching for rules appended to it after the
  checkout. The status set the same function already computed answers this: a
  source that appears there has been edited away from the commit, and its rules
  are the writer's.
- **An ABSENT `.git` was read as "nothing to restore".** Round 16 made the
  probe tree's restore fail closed on a `.git` it cannot resolve and left the
  cheaper state open — `.git` is an untracked pointer file inside the tree the
  PR's own suite runs in, so one `rm` bought "proceed" from every later phase.
  Running the restore anyway is not the alternative: with no `.git`, discovery
  walks UP and checks the enclosing repository out into the tree. Refusing is
  the only answer that is neither.
- **The between-run sweep honored the ignore rules.** `clean -fd` skips what
  the commit's own `.gitignore` names, and those rules are the PR's to write,
  so a plant named to match one survived every restore. It is `-ffdx` now, with
  `-e node_modules`: the borrowed farm is the one ignored thing in that tree
  the probes cannot run without, and everything else ignored goes. The two
  restore spawns also empty `core.fsmonitor`, which both of them execute.

And four that stand on their own:

- **`releaseWorktree`'s symlink guard was leaf-only.** `lstatSync`
  dereferences every component except the last, so a link at `.qwen/tmp` left
  every path under it looking ordinary while `git worktree remove --force`
  landed in whatever checkout it named. `runCleanup` refuses its whole sweep
  for this; `cleanStale` releases with no guard of its own, so the refusal now
  lives at the choke point every caller inherits.
- **`runCleanup`'s own ancestor guard ran before a network-bound audit** and
  nothing re-checked it afterwards, though the lease condition beside it gets
  exactly that re-check for exactly that window.
- **The scratch tree's filter screen read the wrong tree's config.** It runs
  against the review worktree, while the checkout it authorises runs in the
  SCRATCH tree, whose own `config.worktree` is honored once
  `extensions.worktreeConfig` is on. The screen now reads every entry under the
  common dir's `worktrees/`.
- **`runOneMutant` and `runControlMutant` had no pre-write escape re-check**,
  while `runOneHunkProbe` — in this same diff — carries one with a comment
  explaining the threat.

`redirectedAncestor` is now one shared function rather than two, and its walk
STOPS at the checkout instead of climbing to `/`: `/var` is a symlink on every
macOS box, so the unbounded version refused every sweep there while reporting
that it had found a redirect.

Two fixtures became real checkouts, because a probe tree is one in production
and a bare `mkdtemp` no longer reaches the behaviour they pin.

Each fix with a behavioural test was verified by reverting it alone and
watching that test go red. The pre-mutation re-checks are window-narrowing on a
check-then-use race and are not pinned by one.

* docs(review): drop the JSDoc left stranded when redirectedAncestor moved

Round 17 moved `redirectedAncestor` into `lib/worktree.ts` and deleted the copy
here, but not the block above it — which then sat on `scratchWorktreesOf`,
describing a function this file no longer has and, worse, describing the OLD
behaviour: "the walk stops at the filesystem root" is exactly what round 17
changed, because `/var` is a symlink on macOS and an unbounded walk refuses
every sweep there.

* fix(review): close the round-19 Criticals — PR-content channels, and the guards this PR wrote for itself

Two are reachable from a PR's committed content with nothing executed, which is
the line this branch draws everywhere else:

- **A committed `vendor -> ../stash` was never counted.** The rebuild walk
  discloses a `node_modules` symlink resolving back INSIDE the disposable tree
  and silently skipped one resolving OUT of it — so the farm reported
  `{linked: n, failed: 0}` while a `node_modules` under the link's target
  survived every rebuild. Node realpaths the importing file, so imports under
  the link resolve in the stash and decide later verdicts. Outside state cannot
  be wiped from here; it is COUNTED, which is what the contract promises.
- **`workspaces: ["."]` widened the self-link whitelist to the whole shared
  worktree.** npm accepts a root manifest declaring itself a workspace and
  creates the self-link itself, `containedIn(root, '.')` answers the root, and
  every `node_modules` link resolving anywhere inside the review worktree then
  passed the verdict and was mirrored into the disposable tree as a read-write
  channel back. A member that resolves to the dependency root is not a member.

Four more are this PR's own guards not being applied to the code beside them:

- **`resetScratchTree` lstat'd only the immediate parent**, on the stated
  premise that `.qwen/tmp` is the one component above the leaf anything here can
  replace. False one hop higher: a link at `.qwen` redirects the whole path, and
  then every check in that gate agrees with every other because they all resolve
  through it — toplevel self-equality, common-dir equality, gitdir ≠ commondir,
  and even the backpointer round-trip. It walks every ancestor now, bounded at
  the repository the common dir belongs to.
- **`restoreProbeTreeTracked` had none of the gates its sibling reset has.** A
  rewritten gitfile pointing at another repository passed `--show-toplevel`
  self-equality and the "restore" wrote that repository's content in; a
  skip-worktree bit made `checkout --force` skip a tampered file silently while
  the function answered "as the commit left it". Both are checks `scratch-tree`
  documents for the identical reset. The backpointer and ancestor checks apply
  to the linked-worktree shape a probe tree actually has — a standalone
  checkout has no admin entry to round-trip and IS the repository root.
- **The revert phase collected probes screened once, from the index, before the
  baseline.** Every run since executes the PR's own test code, which can replace
  a probe with a link; the mutation writers re-check immediately before they
  write and this collection had no equivalent, so a relinked probe was collected
  THROUGH the link and scored against code the revert never touched.
- **`runCleanup`'s symlink branch never pruned.** It returns before
  `releaseWorktree`, which is where the pipeline's only other prune lives — so
  the family paths were unlinked and reported swept while their registrations
  stayed behind to wedge the next `worktree add`.

And two cheap ones with no argument against them: `sanitizedGitEnv` deleted by
exact case, which removes nothing on Windows where env lookup is
case-insensitive (the `shared-env-keys.ts` list it is modelled on folds case for
this reason); and it now sets `GIT_NO_REPLACE_OBJECTS=1`, because one
`git replace <sha> <evil>` in the common dir makes every `checkout --detach
<sha>` here materialise someone else's tree while `rev-parse <sha>` still
answers the original.

Each fix is pinned by a test that goes red when that fix alone is reverted.

* fix(review): close the round-20 Criticals — an empty probe list, a dropped refusal, a symlinked root

The first one is a bug this branch shipped yesterday, and the worst kind: it
turns a screen into a wider run.

- **The revert phase could call `runProbeSuite` with an EMPTY list.** Round 19
  added a screen that drops probes relinked out of the tree after the baseline;
  the `probes.length > 0` gate the phase opens with was taken before that screen
  could empty the list, and `vitest run` with no file argument collects the
  WHOLE suite — so "every probe was tampered with" became "score everything",
  with the verdicts attributed to files the phase never selected. It now stops
  the phase and says which of the two happened, once rather than per file.
- **`cleanStale` discarded `releaseWorktree`'s refusal.** The guard added in
  round 17 declines to release through an ancestor symlink and reports why;
  `fetch-pr` called it as a bare statement, so the sweep looked successful and
  the next `worktree add` wedged at a path nobody was told about. The result is
  read and the reason printed, like every other failure on that path.
- **The probe tree's own root was never lstat'd.** The ancestor walk added in
  round 19 starts above the leaf, and every identity comparison realpaths both
  sides — so a probe tree that IS a symlink into the shared review worktree
  agrees with itself all the way down, and the restore's `checkout --force` and
  `clean -ffdx` would have run in the tree every other agent is reading.

The first is pinned end-to-end by the fixture that already relinks its probes
mid-run: before round 19 it scored that relinked probe `inert` — a fabricated
verdict read through the link — and the assertion now names the phase-level
refusal instead. Reverting the guard alone turns it red.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-20 07:50:44 +00:00
ChiGao
02d303f849
feat(serve): persist prompt terminal ledger for cold-load reconciliation (#9426)
* feat(serve): persist prompt terminal ledger for cold-load reconciliation

Turn terminal events (turn_complete / turn_error) were synthesized by the ACP bridge and published over SSE only, so a prompt that was in flight when the daemon died could never be resolved after a restart: the cold load replay emits transcript chunks and carries no terminal evidence, leaving promptId-keyed orchestrators stuck on "unknown".

Each session now owns an append-only sidecar ledger next to its transcript. The bridge appends one in_flight record at prompt admission and one terminal record at the single publishPromptTerminal exit (covering the close/kill/channel-crash/daemon-shutdown flushes) through an injected synchronous sink. Ledger writes are best-effort and never block prompt execution or teardown, and records carry only ids, states, and timestamps — no prompt text, user content, or paths.

On a cold session load the serve layer reconciles prompts left dangling by a dead daemon: it classifies the transcript tail with the existing turn-interruption detector and appends a completed (stop reason reconstructed_from_transcript) or interrupted (code daemon_lost) verdict, guarded by an attribution check so an unattributable tail stays unknown (fail-closed). The load response gains an optional promptTerminals field with the trailing 64 terminal records, omitted entirely when the ledger holds no terminal evidence, and archive/unarchive move the sidecar alongside the transcript so evidence survives storage lifecycle.

Design: docs/design/2026-08-19-prompt-terminal-ledger-design.md

* fix(serve): tighten ledger reconciliation fail-closed semantics and complete sidecar lifecycle

Address review findings on the prompt terminal ledger:

- reconcile: fail closed on multiple dangling prompts (no synthesized
  terminal for the newest either); attribute the oldest dangling prompt
  only when the attribution guard skips settled admissions (fixes the
  [A if, B if, B cancelled] misattribution veto), the transcript's last
  write postdates the admission (temporal evidence), and a clean
  verdict is upgraded to interrupted when the model tail holds any
  functionCall part, id or not (id-less tool-call guard covering the
  detectTurnInterruption wire-pairing blind spot)
- lifecycle: removeSessionFiles deletes the ledger in both archive
  states; archive/unarchive move it through a single
  getPromptLedgerPathForState helper with merge semantics when the
  destination already exists (append-and-unlink instead of a permanent
  split); move warnings carry full source and destination paths in
  both directions
- scans: DataProcessor.scanChatFiles and
  usageHistoryService.rebuildFromSessionJsonl exclude .ledger.jsonl
  sidecars (the ledger is not a transcript)
- writer: appendPromptLedgerRecord seals a torn tail before appending
  so a torn fragment cannot fuse with (and destroy) the next record
- tests: pin the new behavior across multi-dangling fail-closed,
  settled-then-queued attribution, valid interleave migration,
  temporal veto, id-less tool-call guard, sidecar lifecycle
  (move/merge/warn-only delete), torn-tail sealing, queued-admission
  flush on shutdown, active-prompt and resume load contracts, and
  ledger exclusion from insight scans
- docs: sync the design doc's reconciliation algorithm, lifecycle, and
  fail-closed invariants

* perf(serve): read only the ledger tail for load-response promptTerminals

readRecentPromptTerminals ran on every POST /session/:id/load (including
attached hot loads) and synchronously read and JSON-parsed the entire
ledger — a multi-megabyte event-loop stall for long sessions on the
per-request hot path.

Add a tailBytes option to readPromptLedgerRecords that reads a trailing
byte window (the first window line is always dropped: the window start
can tear a line in half). The load path now reads a 256 KiB window,
which holds hundreds of ~150-byte records against the 64-terminal
response cap; sessions whose ledger outgrows the window return a
best-effort trailing subset, which the response contract already allows.

* fix(serve): close wrong-terminal attribution classes in cold-load reconciliation

Strengthen the reconcile attribution evidence per review round 2:
measure the temporal evidence on the same api-history projection the
verdict uses, fail closed on a compression checkpoint written after the
target's admission, and require the visible tail to postdate every other
prompt's settled terminal (FIFO evidence). Also fix a TS18048 narrowing
gap in the window test, make the seal test assert the raw file layout,
and restructure the window test so the call-site tailBytes wiring is
actually observable.

* fix(serve): keep ChatRecord import inline so lint-staged cannot merge it into a type-only import

* fix(serve): fail-closed reconciliation on millisecond clock equality and deadline-overlapped turns

* fix(serve): TOCTOU fence before ledger append and documented residual attribution risk

* test(serve): pin the ledger race fixture on the transcript timeline

* feat(serve): bind cold-load evidence to the admission via a dispatch marker

* test(core): pin the ledger sidecar exclusion in usage rebuild

* fix(serve): create ledger sidecar owner-only and fence marker-era compression by position

Round-7 review Criticals:
- appendPromptLedgerRecord created the sidecar with umask-default
  permissions (0o644) while the adjacent transcript is owner-only; the
  ledger now follows the 0o600 convention at creation time.
- Marker-bearing admissions fence post-admission compression by marker
  position instead of wall clock, so a backward clock step cannot hide
  a compression reset that voids the evidence chain.
- Design doc: the residual-risk claim is corrected — the dispatch
  marker binds ordering, not ownership; the two ownership classes that
  survive it (recordless predecessor with continued writes, ledger-less
  cross-client writer) are documented, pending writer identity on
  transcript records (#9483).

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-08-20 07:27:49 +00:00
易良
48b30647d0
refactor: centralize cross-package contracts (#9497)
* refactor: centralize cross-package contracts

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

* docs(core): clarify sub-session prompt limit scope
2026-08-20 06:24:41 +00:00
ytahdn
a41d5ec058
feat(web-shell): unify file uploads and references (#9477)
* feat(web-shell): unify file upload and reference flow

* fix(web-shell): address attachment review feedback

* fix(web-shell): address attachment review feedback

* fix(web-shell): address attachment review feedback

* fix(webui): restore optimistic text prompts

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-20 06:18:34 +00:00
jinye
a659539bc7
feat(cli): Add standalone conversation isolation primitives (#9341)
* docs: finalize standalone PR2 core design

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

* feat(cli): Add standalone conversation isolation primitives

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

* fix(serve): block mixed-case standalone restore

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

* fix(serve): fail closed on corrupt session metadata

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

* test(cli): repair PR2A CI doubles and identity replacement cases

The ubuntu Test job failed on ten PR2A cases that pass on macOS:

- Export SessionIdCaseConflictError from the worktree test's core mock
  and give its SessionService double findSessionIdIgnoringCase, since
  loadSession now resolves persisted spelling before reading metadata.
- Add readCreationMetadataIfReadable to the Live task fake and mirror
  it onto the three server lifecycle spies so the fail-closed store
  interface sees the same fixture metadata as the legacy tolerant
  readCreationMetadata path it replaced.
- Pin the original inode via rename in the two same-path replacement
  cases. ext4/overlayfs recycle a freed inode immediately, so rm+mkdir
  at the same path could satisfy the recorded device+inode identity on
  Linux runners and make a real replacement look valid.

* fix(serve): key restore shared guard on persisted session id spelling

The restore handlers resolved the persisted (possibly uppercase)
spelling of a session id only inside the shared coordinator guard,
while batch delete locks its exclusive guard on the raw caller ids.
A restore of the normalized request id therefore raced a concurrent
batch delete of the persisted-spelled id on case-sensitive volumes.

Resolve the persisted spelling before acquiring the shared guard and
key runSharedMany on the resolved id so both sides contend on the
same key, in both the REST and ACP restore handlers. Regression
tests assert the guard key at both transports.

* fix(serve): unify persisted-session conflict contract across restore surfaces

- SessionIdCaseConflictError now carries an optional candidateSessionId
  with a shape-aware message, so a same-spelling active+archived conflict
  names the persisted spelling instead of blaming the request-case id.
- REST/ACP-HTTP conversion re-checks the candidate spelling before
  mapping to SessionConflictError; ACP child surfaces both shapes as
  INTERNAL_ERROR + errorKind 'session_conflict', and the reserved-source
  rejection carries errorKind 'reserved_session_source'.
- Pin the previously ungated guards from review: lineage validity
  conjuncts and archived-state reads (session-source), persisted-spelling
  adoption in ACP load/resume, ensureStandaloneDirectory EEXIST raced
  re-inspection, and the trailing root revalidation inside
  inspectConversationDirectoryIdentity (fs-interception seam).

* docs: sync standalone PR2 plan and design docs with R1 review

- Plan doc: ship the readCreationMetadataIfReadable signature in the
  store snippet, record jsonl-utils/error-response/readCreationMetadata
  in the PR2A checklist with the second-core-file re-audit outcome,
  name the launcher as child-UUID generator in both sections, extend
  the PR2A vitest and prettier gates to every PR-touched file, and stop
  claiming junction/Windows coverage the matrix cannot run.
- Design doc: reconcile the initial-prompt ordering clause with the
  strict create schema (prompt admitted by createWithInitialPrompt
  after the create transaction commits), replace the stale "before the
  UUID can be released" wording with the terminal-reservation model,
  and enumerate all seven deny categories in the acceptance matrix.

* fix(cli): normalize session-map lookups in restore failure cleanup guards

The sessions map is keyed by normalizeSessionIdForLookup-folded ids, but
the three cleanupAfterRequestFailure guards read it with the raw
config.getSessionId(). After a restore adopts a non-canonical persisted
spelling (uppercase legacy transcript), those reads always miss and the
guard would treat a still-stored session as unstored, shutting its
config down in the double-cleanup-failure window. Normalize at the read
sites; no behavior change for canonical or non-UUID ids. The adoption
test now also pins that caller-case follow-up operations (cancel) still
reach the adopted session through the normalized key.

* fix(cli): snapshot standalone dir entries after final identity inspect

prepareStandaloneDirectory read the child entries before the trailing
identity re-inspection, so a same-uid entry appearing between the two
steps would not flip the not_empty verdict. Read entries after the
final inspect so the emptiness check runs on the freshest snapshot the
identity machinery can guarantee.

Addresses yiliang114's review on PR #9341.

* fix(cli): lock restore guard on both request and persisted session id spellings

Restore keyed its shared SessionArchiveCoordinator guard only on the
resolved persisted spelling while batch delete/archive/unarchive lock on
raw caller ids, so on a case-insensitive filesystem a delete carrying
the request-case spelling never collided and could unlink the
transcript mid-restore (R2-1). Lock both spellings on the REST and
ACP-HTTP restore surfaces.

Also from R2 review:
- Pin the pre-guard/in-guard conflict-conversion stages in the
  both-states restore tests (R2-8: call-count + guard-not-entered
  assertions; mutation witness supplied by the reviewer).
- Add the toRpcError SessionIdCaseConflictError producer case to
  dispatch-error.test.ts and list the suite in the PR2A verification
  block; extend the PR2B block with the collocated suites its checklist
  modifies (R2-5).
- Correct the plan checklist label for the pre-existing shared
  jsonl-utils module from Create to Modify (R2-7).

* fix(cli): canonicalize session-archive coordinator lock keys

The two-spelling restore guard from the previous commit closed only the
enumerated spellings: any third case variant of a caller id took an
exclusive key that collided with no held guard, and on a
case-insensitive filesystem could unlink the transcript mid-restore —
including the common case where request and persisted spellings
coincide and the lock set collapses to one key (R3 review, probe-
verified on a case-insensitive mount).

Canonicalize lock keys with normalizeSessionIdForLookup at the
coordinator boundary (runSharedMany / runExclusiveMany /
assertNotTransitioning) so every case variant of a session id contends
on one key, and revert the restore-side spelling enumeration it makes
redundant. Add a coordinator-level regression test for the case-fold
collision, and fix a misleading comment above the in-guard call-count
assertion (R3-1).

* test(cli): pin workspace ordering/race propagation and align PR2 plan

From the R4 review:

- Pin that prepareStandaloneDirectory reads entries after the final
  identity re-inspection, via an interposed inspect that plants an
  entry mid-sequence (R4-3; mutant-verified to flip).
- Pin that ensureStandaloneDirectory propagates a raced 'compromised'
  inspection verbatim instead of collapsing it to identity_changed
  (R4-7; mutant-verified to flip).
- Plan: daemon bridge live-entry lookups (including
  getSessionEventEpoch) use the canonical ID — acp-bridge byId.get is
  exact-match with no id normalization — while the storage spelling is
  confined to SessionService filename/directory-hash/ACP-child storage
  operations (R4-1).
- Plan: declare the session-archive coordinator lock-key
  canonicalization in the PR2A inventory and run session-archive.test.ts
  in the PR2A block (R4-2); add dispatch-error.test.ts to the PR2B
  block (R4-5).

* fix(core): make case-insensitive resolver conflict decisions content-based

The resolver threw SessionIdCaseConflictError on filename enumeration
alone, before any content validation, and silently dropped a single
candidate whose head recovered no records. Two probe-verified failure
modes from the R5 review:

- A present-but-unreadable case-variant transcript (torn/empty/
  foreign-project head) resolved to undefined, so create admission
  admitted the canonical spelling and materialized a case-only twin;
  every later resolve then threw on the duplicate, permanently locking
  out the just-created session (R5-1).
- A valid session with an unreadable same-spelling twin in the other
  state directory threw on enumeration while getSessionLocation
  cleanly reported one readable copy — listed as loadable, but every
  restore 409'd (R5-2).

Conflict arms now consult getSessionLocation: exactly one readable
spelling wins; conflict is thrown when two or more are genuinely
readable (or a single candidate is conflicted across states); a
candidate whose head fails validation still occupies the id when its
file is on disk, while one that raced away mid-resolution resolves to
undefined. Admission already maps the thrown conflict to
persisted-true, so no admission change is needed.

* fix(cli): make caller-supplied sessionId create admission case-aware

The argv['sessionId'] branch (reached by raw stdio ACP session/new with
a requestedSessionId, without any daemon reserveCreate) checked
occupancy with exact-spelled sessionExistsInAnyState only, so a legacy
mixed-case transcript did not block the create and the daemon persisted
a case-only twin — which the resolver's conflict semantics then make
permanently unrestorable on every surface (R5-2). Route the check
through the case-insensitive resolver, treating its conflict throw as
occupancy.

Also pin that the ACP restore path hands the resolver-adopted storage
spelling to assertSessionLoadable: archived uppercase transcript
restored via the canonical lowercase id must surface errorKind
'session_archived' (R5-3; the request-spelling mutant skips the error
on case-sensitive filesystems).

* fix(cli): narrow reserved-source restore gate to internal runtimes

Two items from the maintainer's round-2 live verification:

- N1: the restore-side reserved-standalone-source gate fired for every
  runtime, so a transcript persisted on main with the client-supplied
  sourceType "standalone" became permanently unloadable while still
  listed. The create side already blocks new reserved-source
  transcripts, so any such file on an ordinary store predates the gate
  — keep it loadable there and hide only on the internal Conversations
  runtime (REST) / isolated ACP surface, where genuine standalone
  sessions will live. Generic-arm tests on both surfaces flipped to pin
  the compat restore; the internal-arm 404 pin is unchanged.
- N2: the R5 occupancy throw reused the both-states message for a
  single unreadable transcript. SessionIdCaseConflictError gains a
  'unreadable_transcript' reason with a truthful message, used by both
  occupancy arms; the case_conflict shape is unchanged.

* test(cli): flip the remaining generic-surface reserved-source test

The exact-spelling variant was missed in the N1 narrowing commit and
failed CI on ubuntu (session/load + session/resume expected the old
generic-surface hide). Flip it to pin the compat restore like its
mixed-case sibling.

* docs: sync PR2A plan with the shipped create-admission resolver consumer

The round-6 triage deferred note flagged the plan as desynced: the
R5-2 fix made loadCliConfig's caller-supplied sessionId branch a sixth
findSessionIdIgnoringCase consumer. Declare it in the per-file
checklist, count it in the consumer inventory, and add config.test.ts
to the PR2A vitest block.

* fix(core): narrow the session id case resolver's occupancy arms

Three shapes were classified as permanent occupancy, regressing paths that
loaded or created fine before the resolver replaced the exact-existence
check:

- Candidates were enumerated by case-insensitive filename match without the
  pattern gate that getSessionLocation applies, so an agent-suffixed id
  (which the CLI admits and writes under the raw session id) resolved to an
  unreadable-head conflict. Skip names the classifier would reject.
- On a case-insensitive filesystem every spelling opens the same physical
  transcript, so a readable copy plus a torn case twin reported two readable
  candidates and raised a conflict for a session with one loadable copy.
  Collapse spellings that share a device/inode and resolve to the one whose
  own directory entry backs the file.
- An unreadable head under the requested spelling itself is a case-only twin
  of nothing, yet it refused the id with no listing entry to delete or
  unarchive. Report it absent, matching getSessionLocation, so a first run
  that crashed before its first record can reuse its own 0-byte transcript.

The twin-minting protection still applies when the persisted spelling
differs from the requested one, and genuinely distinct readable spellings
still conflict.

Also pin the explicit-standalone child branch's sourceId guard and its
documented lineage behaviour, which no case covered.

* fix(cli): key the private conversation directory on the canonical id

Restore derived the directory hash from the persisted spelling while the
seven other materialize/discard call sites derived it from the lowercased
live id, so restoring a legacy mixed-case transcript produced one directory
and every later Live or task call produced a second, empty one — either
orphaning what the first held or failing the call because the session sat
outside its isolated directory. Rollback then inspected the other hash and
leaked the first directory permanently.

The directory belongs to the live entry, which the bridge registers under the
canonical id alongside the lifecycle locks and in-flight maps, so both
restore paths now derive it from that id too. This also keeps directories
that pre-date the change reachable: restore used the lowercased request id
before, so every one already on disk is canonical-keyed.

Storage-facing operations — transcript filenames, metadata reads and the ACP
child's own session storage — keep the authoritative spelling. The design doc
and PR2 plan are corrected to scope the spelling rule accordingly.

* test(cli): follow the canonical private-directory key in the Live restore case

The internal-restore case pinned the directory hash to the persisted
spelling, which the canonical-key change inverted. It now asserts the
canonical id for the directory and bridge cwd, and keeps the original
intent explicit by asserting that the creation-metadata read still uses
the persisted spelling.

* fix(cli): report proven parent lineage from the loadable-session reader

The exported reader returned one verdict for two different situations: a
child whose parent lineage it had verified, and an explicit standalone child
whose parent it never read. PR2B is being built on that reader, so the
ambiguity mattered even though no caller was affected yet.

The verdict now carries the parent's own classification. An explicit
standalone child whose parent is still readable must have a standalone
top-level parent, which also rejects a grandchild or a lineage cycle because
neither parent classifies as top-level. A parent that has been archived away
or deleted keeps the child loadable — it is self-describing, and its own
transcript is the evidence that a valid parent existed when it was created —
but `parentSource` is then absent, so a caller that needs proven lineage
rejects on that rather than guessing from `kind`.

The compatibility adapter reads the new field instead of re-reading the
store to re-derive the same classification, so its behaviour is unchanged
while a duplicated location lookup and metadata read disappear from every
legacy standalone child restore. Adapter output is identical for every
input: explicit standalone was already filtered out before the parent check,
so no current caller changes behaviour.

* fix(core): stop the alias resolver from turning I/O and missing inodes into conflicts

Two defects in the case-variant collapse added earlier in this branch.

The stat guard was statically dead: `statSync` without `{ bigint: true }`
always returns numbers, so the `typeof` test could never fire. The hazard it
was meant to cover is a filesystem that exposes no inodes — FAT/exFAT and
some SMB mounts report `ino === 0` for every file — where `dev:ino` collapses
genuinely distinct transcripts onto one identity and a real two-transcript
conflict silently resolves to one spelling. That is a fail-open on a
correctness decision, so it now uses the existing `hasVerifiableInode()`
helper, whose docblock describes exactly this case.

The resolver also swallowed every `statSync` failure into `undefined`, which
its caller reads as positive proof of a conflict. A transient EACCES or
EMFILE therefore surfaced as `409 session_conflict` — a permanent-looking
answer for a blip that succeeds on retry. Only ENOENT is now treated as
meaningful: a transcript that raced away is no longer a competing spelling.
Every other error propagates.

* fix(core): let a crashed first run resume its transcript past a case twin

The self-escape added for an unreadable transcript under the requested
spelling only covered the single-candidate arm. Once any case twin was
enumerated, resolution took the all-unreadable arm instead, where presence
was computed over every candidate including the requested spelling's own
file — so the documented crash recovery vanished the moment a stale twin
existed, and neither file could be deleted because both classify as
nonexistent.

Reusing an id whose file is already on disk mints no case-only twin, so that
arm now takes the same escape. A twin under a different spelling still
occupies the id, because minting the requested spelling beside it is what
would make both unrestorable.

The disappearance test was vacuous: its candidate spelling equalled the
request, so it returned at the self-escape and never reached the race loop it
named — deleting that loop left it green. It now uses a differing spelling
with `existsSync` false, and forcing the loop to throw unconditionally kills
it. The all-unreadable rejection test likewise needed two spellings that are
both distinct from the request to exercise twin-minting protection.

* fix(cli): fail closed when a filesystem cannot prove directory identity

The conversation-directory checks compared `dev`/`ino` directly, so on a
filesystem that exposes no inodes — FAT/exFAT and some SMB mounts, where Node
reports `ino === 0` for every entry — every directory compared equal. The
root pin, the two anti-swap re-probes around `realpath`, and the expected
identity check would all confirm a directory that had in fact been replaced,
which is the swap those probes exist to catch.

They now require a verifiable inode on both sides before treating a match as
proof, reusing the `hasVerifiableInode()` helper already written for this in
core and exporting it from the package surface. An unverifiable inode reads
as a changed identity rather than as a match.

The regression test pins a root whose inode is also 0, so a plain `===`
comparison still matches and only the verifiability guard can fail it.

* fix(cli): keep caller-supplied session-id admission fail-closed on I/O errors

Swapping the existence check for the case-insensitive resolver narrowed the
catch to `SessionIdCaseConflictError` and rethrew everything else, but the
resolver deliberately propagates non-ENOENT `readdir` and transcript-read
failures. An unreadable chats directory therefore killed startup with a raw
EACCES or ENOTDIR instead of the guarded message, and bypassed the
`throwOnSessionIdConflict` contract the ACP path depends on.

The previous check answered "occupied" for any read failure. Restoring that
keeps an unprovable id on the guarded path; distinguishing "cannot determine"
from "occupied" would be a new response shape and is left alone here.

* fix(cli): keep the directory identity module out of the core package barrel

Importing `hasVerifiableInode()` from the core package barrel pulled core's
whole module graph into the serve pre-listen bundle closure, so
`check:serve-fast-path-bundle` reported glob, chokidar, fzf, @iarna/toml and
the core shell tool runtime as statically reachable from `run-qwen-serve`.
This module is deliberately dependency-free for that reason.

The predicate is restated locally with a comment recording why it is not
imported, since core has no subpath export for it. The barrel export added
for that import is reverted so the package surface is unchanged.

* fix: correct three defects introduced by the previous review round

**The inode guard made a directory fail to equal itself.**
`createConversationRootIdentity()` compares `before`/`after` of the same path,
so requiring a verifiable inode threw `identity_changed` on the very first
root establishment and, because the workspace clears its cached root on
failure, Conversations never started on exFAT/FAT or an inode-less SMB mount.
"Cannot prove unchanged" is not "changed": the root is now established with
`inodeVerifiable: false` recorded, comparisons fall back to device, canonical
path and stat shape, and the weaker guarantee is explicit on the identity for
callers to surface. Where inodes exist they are still required to match.

**The occupancy escape was placed to discard a real twin.**
It returned early for the whole arm whenever the requested spelling was
enumerated, so a present-but-unreadable twin stopped occupying the id — the
case-only twin the surrounding comment exists to prevent. The escape belongs
per candidate, not per arm: the requested spelling's own file never counts as
occupancy, every other spelling still does.

**The private directory was still a caller obligation.**
The comment claimed the bridge registers live entries canonically and every
materialize derives from that id, but the bridge echoes whatever the caller
passed, and `LiveTaskService.ensureResident()` passes an id that originates in
a tool argument. `ConversationWorkspace` now canonicalizes before hashing, so
one session resolves to one directory by construction.

Also unifies the resolver's two arms, which were the same algorithm written
twice — that duplication is why the escape landed in only one copy.

* fix(cli): Collapse case-variant session ids in batch lifecycle and CLI create

Batch delete/archive/unarchive locked on canonical keys but still
deduped raw spellings, so two case variants of one id deadlocked
the batch. CLI --session-id now stores the lowercase spelling so
new mixed-case transcripts stop accumulating.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 06:15:52 +00:00
qqqys
36c77ff803
fix(dingtalk): attach media from quoted messages (#9347)
* fix(dingtalk): attach media from quoted messages

* fix(dingtalk): keep the reply text when attaching quoted media

R1-1: `attachMedia`'s placeholder cleanup was written for the direct-media
path, where `extractContent` generates `(audio)` / `(video)` /
`(file: name)` itself. This PR made the quoted-media path reach it, and there
`envelope.text` is the user's own reply — so a reply reading exactly like one
of those placeholders was blanked and the agent got an attachment with no
prompt. A group `@Bot (audio)` arrives here as exactly `(audio)`, the mention
having been stripped upstream. `attachMedia` now takes the placeholder to
erase as a parameter; only the direct-media call site passes one.

R1-2: the same path newly routes text-only replies through the unguarded
`mkdirSync`/`writeFileSync`/`basename` block. Those are synchronous throw
sites — ENOSPC on a write of up to 50 MB, ENAMETOOLONG from a quoted fileName
over 255 bytes (`basename` does not truncate), a TypeError from a truthy
non-string fileName. An escape rejects `processMessage`, whose catch sends
the generic error reply and never calls `handleInbound`; the msgId is already
in `seenMessages`, so DingTalk's retry is deduped and the prompt is lost for
good. The block now degrades the way a failed download already does: log,
skip the attachment, deliver the text. This also covers the pre-existing
direct-media path.

Verified: dingtalk 310/310. Both mutation-checked — restoring the caller-blind
cleanup fails 3 tests, letting the fs block throw fails 1.

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

* fix(dingtalk): address quoted-media review findings

Unify the msgType->mediaType mapping in a shared helper, make extractContent
the single source of truth for the placeholder text cleaned on attach, and
remove the store directory when a media write fails so failed stores no longer
leak into tmpdir. Merge the stacked attachMedia JSDoc blocks, document quoted
media downloads, and pin the previously uncovered paths: unmapped quoted
msgTypes with a downloadCode, own-media + quoted-media combinations, direct
placeholder cleaning, and the degraded-store attachment shapes.

* fix(dingtalk): file-back a quoted image colliding with the own image

* fix(dingtalk): give generated media store names a mime-derived extension (#9347)

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-20 03:18:32 +00:00
易良
b5577b7d11
fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel (#9202)
* fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel

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

* fix(sdk): align browser bundle budget

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

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

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

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

* fix(webui): avoid flushing sidechannel diagnostics

* fix(sdk): preserve diagnostics across rewind

* fix(webui): dedupe sidechannel history records

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

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

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

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

* fix(sdk): raise diagnostic sidechannel bundle budget

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

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

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

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

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

* fix(ci): prevent bite harness SIGPIPE

---------

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
2026-08-19 14:13:12 +00:00
ChiGao
d96f264de7
feat(telemetry): link daemon HTTP request spans to inbound W3C traceparent (#9391)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(telemetry): link daemon HTTP request spans to inbound W3C traceparent

The daemon HTTP surface records a request span per request, but every span
starts a new trace: a caller forwarding the standard W3C traceparent header
(OTel-instrumented clients, proxies, gateways) gets no linkage back to its
own trace.

Extract traceparent/tracestate from inbound request headers in the daemon
telemetry middleware and parent the request span to that remote context.
Extraction reuses the same path as the existing JSON-RPC _meta extraction
(global propagator first, strict manual fallback so behavior is identical
without a registered SDK) and fails closed: requests without a valid header
keep the exact current span shape.

* fix(telemetry): guard inbound traceparent sampling and align W3C fallback

- Force TraceFlags.SAMPLED on inbound HTTP parents via the existing
  shouldForceSampled() matrix: an unsampled remote parent under the
  default parentbased_always_on sampler silently dropped the request
  span, the whole next() subtree, and the session-subprocess spans
  forwarded via _meta (review C1).
- Replace the hand-rolled manual fallback parser with a direct
  W3CTraceContextPropagator instance so acceptance rules (future
  versions, tracestate, all-zero ids, version-00 extension field)
  match the registered path with or without an initialized SDK.
- Gate middleware extraction behind isTelemetrySdkInitialized() to
  skip the hot-path parse when telemetry is off, and emit a debug
  daemon log when a present-but-invalid traceparent header is
  rejected.
- Re-export DaemonRequestSpanOptions from the core barrel and add a
  type-level guard so the parentContext field cannot silently
  disappear (vitest alone cannot catch its removal).

* chore(vscode): regenerate companion NOTICES.txt for @opentelemetry/core

* fix(telemetry): lazy-load OTel core fallback propagator behind SDK init

Address review feedback on the inbound traceparent linkage:

- Keep @opentelemetry/core out of the static graph. The module-level
  W3CTraceContextPropagator in daemon-tracing.ts pulled the CJS barrel
  (bot-measured +65,046 bytes) into every closure loading that module,
  including telemetry-off deployments. daemon-tracing.ts now keeps only a
  holder + setter (setDaemonFallbackPropagator, typed against
  @opentelemetry/api — type imports stay free at runtime); the lazy
  sdk-impl.ts chunk, whose closure already contains @opentelemetry/core via
  sdk-node/resources, constructs and injects the W3C instance on the
  successful SDK assembly path. Until injection, extraction returns no
  parent context: the HTTP edge is already gated on
  isTelemetrySdkInitialized (nothing changes when telemetry is off), and
  the _meta edge's consumers (withDaemonSpan / withInteractionSpan)
  short-circuit on the same flag, so an unresolved pre-init parent never
  had an observable effect.
- Add the mutation-verified fail-closed test for the header-extraction
  try/catch in daemonTelemetryMiddleware: a throwing extractor leaves the
  request settling normally (recordDaemonHttpRequest still fires once)
  with no parentContext on the span options.
- Record the rejected traceparent value (truncated to 128 chars) as
  http.request.header.traceparent on the invalid-header breadcrumb —
  traceparent only carries trace-id/span-id/flags, so this is
  privacy-safe and makes broken cross-service joins diagnosable.

Also document why the _meta extraction path deliberately skips
shouldForceSampled (trusted in-process bridge vs external HTTP input).

* feat(telemetry): carry inbound trace id into daemon access log with telemetry off

Telemetry off (the default) left daemon logs without any trace id: with no
request span, the log trace prefix never fires, so a caller forwarding W3C
traceparent could not be joined to its daemon log lines.

The middleware now parses the header with a plain regex
(extractInboundTraceId — same shape/all-zero/ff rejections as the W3C
propagator, no OTel machinery) and stores the trace id on the per-response
telemetry context. The access log emits it as the camelCase traceId field
of "request completed", keeping the log-based join alive with no telemetry
config and no trace backend. With telemetry on nothing changes: the request
span already carries the caller's trace id into the log prefix.

* fix(telemetry): unify _meta/HTTP sampling and repair build export

- Export extractInboundTraceId from the core barrel: the previous commit
  exported it from daemon-tracing.ts only, so downstream package builds
  failed with TS2305.
- extractDaemonTraceContext now applies the same shouldForceSampled()
  matrix as the HTTP edge: the _meta path is also reachable from direct
  ACP clients (acpAgent newSession/loadSession/unstable_resumeSession
  and Session.prompt pass caller-controlled _meta), so an external
  sampled=0 parent no longer silences daemon spans there either. The
  in-process bridge is unaffected (its injected values are already
  SAMPLED).
- The rejected-header breadcrumb now goes through sanitizeLogText so a
  crafted traceparent cannot forge log line structure with control
  characters.
- Add the sdk-impl wiring test: after initializeTelemetry the injected
  W3C fallback propagator resolves inbound HTTP parents.

* fix(telemetry): align log-path traceparent parsing and emit traceId in both modes

- extractInboundTraceId now mirrors the vendored W3C propagator's
  acceptance exactly: single optional leading/trailing whitespace and
  trailing extension fields above version 00 (version 00 must stay
  four fields). Previously the strict four-field anchor made the two
  paths disagree on the same forward-compatible header, silently
  dropping the access-log traceId for exactly the callers the
  propagator path supports.
- The camelCase traceId access-log field is now captured whenever a
  valid header parses, regardless of telemetry mode, so one saved log
  query / alert shape works for every deployment; with telemetry on the
  snake_case span prefix carries the same id redundantly.

* fix(telemetry): move inbound trace id getter out of the middleware module

52d572c0f2 made the access log statically import the telemetry
middleware module to read the captured inbound trace id. The access log
sits inside the serve fast-path pre-listen closure (run-qwen-serve
imports it directly), so the middleware's core-barrel import graph came
along for the ride and check-serve-fast-path-bundle started failing:
the 5.6MB core chunk (shell tool, glob, chokidar, @iarna/toml, fzf)
became statically reachable from run-qwen-serve.

Move the response-context symbol, its type, and the
getDaemonTelemetryInboundTraceId getter into a new import-light
telemetry-context.ts; the middleware imports the symbol from there and
re-exports the getter, so the access log no longer links against the
telemetry module at all.

* fix(telemetry): capture inbound trace id pre-auth under a dedicated symbol

* test(telemetry): pin the trace id seam through the context module getter

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-08-19 12:21:49 +00:00
callmeYe
e4f5504e9f
feat(extensions): support authenticated HTTPS Git installs (#9458)
* feat(extensions): support authenticated HTTPS Git installs

* test(serve): update capability integration baseline
2026-08-19 09:06:54 +00:00
jinye
577f719130
fix(cli): surface daemon duplicate tool-call breaker as loop-detected stop (#9435)
The ACP daemon Session was the only duplicate-provider-id circuit
breaker path (PR #5657) that terminated silently: the turn ended as a
normal end_turn with nothing in the transcript and no telemetry, so the
session looked hung. Route the breaker through
recordDaemonLoopDetected with LoopType.GLOBAL_TOOL_CALL_DUPLICATE — the
same loop type the non-interactive CLI reports — so foreground turns
fail with the visible LOOP_DETECTED turn error, the context message is
preserved for the next turn, and the LoopDetectedEvent telemetry is
emitted. The bespoke repeatedDuplicateProviderToolCall result flag and
its dead consumer branch are removed in favor of the existing
loopDetected plumbing.
2026-08-19 07:14:27 +00:00
callmeYe
daa7d61990
feat(daemon): add batch extension activation APIs (#8788)
* feat(daemon): add batch extension activation APIs

* fix(daemon): focus extension batch API on V2

* fix(sdk): export extension batch types

* fix(core): reject empty extension batches

* test(extensions): strengthen batch regression fences

* feat(extensions): allow batch activation declarations

* fix(extensions): preserve legacy activation declarations

* fix(extensions): reject ambiguous legacy identities

* fix(extensions): preserve batch activation lifecycle

* feat(extensions): key batch activation by name

* fix(extensions): harden batch activation lifecycle

* fix(extensions): preserve declared activation lifecycle

* fix(extensions): reconcile renamed and legacy policies

* fix(extensions): preserve renamed artifact lifecycle

* fix(extensions): validate persisted artifact paths

* fix(extensions): preserve re-keyed artifact directory
2026-08-19 06:42:48 +00:00
Shaojin Wen
1eb8a0c7f8
feat(review): wire --resume through /review and the review run subcommand (#9153)
Surface the local resume feature (PR #9092) on the paths a user reaches
it from:

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

The CI review workflow runs FRESH — it does not pass `--resume`. A CI
attempt runs no-sandbox on the reviewed PR's own code and its worktree is
deleted the moment it exits, so there is no interrupted state on disk for
a retry to continue; a resume would refuse `worktree-gone` and start over
anyway. The retry loop and its test assert the fresh-only wiring.
2026-08-19 05:13:09 +00:00
jinye
83fc634f61
feat(serve): measure ACP child peak old-generation heap (#9380)
* feat(serve): measure ACP child peak old-generation heap

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): Omit unmeasured child heap reports and pin the GC observer path

Self-review round 1 on the child heap measurement:

- A child whose every getHeapSpaceStatistics() call throws (restricted
  container) reported a zeroed heap object with unclassifiedSpaceNames: [],
  which downstream reads as measured, needs-nothing, and coverage-complete.
  The probe now reports no heap at all until its first successful space
  read, matching what a child without the probe already sends.
- The GC observer callback — the only writer of peakLiveSetBytes,
  majorGcCount, and majorGcMs — had no test delivering a gc entry, so a
  wrong detail.kind check or a callback that never runs stayed green. The
  observer is now injectable and a test pins the major/minor split.
- The status/protocol/design text said the aggregate maximum "names the
  single worst-off child", but the aggregation takes Math.max per field
  independently. Wording now says each field is an independent maximum.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 05:00:07 +00:00
ytahdn
3192323c1d
perf(web-shell): keep streaming output responsive (#9405)
* perf(web-shell): keep streaming output responsive

* fix(web-shell): address streaming performance review

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-19 03:54:00 +00:00
jinye
dd82ba404e
feat(serve): Add live-state session activity watermark (#9396)
* docs(serve): Design live-state session activity timestamps

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

* docs(serve): Clarify live-state timestamp semantics

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

* feat(serve): Add live-state session activity watermark

Advance a bridge-local per-session activity watermark once when a prompt
that reached the running state publishes its formal terminal, project it
as the existing optional BridgeSessionSummary.updatedAt, and expose it on
the workspace live-state route. The advance is written before the
terminal is published so a client that observes the terminal cannot read
a stale value, and the extra millisecond keeps the watermark strictly
increasing when several terminals share a wall-clock millisecond or the
clock moves backward. A queued-only terminal, heartbeat, attach/detach,
or streamed update never advances it, and turn activity does not change
the session catalog version.

Populating the already-typed summary field lets full workspace session
lists merge live and persisted timestamps. Because the mtime and the
running-turn watermark are different authorities and the recorder writes
asynchronously, the merge picks the later valid timestamp instead of
blindly preferring the live value, so a row cannot move backward when an
async transcript write lands after the terminal.

Extend the response schema documentation for the live-state route and
GET /session/:id/status, and add the optional field on the TypeScript
SDK DaemonSessionLiveState type so consumers can pre-flight the tag once
and read the recency directly.

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

* fix(serve): Apply the later-valid activity rule on Live Task read paths

read_thread and wait_threads read the bridge summary directly, so once the
summary began carrying the running-turn watermark their fallbacks stopped
consulting the persisted transcript timestamp. Because the recorder writes
asynchronously, one task could report a later recency from the thread list than
from a thread read, and a wait cursor keyed on the live value alone stopped
changing when only the transcript advanced, so a consumer waiting for that
flush saw an unchanged cursor and exited early. Move the merge rule into a
shared helper and apply it at all three read points, including the revision
fallback used for a session with no attached client.

Add the watermark cases the design doc enumerates but the previous commit did
not ship: the deadline path publishes its terminal twice and must still advance
exactly once, a corrected forward clock jump must never decrease the value, and
a clock that advances between terminals must be reported instead of the logical
tie-breaker. Cover the single-session status route's verbatim pass-through of
the field, and cover the helper's both-invalid tail directly because no route
can supply two invalid candidates.

Correct two design-doc test-plan claims that did not match the code: the
teardown paths advance a watermark no consumer can read, because the entry
leaves live state in the same operation, and the duplicate deadline terminal
comes from the raced rejection reaching the settle handler rather than from a
late agent result.

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

* fix(serve): Merge live state on every organized session page

The organized view applied the live merge only on the first page, so page 1
sorted rows and encoded its cursor from merged activity keys while later pages
keyed the same rows by persisted mtime alone. That was harmless while bridge
summaries never carried an activity timestamp, because both keys were the mtime.
Now that a settled turn advances a watermark that leads storage until the
recorder flushes, a live row ordered onto page 1 by its watermark falls behind
the page-1 cursor boundary on page 2 and is returned a second time, displacing a
genuinely new row. Merge live state on every page so both pages key rows the
same way; a live-only row still has no persisted key to page by and stays a
first-page insertion.

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

* docs(serve): Document activity-cursor duplication on live retirement

The activity key merges the live watermark, which is in-memory only, so a
session whose live entry retires mid-pagination falls back to its transcript
mtime and can be admitted again by a cursor encoded from the higher watermark.
Pre-change keys came from mtime alone and only advanced, so pages could skip a
row but never repeat one. Name that mode in the design doc and warn SDK
consumers of the activity-ordered cursors to key accumulated pages by
sessionId.

* fix(serve): Exclude emitted identities from activity-cursor re-admission

An activity key merges the bridge's in-memory watermark, so it is not a
stable property of a row: when a live entry retires mid-pagination the key
regresses to the transcript mtime, and a live-only row that persists mid-pass
re-enters the scan keyed by its first flush. Either way a row already emitted
on an earlier page could pass the strictly-older cursor filter again and
displace a genuinely new row, which was structurally impossible while activity
keys came from mtime alone.

The organized and metadata activity cursors now carry the identities already
emitted at a live-derived key, and the after-cursor filter excludes them, so
one pass returns a session at most once. The list prunes itself: an identity
is dropped once its persisted floor alone can no longer pass the key filter or
once the row leaves the filtered collection while not live. Past a 64-identity
cap the highest floors are dropped first, degrading to the previous at-most-
once duplicate instead of failing the pass. Cursors minted before the field
existed stay valid, and the field is omitted when empty.

* fix(serve): Close carried-identity drop paths in activity-cursor pagination

The emitted-identity carry could still drop a carried session mid-pass and
re-admit it later: an identity absent from a page's collection was discarded
even though absence can be transient (pre-flush TTL cache, mid-pass group
movement), organized re-entry was evaluated under the row's current pin state
only, and the live-only cursor key could move backward when a wall-clock
rollback landed the first watermark behind createdAt.

Retain absent carried identities at a negative-infinity floor, test organized
re-entry under both pin states, and floor the first watermark advance at the
entry's createdAt. Extend the retire test to a three-page pass so carried-set
propagation through an intermediate cursor is pinned, probe scan visibility in
the mid-pass-flush tests, and cover the live-list-failure and unpin paths.
Scope the at-most-once pagination wording in the design and protocol docs to
what the carry actually guarantees.

* docs(design): floor the first watermark advance at createdAt in the normative formula

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-19 03:31:44 +00:00
Shaojin Wen
846fc05461
feat(ci): post autofix failure-path handoff comments bilingually (#9386)
* docs(autofix): design bilingual failure-path handoff comments

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-19 00:17:35 +00:00
Shaojin Wen
081a96d864
feat(cli): plain-prose /review comments; severity markers follow review.attribution (#9027)
* feat(cli): Add review settings for attribution, default effort, and default comment

* fix(cli): resolve review settings from operator scopes and close gate gaps (#8994)

Address review feedback on the review settings:

- Resolve review.attribution/effort/comment from operator-controlled
  settings scopes only (system defaults, user, system); a repository's
  .qwen/settings.json is content under review and must not control
  whether findings publish, whether the review names its model, or how
  deeply the pipeline verifies.
- Normalize the configured review.effort through the same case-
  insensitive validation as the --effort flag, so "Low" cannot miss the
  exact comparisons the forcings run and invalid values cannot leak into
  the verdict.
- Gate the modelId requirement and footer-safety validation on
  attribution: with the footer gated off, the field has no consumer and
  must not refuse the run.
- Pass the standing review.comment setting into publish-assets' call of
  the shared authorisation gate, so both callers agree on what
  authorises a run.
- Make presubmit's self-comment detection footer-independent by also
  matching the reviewing account's own top-level comments, so
  attribution-off posts still dedup.
- Align SKILL.md's Step 7 gate and every --comment branch on
  comment.effective, and add handler-level wiring tests for all
  configured defaults.

* test(cli): pin the review-settings operator defaults with unit tests (#8994)

* fix(cli): share the guarded footer strip and pin the gate audit text (#8994)

* fix(cli): raise the repository-context array bound to 256 (#8994)

* fix(cli): validate review setting values and tighten the review gates (#8994)

* feat(cli): drop the AI template tells from unattributed /review posts

review.attribution: false already drops the footer; the posted text still
read as machine output. With attribution off, inline comments now post
without the **[Critical]**/**[Suggestion]** prefixes and are written as
plain reviewer prose, the review body loses its fixed template markers
(LGTM! , the ⚠️ glyph, the **[Critical]** bullets in body lists), and
the Step 1 verdict carries the attribution flag so the orchestrator can
pick its register. The severity strip happens in the final post object
only — counting, the unmarked gate, and the ledger all still run on the
marked payload, so verdict semantics are unchanged and the default mode
is byte-identical to before.

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

* fix(cli): align presubmit dedup with severityOf and normalize auto effort (#8994)

* feat(cli): make plain prose the only register for posted /review text

The template voice is worse prose for every audience, not just the
unattributed one, so the phrasing now goes plain unconditionally: comment
bodies drop the '— Failure scenario: trigger → outcome' label and arrow
notation (the evidence rule is unchanged — trigger and wrong outcome must
be in the sentences), and the fixed review-body copy loses LGTM!  and
the ⚠️ glyph in both modes. What still follows review.attribution is the
machine-readable layer — the severity prefixes and the footer — because
qwen-autofix.yml's Critical-only mode greps posted bodies for the literal
**[Critical]** marker. With prose unconditional there is no register to
branch on, so the parse-args verdict's attribution field goes away again;
submit keeps stripping prefix and footer at post time when the operator
turned attribution off.

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

* fix(cli): keep the copy humans actually write — restore LGTM! and the warning glyph

Readability, not concealment, is the criterion: reviewers type LGTM! and
reach for the ⚠️ glyph on a warning line every day, and both aid scanning.
The earlier commits stripped them along with the real scaffolding, which
overshot. What goes is only the labelled failure-scenario template; the
fixed review-body copy is now byte-identical to before in both attribution
modes, and the pr-context LGTM filter needs no change after all.

* fix(cli): close the review-found gaps in the unattributed post path

CI review on the PR found three real defects and four test/doc gaps in the
first two commits; all addressed:

- presubmit dedup went blind to attribution-off posts: the authorship
  fallback gated on severityOf, but submit strips exactly that prefix, so a
  later round re-posted its own findings as duplicates. Attribution-off
  comments now carry an invisible <!-- qwen-review --> marker and presubmit
  matches on it — from any account, which also closes 8994's documented
  other-accounts gap.
- The attribution-off body-Critical branch quoted entries verbatim, leaking
  a model-written **[Critical]** marker into the posted body; it now strips
  like the inline path does.
- The SKILL.md payload example still showed the labelled template the
  rewritten body-format paragraph forbids; both examples now show plain
  prose.
- A comment that is nothing but its severity marker is refused at the
  consistency gate instead of posting the bare marker.
- Forged footers followed by text survived the trailing-anchored strip and
  were the only attribution an unattributed post carried; the off leg now
  strips footer-shaped lines regardless of position.
- The setting's description now names both stripping consequences (autofix
  Critical-only deferral; the invisible marker), and the loosened
  clean-approve test assertions are exact again.

* test(cli): pin the reply guard with a finding-shaped reply fixture

The unmarked reply body was excluded by the severityOf gate even with the
reply guard deleted, so nothing pinned the guard itself (mutation-verified
by CI review).

* fix(cli): close the round-3 review findings on the unattributed post path

Five Criticals and twelve Suggestions from the third CI review round, all
addressed:

- presubmit dedup: the invisible-marker branch was ungated — any account
  could plant the public marker string on a line expecting a blocker and
  have the next round silently withhold it. The branch now requires
  authorship by the reviewing account plus the exact trailing shape submit
  posts; adversarial and quote-reply fixtures pin both guards. The
  'other accounts escape dedup' disclosure from 8994 stands again.
- The marker-only gate was dead under attribution ON (the canonical footer
  was appended before the check) and stacked markers bypassed it: the
  strip is now iterative, delegates the classification to severityOf, and
  the gate refuses when the footer-and-marker-stripped remainder is empty
  or still marker-led.
- bodyCriticals and cannot-tell entries now get the forged-footer strip on
  the unattributed leg (a surviving mid-entry footer was the post's only
  attribution), the cannot-tell parse trims before matching, and ledger
  titles strip the marker (the ledger rides the body as an HTML comment
  the autofix grep reads).
- stripForgedFooterLines rewritten line-based: closing underscore optional
  (looping-model truncation), CRLF tolerated, 400-char line bound, fence-
  and indented-code aware, and byte-identical when nothing matches.
- The comment marker now carries severity (<!-- qwen-review critical -->);
  pr-context's blocker promotion reads it, so an unresolved unattributed
  Critical re-enters the re-check section every round — including past the
  ledger's horizon.
- Tests: stripForgedFooterLines unit coverage, grouped cannot-tell strip,
  ledger leg under a prNumber plan, and the adversarial presubmit shapes;
  loosened assertions re-tightened. Docs and the settings description now
  match the shipped behavior.

* fix(cli): close the round-4 review findings — marker read/write hardening

Seven Criticals and four Suggestions from the fourth CI review round:

- commentMarkerSeverity now reads only the trailing posted shape, and
  submit strips pre-existing bare marker lines before appending the
  canonical marker — a marker string quoted or planted in a reviewed file
  can no longer choose the severity the classifiers see.
- The marker disjunct in the blocker classification is gated on the
  reviewing account, via one shared predicate (isBlockerBody) now used by
  BOTH pr-context and comment-status — an empty planted 'critical' comment
  no longer becomes a permanent irrefutable blocker, and the two consumers
  can no longer diverge on the posted shape.
- The ledger's drafted-comments leg strips like the bodyCriticals leg
  (iterative markers, forged footer lines first, footer spans off the
  title), and stripSeverityPrefix now strips to empty for marker-only
  bodies — the submit gate refuses exactly that shape, in both modes.
- The fence scanner is a faithful model now: ~~~ fences count, a fence
  opener indented 4+ spaces does not open one, and lines inside a simple
  HTML block never toggle fence state.
- Producer/consumer roundtrip tests pin the marker shape (the drift class
  the module header exists to prevent); the iterative strip, the
  attribution-on marker-only gate, and the strip order each carry the
  assertion the mutations showed missing.

* fix(cli): make the unattributed strip a fixpoint, closing the round-5 escapes

Seven Criticals from the fifth CI review round, all probe-verified escape
hatches in the strip chain, closed by restructuring it:

- One shared stripForUnattributedPost iterated to a fixpoint now serves
  every attribution-off leg (submit's post transform and gate, compose's
  body lists, both ledger legs), so the sites cannot drift on order:
  forged footer lines, severity prefixes (leading AND paragraph-initial,
  via a new fence-aware stripParagraphMarkers), bare marker lines, and
  footer spans interleave arbitrarily in a looping draft and only the
  fixpoint posts none of them.
- The marker-only gate runs the full chain: a prefix over a bare marker
  line no longer posts an empty visible comment carrying a live marker.
- Marker-only body Criticals and cannot-tell entries are refused at
  compose (both modes), mirroring submit's gate — an empty-stripped entry
  no longer counts toward REQUEST_CHANGES while rendering nothing.
- The version-parens truncation (the natural mid-character cut) is
  admitted by all three footer regexes; blockquoted forged footer lines
  strip; HTML blocks stop shielding footer lines (their content renders
  visibly) while still not toggling fence state.
- The design doc's definitional line now says what ships: no VISIBLE
  attribution — the machine contract moves to the invisible severity
  marker.

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

* fix(cli): drop the dead bare-marker arm and tolerate whitespace before the colon

- submit's post transform no longer references COMMENT_MARKER: the
  unmarked gate runs first, so every body reaching the transform has a
  known severity and posts the severity-carrying marker unconditionally.
- stripSeverityPrefix tolerates whitespace before the colon after a
  marker and treats a whitespace-only remainder as empty (a trailing
  newline no longer survives as a phantom non-empty body).

* fix(cli): close the round-6 findings — faithful fences, render-nothing gates

Seven Criticals from the sixth CI review round:

- mapLinesAware now applies the map inside HTML blocks (the round-5 fix
  updated the comment but not the code — the shield stood), tracks the
  opening fence by delimiter character and run length with no info string
  on the closer (CommonMark), and the chain's final span pass is line-aware
  so fenced quotations survive the full strip.
- The emptiness gates (submit + both compose lists) project through a new
  rendersAsNothing — Cf characters, HTML comments, hollowed fences, and
  forged-footer residue are not content — and run the full post-transform
  chain, so a scaffolded-but-invisible comment can no longer post, count
  toward REQUEST_CHANGES, and re-promote as an unanswerable blocker.
- stripCommentMarkerLines admits blockquoted marker lines, matching its
  sibling regexes.
- buildLedger keeps the carried id and title when the finding text starts
  on the line after the severity marker (trimStart before titleOf) — a
  regression from routing titles through the new chain.

* fix(cli): close the round-7 findings — faithful quotes, code spans, render-nothing classes

* fix(cli): close the round-8 findings — one displayed projection for every strip

* fix(cli): close the round-9 findings — fail-closed identity, one shape per leg

* fix(cli): close the round-10 findings — rendered-text signals, bounded spans, fence-safe entries

- blocker prose scan reads only rendered text: an HTML comment renders as
  nothing, so a planted `<!-- [critical] -->` can no longer promote an
  invisible, irrefutable blocker through the ungated channel
- the footer-span version group admits only the version shape
  footerVersion validates — a span truncated inside the parens can no
  longer swallow the prose after it
- the marker strips admit the full-width colon, closing the marker-only
  refusal's ASCII-only hole in bilingual drafts
- entries containing a code-fence delimiter line are refused for redraft:
  the one-line collapse turns them into an unclosed fence that swallows
  the rest of the posted body
- the identity fail-closed trigger narrows to what identity actually
  gates — critical markers on root comments — so a planted reply cannot
  convert a transient identity blip into a repeating refusal
- the ledger's carried-id anchor reads through render-nothing residue
  left between the marker and the id, ending the silent renumbering
- the marker-only contract covers trailing Cf/comment residue

* fix(cli): close the round-12 Criticals — bounded footer version, drop-scoped blank cleanup, quote-preserving markers, gate-matched post leg

* fix(cli): close seven review sanitation entrances from round-20 review (#9027)

- CR-aware line model: scanLines and rendersAsNothing split on CR/CRLF;
  entry lists normalize line endings on ingest, so a bare CR can no
  longer hide a forged footer, a hollow fence, or a fence delimiter
  from the refusal and emptiness gates (R20-1)
- empty-login identity lookups fail closed like thrown ones in both
  pr-context and comment-status while a critical marker is posted (R20-2)
- whitespace-only body-list entries fail the renders-nothing gates
  instead of vanishing before them; the dead raw sha check drops (R20-4)
- drop-collapse never touches blank runs around an HTML-block content
  drop — quotation blanks render and survive (R20-6)
- attribution-off posts refuse drafts whose post-strip shape leaves a
  fence open at the appended invisible marker (R20-9)
- carriedClaimLine slices on the classifier's projection and both
  colon widths; presubmit reads carried ids off the attribution-off
  posted shape (R18-1)
- duplicates disclosure routes through the attribution-off fixpoint
  chain like every other body leg (R15-1)

---------

Co-authored-by: qwen-code-autofix[bot] <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-18 14:47:42 +00:00
ytahdn
0c35b304a3
fix(web-shell): use backend-authoritative queue state (#9407)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-18 12:07:55 +00:00
Shaojin Wen
da26cffc36
feat(review): Aone Code read path (second review-platform provider) (#9226)
* docs(design): /review Aone Code read path (Phase 2)

* feat(review): Aone Code read path (Phase 2)

Adds an Aone Code provider so /review can review a MaxCompute CR locally.
The read path works end to end against a real odps_src CR (verified E2E):
fetch-pr fetches `refs/merge-requests/<global-id>/head` and builds the
worktree + diff (stats computed locally, Aone advertises none); meta /
issue-context / fetch-diff resolve identity, Aone workitem evidence, and the
diff via the a1 CLI. The four reader-backed subcommands and fetch-pr select
the provider from the clone's remote (or an Aone host), so a GitHub clone is
unchanged.

Read-only this phase: pr-context / comment-status / presubmit have no Aone
backing yet (the run degrades to context-unavailable), and --comment is
refused on an Aone target. SKILL.md + code-review.md document the Aone
target and the degradations. See docs/design/2026-08-15-review-aone-provider.md.

* fix(review): address PR #9226 round-1 Aone review findings

Critical:
- match-remote: Aone CR URLs use the WEB host (code.alibaba-inc.com) while
  a clone's remote uses the GIT host (gitlab.alibaba-inc.com) — treat them as
  one equivalence class (hostsEquivalent) so a codereview URL matches its
  clone's remote and the worktree flow is reachable; nested-group remotes
  (group/subgroup/project) now collapse to the last two segments instead of
  failing to match
- registry: an explicit non-Aone host/remote now beats the cwd probe, so an
  explicitly-GitHub subcommand run from an Aone clone is not hijacked to
  Aone; hint host is trimmed; the four reader-backed subcommands thread
  --host into detection (previously dropped); dropped the unwired --platform
  dead switch
- aone parseRemoteUrl: user-less scp remotes (ssh-config/insteadOf), nested
  groups, a trailing slash after .git, and empty segments now parse; the
  parse-failure message redacts a user:token@ origin (no credential leak)
- aone getCommentBody throws on a missing id (was an indistinguishable empty
  string); fetchDiff uses gitRaw (512 MiB buffer, no CRLF rewrite, latin1)
  instead of git() (1 MiB ENOBUFS, CJK byte loss)

Suggestions:
- aone-client: 120 s timeout, ENOENT branch in the auth check ("install the
  a1 CLI"), TRANSIENT_RE anchored to HTTP 5xx (bare 502/503/504 misfired on
  command lines containing those digits), one stderr trace line per retry
- fetch-pr: validate pr_number before Number() coercion (1e3 fetched PR
  1000), trim --host before detection; submit guard trims --host;
  comment-body --pr help notes the Aone per-MR requirement
- parse-args: nested-group codereview URL grammar; invalid-url warning names
  both grammars
- SKILL.md + code-review.md: Aone paragraph corrected (clone-origin trigger,
  Agent 0 skipped, test-plan/publish-assets unbacked, pass --host), design
  doc updated (detection, Agent 0 gating)
- tests: aone.test.ts, registry cwd-mock + precedence + parseRemoteUrl cases,
  remote-match hostsEquivalent + nested collapse, parse-args nested codereview,
  submit-aone refusal

* fix(review): address PR #9226 round-2 Aone review findings

Critical:
- parse-args: the Aone CR URL grammar is now constrained to Aone hosts
  (*.alibaba-inc.com) — a /codereview/ URL on any other host hits the
  fail-closed invalid-url refusal instead of becoming a live PR target
  (unlike …/pull/<n>, which any GHE host legitimately serves)
- aone fetchDiff: merge-bases against a fetched target branch (not a
  present-but-stale origin/<target>), and the MR-head refspec is now
  force-fetched (+) so a stale throwaway ref from an interrupted run does
  not fail the fetch when the head was rewritten (normal AGit-Flow iteration)

Suggestions:
- fetch-pr: countDiffChangedLines now delegates to the single hunk-state
  walker in computeDiffStats (the two could not disagree silently); the
  changedFiles count is pinned on `diff --git` via a binary-file fixture
- aone parseRemoteUrl scheme case made explicit + pinned (RFC 3986)
- submit.test.ts pins the platform registry to GitHub so the Aone refusal
  guard neither spawns a real git in the vitest cwd nor couples to the
  machine's clone origin

* fix(review): address PR #9226 round-3 Aone review findings

Critical:
- registry: hostOfRemoteUrl now makes `user@` optional in the scp branch
  (user-less scp remotes from ssh-config/insteadOf no longer misroute an
  Aone clone to GitHub); the token-bearing scp userinfo parses to the host,
  not an owner
- parse-args: the Aone CR-URL host group now requires a REAL subdomain dot
  boundary (`(?:[A-Za-z0-9-]+\.)+alibaba-inc.com`), so lookalikes
  (`evilalibaba-inc.com`) hit the fail-closed refusal; a `/pull/<n>` URL on
  an Aone host is refused too (Aone serves no /pull/ pages)
- meta: on a non-GitHub platform an explicit `--repo` without `--host` is
  refused (no default host off GitHub) instead of emitting the contradictory
  `platform:aone` + `host:github.com`
- aone fetchDiff: spreads PINNED_DIFF_CONFIG/PINNED_DIFF_FLAGS (an un-pinned
  color.diff=always zeroes computeDiffStats), discloses a failed target-branch
  fetch via a stderr WARNING, and refuses to diff from a clone of a different
  repo; scp-form userinfo is redacted in the parse-failure message

Suggestions handled:
- comment-body: the Aone per-MR `--pr` requirement is enforced before the
  auth gate (usage errors precede auth)
- aone-client: the auth-failure diagnostic surfaces a1's real first stderr
  line (not the execFileSync preamble) and reports a timeout/kill distinctly
- remote-match docstring + registry precedence comment updated to the
  implemented behavior

Deferred to follow-up #9194: the 16 test-gap patterns, headRefOid dead-field
removal, MAX_SAFE_INTEGER digit guard, and the refusal-message host branch.

* fix(review): address PR #9226 round-4 Aone review findings

Critical:
- aone fetchDiff + fetch-pr merge-base fetch: the server-controlled
  target/base branch reached `git fetch` bare — a dash-leading branch name
  (creatable by full-refname push) parses as an option, so
  `--upload-pack=<payload>` executed attacker-named code with the
  reviewer's credentials. Pass `--` to end option parsing and refuse
  dash-leading values outright on both providers
- meta: the no-default-host guard now gates on the FLAG, not the resolved
  value — a GH_HOST export no longer bypasses it (and an empty-string
  --host counts as missing); the whole --repo branch's pure resolution
  moves above the auth gate (usage errors precede auth)
- skill: pass --host for EVERY pr-url target including github.com — an
  omitted hint falls back to the cwd origin probe, which hijacked a
  github.com review run from an Aone clone (and vice versa); lightweight
  fetch-diff/pr-context carry the host too
- submit: the Aone refusal moves BELOW the authorisation gate and takes
  the exit-3 + {"posted": false} shape instead of throwing — an
  unauthorised Aone run now ends as the skill's contract defines, and
  detection reads the effective host (flag → GH_HOST), so an Aone-pointing
  GH_HOST export is refused instead of dying opaque inside gh

Suggestions handled:
- parseRemoteUrl: strip query/fragment (credential channel into repo
  identity), fix the cleaning order for two-plus trailing slashes after
  .git, and discard an explicit port instead of folding it into the path
- registry: isAoneHost normalizes the trailing-dot FQDN spelling; the cwd
  probe delegates to lib/git's gitOpt (shared git policy)
- aone: the MR-head refspec is stated once (mrHeadRefSpec); resolveRepo
  quotes git's real error line, not the execFileSync preamble
- aone-client: the auth fall-through message is neutral (covers
  non-auth failures the login hint cannot fix)
- fetch-pr: pr_number guard tightened to ^[1-9]\d*$ (no PR zero, no
  leading zeros, no side effects before the refusal)
- the five detection-consuming subcommands' --host describes now state
  the implemented semantics; SKILL.md/code-review.md read-only phrasing
  corrected and the false "detection reads the clone's remote, not the
  URL" claim fixed

Tests: dash-leading refusal on both providers, meta guard flip tests
(GH_HOST bypass, empty flag, pre-auth), submit exit-3 shape (authorised,
unauthorised, padded host, GH_HOST), trailing-dot and /pull/-on-Aone
parse refusals, port/query/slash parse cases, fetch-pr zero-number and
base-ref refusals.

Deferred to follow-up #9194: the single-branch merge-base disclosure
(R3-9), cleanup audit skip-in-code (R3-13), URL-form --remote hint
(R3-19), publish-assets refusal parity (R3-22), and the data-path
deadline translation (R3-25).

* chore(review): re-push to re-link PR head after branch recreation

* test(review): repin SKILL.md host-rule wording in SKILL.test.ts

The round-4 fix rewrote the skill's --host notes (pass --host for every
pr-url target, github.com included); three revert-guard tests pinned the
old 'add --host <host> for Enterprise' phrasing and reddened the core
suite in CI. Repin them at the new wording.

* fix(review): address PR #9226 round-5 Critical findings

- aone resolveRepo: redactUrl now strips the query/fragment channel too —
  a ?private_token=… origin carries no @ for the userinfo redaction, so the
  parse-refusal message echoed the secret the success path strips (test
  pins the refusal message secret-free)
- aone fetchDiff: the merge-base fallback (base = ref~1) DISCLOSES via a
  stderr WARNING — previously silent, a multi-commit MR got only its last
  commit served as the complete diff (shallow/single-branch clones hit
  this; the GitHub path is loud about the same class)
- submit: the Aone write-refusal binds the platform in BOTH directions —
  the authorisation gate now surfaces the recorded target's host, so a
  recorded Aone host refuses whatever the runtime-effective host resolves
  to (an ambient GH_HOST export can no longer steer an Aone review into
  posting at a same-named repo), while a recorded non-Aone pr-url binding
  is no longer vetoed by the cwd probe from an Aone-origin clone

Tests: refusal-message redaction, fallback disclosure (spy calls captured
before mockRestore — vitest's restore clears them), bidirectional refusal
arms (recorded-Aone + GHE env refuses; recorded-github + Aone cwd posts).

* fix(review): address PR #9226 round-6 Critical findings

- authorization: the --user-authorized fast path now surfaces the recorded
  target's host too (best-effort read of the recorded args) — it returned
  before the args file was read, so recordedHost was always undefined on
  that path and the 'a recorded Aone host always refuses' invariant leaked:
  a user-authorised post of a recorded Aone codereview review from a
  non-Aone cwd with no --host/GH_HOST posted at github.com's same-named
  repo. Tests pin the fast-path host through the REAL gate and the
  end-to-end refusal (the witness scenario)
- aone: the query/fragment strip now uses [\s\S]* in both redactUrl and
  parseRemoteUrl — git stores newline-bearing remote URLs, and a plain .
  stopped at the first \n, letting ?private_token=SECRET\nx smuggle the
  token past the strip into the parse-refusal message. Tests cover both
  the parse-success and refusal paths of the smuggle

Round-6 is Critical-only per the ~5-round policy (user-confirmed for
convergence); the 13 Suggestions are deferred to follow-up #9194.

* fix(review): address PR #9226 round-7 Critical findings

- fetchDiff's throwaway ref now carries a pid suffix — two concurrent runs
  for the same MR in one clone shared the name: one session's finally-
  delete killed the other mid-review (unknown revision), and a
  pre-existing local branch of the reserved name was force-moved then
  deleted, reflog and all (race probe: 12/60 failures → 0 with the
  per-run unique name)
- the target/base-ref guards close the refspec channel the dash-only
  check left open after `--`: a leading `+` parses as a force refspec
  (fetches the wrong head — stale evidence, no WARNING) and a colon as
  src:dst (force-moves the throwaway ref or a reviewer-local branch).
  Both providers now refuse '-', '+', and ':' shapes (probe-confirmed on
  real fetchDiff incl. the served-wrong-diff and local-branch-overwrite
  witnesses); tests pin the new channels on both guards
- redactUrl and parseRemoteUrl clean userinfo BEFORE the query/fragment
  strip: a userinfo that itself contains '?' or '#' was truncated
  mid-credential, leaking the username+secret prefix into the refusal
  message and making parseable origins unparseable (flip-verified on the
  witness shapes)

Round-7 is Critical-only per the convergence directive; the 8
Suggestions (incl. the 4 bot findings) are deferred to #9194.

* fix(review): address PR #9226 round-8 Critical findings

- the server-controlled branch-name guards now validate ALLOWLIST-style on
  both providers (aone.fetchDiff's target, fetch-pr's baseRefName): the
  denylist admitted HEAD (silent fetch + merge-base through the stale
  clone-time symref), rev-parse metasyntax (wrong base under a
  misdescribing warning), ranges, and the empty string (garbled diff-less
  fallback) — a plain-branch-name shape closes every channel
- parseRemoteUrl/redactUrl consume userinfo GREEDILY up to the last @ of
  the authority — multi-@ and :-/-bearing token userinfo no longer leaks
  cleartext residue through the refusal messages or folds into the parsed
  host (take() fails closed on any surviving @); the scp strip admits only
  a removal that leaves a host: shape behind
- fetch-pr's Aone stats backfill moves AFTER the plan/rescue, where
  diffText is final — the partition-rescue republishing the full range no
  longer leaves delta-scoped numbers beside a full-range diffPath — and
  isCollapsedFromUpstream is skipped when the stats are locally derived
  (one source, not two: the disclosure needs an independent advertised
  fact, and a delta-scoped round beside the full-range count fired a
  false collapse)
- remote identity is injective again: Aone nested-group targets carry the
  full group path (parse-args → match-remote --group-path → matchRemotes
  compares every segment when both sides have three or more), and
  fetchDiff's origin guard adds the origin's host (Aone family) — a
  same-named repo in another group or on another platform can no longer
  pass either gate; SKILL.md passes --group-path for nested targets
- meta's discovery branch drops GH_HOST inheritance off GitHub — an
  ambient GHE export beside an Aone-origin clone no longer vetoes the
  valid invocation at HOSTNAME_RE; only an explicit --host steers routing

Round-8 is Critical-only per the convergence directive; all five findings
fixed, no deferrals this round.

* fix(review): address PR #9226 round-9 Critical findings

- redactUrl is fail-closed BY CONSTRUCTION: split at the last @, redact
  everything before it — the per-regex redaction kept missing shapes
  (round-9: URL userinfo with a / in the secret, scp userinfo with a
  newline, residues with no host: shape all leaked verbatim through the
  parse-refusal message)
- parseRemoteUrl cleans per form and fails CLOSED: URL-form userinfo is
  bounded to the authority (greedy within it — multi-@ and ?/# inside
  secrets consumed whole, /-bearing secrets left to fail closed in take),
  scheme inputs never fall through to the scp grammar (a malformed
  https://user:pa/ss no longer parses host user); the round-8 scp-strip
  firing on scheme URLs fabricated coordinates from query-borne and
  path-borne @ witnesses — all witnesses now parse correctly or refuse
- registry hostOfRemoteUrl consumes token-bearing userinfo (':' AND '/'
  in the secret) on both branches, mirroring aone.parseRemoteUrl —
  detection no longer parses the credential prefix as the host and
  misroutes Aone clones to GitHub; detectPlatformKind ranks an explicit
  --host above the remote-URL hint in BOTH directions (an Aone origin
  can no longer hijack an explicitly-GitHub invocation into fetching a
  global MR id from the wrong remote)
- nested-group identity is injective in both directions: matchRemotes
  compares the full group path exactly whenever the target carries one
  (any length — a 3+-segment target no longer matches a two-segment
  remote sharing its tail, nor the reverse); Aone CR targets carry the
  path even at two segments and the canonicalized URL keeps the full
  path; fetchDiff's origin guard compares the origin's full path against
  the MR's own detailUrl path (authoritative repo identity, where the
  seam's ownerRepo is collapsed); the rescue pool keys on the full path
  and same-id cross-group CR URLs are refused as ambiguous

Round-9 is Critical-only per the convergence directive; the 8
Suggestions (R8-6..R8-13) are deferred to follow-up #9194.

* fix(review): address PR #9226 round-10 Critical findings

- aone.fetchDiff's host arm keys on the CANONICAL Aone-family predicate
  (new remote-match isAoneHostFamily: port/trailing-dot/case normalized;
  registry.isAoneHost now delegates to it) — a trailing-dot FQDN clone
  that detection accepts as Aone can no longer be refused by the diff gate
  with a misdirecting remedy
- the URL cleaning/redaction class is closed structurally, not per shape
  (sixth consecutive round a new entrance was found): parseRemoteUrl's
  URL-form userinfo is consumed whole WITHIN the authority (span between
  // and the first /), and the scp-form userinfo strip + its lookahead are
  bounded at ?/# — an @ inside a query or fragment value is the
  credential's own character and can no longer fabricate coordinates from
  the query tail; redactUrl fails the DISPLAY closed with a constant when
  the last @ sits after a ?/# marker — the token tail can no longer reach
  the refusal message (URL/scp/fragment witnesses all pinned)
- isPlainBranchName rejects git's pseudo-ref set (FETCH_HEAD/ORIG_HEAD/
  MERGE_HEAD/…) on both guards — FETCH_HEAD resolves to the just-fetched
  PR head (empty diff beside full-range metadata), ORIG_HEAD to an
  arbitrary ancestor; both shape-legal, both silently wrong
- fetch-pr's merge-base probe requires the fetch to have produced the
  tracking ref — a tag-only baseRefName exits 0 writing only FETCH_HEAD,
  and the bare-name fallback once merge-based against the reviewer's
  local tag with baseFetchFailed falsely false; the tag shape now lands
  in the disclosed state
- parse-args: the repo-qualified CR URL outranks a same-number bare
  spelling as the target in BOTH the rescue pool and positional order —
  the bare number carries no host, and letting it win flipped detection
  onto the cwd fallback (a loud refusal at the merge base had degraded to
  a silent wrong-platform retarget); bare restatements of the URL target
  are skipped silently, matching the rescue loop's restatement handling

Round-10 is Critical-only per the convergence directive (the bot's own
ledger is at its round cap); the 6 convergence-posture deferrals named in
the review body join follow-up #9194.

* fix(review): address PR #9226 round-11 Critical findings

- the URL cleaning/redaction surface is closed STRUCTURALLY: one parser,
  one source of truth — registry.hostOfRemoteUrl now delegates to the
  canonical aone.parseRemoteUrl (detection and the identity parser can no
  longer disagree), and the scp branch reads GIT'S OWN grammar
  (GIT_TRACE-probed: hostinfo ends at the FIRST ':', userinfo carries no
  ':' or '/') — the last-'@' consumption once parsed a different host than
  git connects to, letting fetchDiff's same-repo guard pass while git
  fetched from another server; token-bearing scp shapes now fail closed,
  and the round-8 detection tests are re-blessed onto shapes git reads
  that way
- the pseudo-ref allowlist is CASE-INSENSITIVE on both twins: on
  case-insensitive filesystems (macOS/Windows defaults) fetch_head folds
  onto FETCH_HEAD, resolving the merge-base to the just-fetched MR head
  (empty diff beside full-range metadata); lowercase spellings refused,
  pinned
- submit.test.ts's file-level setup now saves/clears/restores GH_HOST —
  the Aone refusal reads the ambient env, and the org's standard intranet
  export pattern (an Aone-family host) turned 50 of 69 posting tests into
  refusals
- the --user-authorized fast path binds the recorded host to THIS write
  (same-PR number only — a stale recording of another PR must not supply
  a host) and scans SIBLING session recordings when the session-scoped
  args file is absent — the characteristic cross-session publish shape
  otherwise lost the host and posted a recorded Aone review at
  github.com's same-named repo (real-gate witness: exit 0, COMMENT filed);
  tests drive the real gate through a sibling-session fixture
- the tracking-ref requirement and both merge-base sites are FULLY
  QUALIFIED (refs/remotes/…): git resolves unqualified origin/<name> in
  refs/tags and refs/heads first, so a tag or branch literally named
  origin/<baseRefName> — a PUSHABLE, server-controlled refname a plain
  clone auto-carries — shadowed the just-fetched tracking ref and moved
  the merge base with no disclosure; shadow-tag tests pinned on the
  resolveMergeBase probe, the fetch-pr seam, and aone.fetchDiff

Round-11 is Critical-only per the convergence directive; the bot's own
ledger is at its round cap and this round still produced findings —
recommend freezing the bot loop and moving to human security review.

* fix(review): address PR #9226 round-12 Critical findings

- the recorded-args host lookup is HARDENED — the store lives under
  .qwen/tmp/ beside review worktrees checked out from the PR's own tree,
  so its content is attacker-influenceable: only s-* session directories
  are scanned (a malicious PR can no longer plant a root-level args file
  that binds a host), symlinks are skipped at both the directory and file
  levels (mirroring writeSkillArgs' O_NOFOLLOW write-side policy), reads
  are size-bounded, and the host binds only when the recording names the
  same PR number AND the same repo
- the canonical Aone invocation shape (bare global MR id, no URL) can no
  longer post cross-session without host evidence: a same-number
  recording with no host binds the recorded --host flag when present
  (parse-args now records it), and without one the write gate FAILS
  CLOSED with the exit-3 shape and names the remedy — instead of posting
  the review at github.com's same-named repo (the probe-verified witness
  once exited 0 and POSTed)
- parse-args: the URL-outranks-bare-number invariant now holds for MIXED
  shapes — a positional bare number restating the rescue pool's single
  PR is carved out of hasValidCandidate, so --effort <cr-url> 7 (and
  both orderings/equals-form) target the CR URL instead of silently
  retargeting onto the cwd clone's same-number PR; a different number
  still outranks
- aoneReader.resolveRepo refuses an origin outside the Aone host family —
  an explicit --host can steer detection onto this reader while the cwd
  clone is a GitHub mirror (the dual-remote migration setup), which once
  emitted {platform:'aone', host:'github.com'} and queried a1 with the
  mirror's coordinates; same predicate fetchDiff's origin guard applies
- isPlainBranchName (both twins) rejects refs/-prefixed names: legal
  branch names (check-ref-format --branch) that resolve qualified refs
  the server controls as fetch/merge-base arguments (refs/remotes/origin/
  HEAD is the clone's default-branch symref — wrong base, misdescribing
  WARNING)
- the ref-dwim class is closed at the verified sites: fetch-pr's base
  probe fetches an EXPLICIT branch refspec (bare names dwim onto
  same-named tags — exit 0, tracking ref untouched, stale base passing
  the freshness guard it never refreshed), its fetchedSha/merge-base head
  reads are refs/heads-qualified (a planted same-name tag can no longer
  shadow the real head), and aone.fetchDiff's target fetch + merge-base +
  diff-range reads are qualified the same way

Round-12 is Critical-only per the convergence directive (the bot's own
ledger is past its round cap).
2026-08-18 09:19:52 +00:00