Commit graph

1499 commits

Author SHA1 Message Date
samuelhsin
e19456b65b
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-22 15:02:29 +08: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
samuelhsin
7992505dda
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-22 10:15:08 +08: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
samuelhsin
321a6712c3
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-21 21:03:19 +08: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
samuelhsin
9d3b31f7b2
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-21 18:00:46 +08: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
samuelhsin
08c35b3c66
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-21 13:27:32 +08: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
samuelhsin
fffeb18bd7
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-21 03:25:10 +08: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
samuelhsin
c8c554c71d
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-20 19:51:03 +08: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
samuelhsin
7fcc3bedb9
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-20 14:44:42 +08: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
samuelhsin
4426dad81d
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-20 14:20:40 +08: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
samuelhsin
acc4a63eec
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-20 13:10:28 +08: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
samuelhsin
f1a03e4928
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 23:10:58 +08: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
samuelhsin
33c2f8ee98
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 20:23:51 +08: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
samuelhsin
fd85efe474
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 17:46:43 +08: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
samuelhsin
83ad1eec30
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 16:10:17 +08: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
samuelhsin
fdecc02830
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 13:45:54 +08: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
samuelhsin
37cf8be24c
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 12:45:42 +08:00