feat(core): bind PRs created via gh pr create in the session shell - #9739
Conversation
Legacy sessions predate the PR-binding feature, so the sidebar had no way to answer 'which session produced PR N'. An on-demand route scans every trusted workspace's persisted sessions, resolves PR numbers from the worktree slug/branch convention and from transcript gitBranch x gh headRefName intersections (the dominant source in practice), and writes the existing .pr.json sidecars. Bound PRs now carry a state snapshot (open/merged/closed) that a 5-minute daemon sweep advances via a slim gh pr list --state all query, and the sidebar badge dims merged PRs while the tooltip names merged/closed ones.
run-qwen-serve is a pre-listen bundle root whose static closure must stay free of the SessionService chain (glob et al.). Loading session-pr-refresh statically pulled that chain in; a dynamic import() of the core barrel from inside the refresh module was worse — it made the barrel's full namespace live and poisoned the shared chunk for every static barrel importer (ACP agent included). Load the whole refresh module through a dynamic import at timer start instead, guarded by a generation counter against dispose races.
The shell tool now recognizes a completed gh pr create (excluding --dry-run) by the PR URL gh prints on success and writes the session's PR sidecar directly with state open, mirroring the worktree sidecar pattern (CLI and daemon modes alike). The backfill route gains the same detector as a third, retroactive source, pairing run_shell_command calls with their responses in persisted transcripts so sessions predating the live hook bind too.
|
Re-run of triage on the current head (after the round-27 fix and the second main merge).
Moving on to code review. 🔍 中文说明对当前 head(第 27 轮修复与二次合并 main 之后)重跑 triage。
进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
Code review (full pass over the production diff)No blockers found. This has been through 27 review rounds and it shows — the adversarial cases I looked for are already pinned by negative tests. What I verified in the current head:
Two non-blocking observations:
The changed-files map and the live-binding flow: sequenceDiagram
participant P1 as Command
participant P2 as ShellToolInvocation
participant P3 as gh and git
participant P4 as PR sidecar
participant P5 as Bridge
participant P6 as Web Shell client
P2->>P3: pre-run snapshot - branch PR, branch name, repo keys
P2->>P1: run the command
P1-->>P2: exit 0 with output
P2->>P3: post-run gh pr view for the branch
P3-->>P2: open PR with url
P2->>P2: gates - new number, repo and branch identity, url in output
P2->>P4: upsertSessionPrs, source create, cross-process lock
P2->>P5: pr-binding notification
P5->>P6: catalog mark, client refetches in about 2s
Files changed (30 of 39 shown)
Testing — the PR's own CI on the reviewed commitAll four
Sandboxed verification is already settling the one claim CI cannot: the end-to-end live flow (a real 中文说明代码审查(生产代码 diff 全量过了一遍)未发现阻断项。这个 PR 经历了 27 轮评审,效果显而易见——我去找的攻击面大多已有负向测试钉住。在当前 head 上核实的内容:
两条非阻断观察:
测试——被审提交上的 PR 自身 CI四个 沙箱验证正在落定 CI 无法覆盖的那一项主张:端到端实时流程(真实 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean across every stage; the reservations are non-blocking (one small reuse nit, and the end-to-end live flow rests on the sandboxed verification still in flight plus the author's macOS testing, not this review). Stepping back: this PR started as "detect The standing Approving, pinned to the reviewed commit. The sandboxed A/B verification report will follow on this thread when it lands — worth a read before merge, but from where I sit this is ready. 中文说明置信度:4/5 —— 各阶段均干净;保留意见都是非阻断的(一条小的复用建议,以及端到端实时流程依赖仍在进行的沙箱验证与作者的 macOS 自测,而非本次评审)。 退一步看:这个 PR 从"检测 当前挂着的 予以批准,钉在所审提交上。沙箱 A/B 验证报告会随后贴在本帖——合并前值得一读,但就我目前所见,这个 PR 已经就绪。 — Qwen Code · qwen3.8-max Reviewed at |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
process-env-guard scans serve sources for process.env reads; register QWEN_SESSION_PR_REFRESH_MINUTES as a documented process-scoped switch. GitDialog now sends state 'open' with the binding, so the dialog tests assert it.
A search command whose arguments mention 'gh pr create' (e.g. grep) paired with a PR-looking URL in its output would bind a bogus entry; require the phrase to start a command segment (env-prefixed and piped forms still count). Summarized output also elides owner/repo (github.com/.../pull/N) — such URLs are not usable link targets and are rejected.
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
— qwen3.8-max via Qwen Code /review (v0.22.0)
Review round 2 for the session PR binding feature: - Bind the LAST printed /pull/N URL instead of the first, and require it to belong to the workspace repository (host/owner/repo vs the origin remote); the live shell hook additionally requires exit code 0 and skips when the remote cannot be resolved. Closes forged/foreign URL persistence via compound commands. - Backfill inserts bindings in ascending authority (branch-mapped first, gh-pr-create evidence next, worktree convention last) so the session's own PR survives the tail-10 cap, and maps a shared head branch to the newest PR (slim query now requests updatedAt; first-wins mapping). - Reject pr-0 and leading-zero worktree slugs — number 0 poisoned the whole sidecar read. - Refresh sweep only stamps states onto bindings whose URL belongs to the queried repository, and counts entries actually rewritten. - Remote resolution is async with a bounded timeout and attempted once per run; normalizeRemoteToWebUrl moved to core, drops ssh:// ports, and accepts any scp-style user.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Review round summary — PR #9739 (commit
|
| Finding | Disposition | Change |
|---|---|---|
[Critical] R1-2 (rc:3836860789) — detector binds the FIRST /pull/N URL anywhere, no exit-code gate, no attribution |
Fixed | detectGhPrCreateBinding now binds the LAST matching URL and, when given a repo key, only URLs whose host/owner/repo matches it. Live shell hook additionally requires result.exitCode === 0 and resolves the workspace origin remote (fetchRemoteWebUrl) — an unresolvable remote binds nothing. Backfill applies the same repo gate to transcript-recovered URLs. |
| [Critical] R1-3 (rc:3836860792) — insertion order evicts the most authoritative binding first | Fixed | Backfill now inserts in ascending authority: branch-mapped → gh pr create evidence → worktree slug/branch convention last, so the session's own PR survives the tail-10 cap. |
| [Critical] R1-4 (rc:3836860793) — slim field set breaks newest-first ordering; last-write branch map resolves to the oldest PR | Fixed | Both halves: updatedAt added to GH_PR_LIST_FIELDS_SLIM (restores the newest-first sort) and branchToNumber is now first-wins, so a shared head branch maps to the newest PR. |
[Critical] R1-5 (rc:3836860795) — pr-0 slug binds number 0, poisoning the whole sidecar read |
Fixed | Both patterns are now [1-9]\d{0,8} (mirrors parsePRReference's n > 0 invariant; leading zeros rejected too). A pr-0 worktree session binds nothing. |
| [Critical] R1-6 (rc:3836860796) — sweep stamps states by bare number across repositories | Fixed | The sweep resolves the workspace repo key and only stamps bindings whose URL belongs to it; foreign-repo bindings are skipped, and an unresolvable remote updates nothing (fail closed, no gh call). |
[Suggestion] R1-11 (rc:3836860805) — blocking execSync remote lookup, retried per candidate |
Fixed | Remote resolution moved to core fetchRemoteWebUrl (async execFile, 5s timeout) and cached by attempt — one lookup per backfill run even when it fails. |
[Suggestion] R1-19 (rc:3836860817) — ssh:// port kept in badge links; non-git@ scp remotes rejected |
Fixed | normalizeRemoteToWebUrl (moved to core) reassembles with url.hostname (port dropped) and accepts any [user@]host:path scp-style remote. Regression tests added for ssh://git@host:2222/o/r.git and jdoe@host:o/r.git. |
[Suggestion] R1-20 (rc:3836860818) — updated counts gh-confirmed numbers, not rewrites |
Fixed | updateSessionPrStates returns the count of entries actually rewritten; the sweep accumulates it. Two-binding test asserts updated: 1 when gh confirms one change. |
The design doc section describing the shell post-hook was updated to match the hardened semantics.
Deferred to the next round (explicit thread replies posted)
- [Critical] R1-1 (rc:3836860788) — live binding never notifies the bridge, and [Critical] R1-7 (rc:3836860798) — cross-process sidecar writer race. These two share one root-cause fix and are intentionally tackled together: route the child-detected binding over the existing
qwen/notify/*extNotification side-channel (title-update precedent) into a daemon-side handler that performs the GitDialog sequence (seedSessionPrs→bridge.updateSessionMetadata→upsertSessionPr), making the daemon the sole serialized writer and bumping the catalog revision. This spanscore(sink on Config),cli(ACP Session wiring), andacp-bridge(demux + handler) and exceeds this round's budget; the fallback for R1-7 alone is a two-tierproper-lockfileguard (mailbox.ts convention; dependency already present). - Suggestions R1-8, R1-9, R1-10, R1-12, R1-13, R1-14, R1-15, R1-16, R1-17, R1-18, R1-21, R1-22, R1-23 — deferred per the per-round batch bound; each has a reply recording the deferral. R1-23's surviving-mutant list (alreadyBound read-back, out-of-alignment transcript pairing, effectiveEnv fixture,
toHaveBeenCalledWitharg pins, untrusted-primary fixture) is the test plan for those fixes. - rv:5000918652 (review body) — its actionable content is the inline findings above; the disclosed gaps (integration suite not run locally, reverse-audit cap) are process notes, addressed here by the focused verification below.
- ic:5381304346 (web-shell visual preview) — "one or more scenarios failed to render" against the CI mock daemon; no code-level evidence of a defect was available (linked artifacts are CI-side). The badge/merged-state wiring was independently audited and is covered by
SessionPrBadge.test.tsx. If the preview keeps failing on the next head, its workflow artifacts are the place to look. - ic:5381345419 (serve A/B) — passed, no action.
Verification
Commands actually run this round (post-fix, at commit bd968f2b23):
npm run build— passed (also re-run after formatting; 0 errors)npm run typecheck— passed (0 TS errors)npm run lint— passednpx prettier --checkon all 10 changed code files — passed (after--writeon 5)cd packages/core && npx vitest run src/services/session-pr-service.test.ts src/utils/github-prs.test.ts src/tools/shell.test.ts— 384 passed (incl. 5 shell binding tests, 10 detector tests, 20 new helper tests)cd packages/core && npx vitest run src/services/sessionService.test.ts src/utils/atomicFileWrite.test.ts(adjacent suites) — passedcd packages/cli && npx vitest run src/serve/routes/session-pr-backfill.test.ts src/serve/server/session-pr-refresh.test.ts src/serve/server.test.ts src/serve/process-env-guard.test.ts— 1112 passed (20 backfill + 11 refresh tests incl. 8 new)cd packages/web-shell && npx vitest run client/components/SessionPrBadge.test.tsx client/components/dialogs/GitDialog.test.tsx— 18 passed
Mutation probes (each fix's witness verified red on pre-round code, then restored green):
- R1-2 detector reverted to first-match/no-key → 3 detector tests fail
- R1-2 exit-code gate removed → "does not bind a non-zero exit even when the output carries a URL" fails
- R1-2 backfill repo gate removed → "does not recover a transcript URL from another repository" fails
- R1-3 order reverted → "keeps the convention binding when weaker numbers overflow the cap" fails
- R1-4 last-wins mapping restored → "maps a shared head branch to the newest PR" fails
- R1-5 patterns reverted to
\d{1,9}→ both zero/leading-zero tests fail - R1-6 scoping removed → "never stamps a same-number PR of another repository" fails
- R1-11 attempt cache removed → "resolves the git remote at most once per backfill run" fails
- R1-19 normalization reverted (port kept,
git@-only) → both new remote-shape tests fail - R1-20
states.sizecounting restored → "counts only bindings whose state was actually rewritten" fails
Integration tests were not run: every changed behavior is exercised by the focused Vitest suites above (route tests use supertest; the sweep/backfill are tested through their public functions), not only through the bundled CLI.
中文说明
审查轮次总结 — PR #9739(提交 bd968f2b23)
本轮实现了全部五个有界的 Critical 发现(R1-2 … R1-6),以及与它们纠缠在一起的三个 Suggestion(R1-11、R1-19、R1-20)。每一项都附带回归测试,并已验证该测试在轮次前代码上会失败(见下方变异探针)。按照每轮约 8 个发现的上限,其余发现推迟到下一轮,并在各自线程中留下了明确的回复(comment-replies.json)。
已在代码中解决
| 发现 | 处置 | 变更 |
|---|---|---|
[Critical] R1-2(rc:3836860789)——检测器绑定输出中任意位置的第一个 /pull/N URL,无退出码闸门、无归属判定 |
已修复 | detectGhPrCreateBinding 现在绑定最后一个匹配 URL,且在给定仓库 key 时只接受 host/owner/repo 与之匹配的 URL。实时 shell hook 额外要求 result.exitCode === 0,并解析工作区 origin remote(fetchRemoteWebUrl)——remote 无法解析时不做任何绑定。回填对 transcript 恢复出的 URL 应用同样的仓库闸门。 |
| [Critical] R1-3(rc:3836860792)——插入顺序导致最权威的绑定最先被挤出 | 已修复 | 回填现在按权威性升序插入:分支映射 → gh pr create 证据 → worktree slug/分支约定最后,确保会话自身的 PR 在尾部 10 个上限下幸存。 |
| [Critical] R1-4(rc:3836860793)——slim 字段集破坏"最新优先"排序;后写覆盖的分支映射解析到最老 PR | 已修复 | 两步都做:GH_PR_LIST_FIELDS_SLIM 增加 updatedAt(恢复最新优先排序),且 branchToNumber 改为先到优先,使共享 head 分支映射到最新 PR。 |
[Critical] R1-5(rc:3836860795)——pr-0 slug 绑定数字 0,毒害整个 sidecar 读取 |
已修复 | 两个模式均改为 [1-9]\d{0,8}(对齐 parsePRReference 的 n > 0 不变量;同时拒绝前导零)。pr-0 worktree 会话不再产生任何绑定。 |
| [Critical] R1-6(rc:3836860796)——扫描按裸 PR 号跨仓库盖状态 | 已修复 | 扫描解析工作区仓库 key,只对 URL 属于该仓库的绑定盖状态;外部仓库绑定直接跳过;remote 无法解析时本轮不更新任何内容(失败即关闭,不发 gh 调用)。 |
[Suggestion] R1-11(rc:3836860805)——阻塞式 execSync remote 查询,且按候选重试 |
已修复 | remote 解析移入 core fetchRemoteWebUrl(异步 execFile,5 秒超时),并按尝试缓存——即使失败,每次回填运行也只查询一次。 |
[Suggestion] R1-19(rc:3836860817)——ssh:// 端口保留进 badge 链接;非 git@ 的 scp remote 被拒绝 |
已修复 | normalizeRemoteToWebUrl(移入 core)用 url.hostname 重组(丢弃端口),并接受任意 [user@]host:path scp 风格 remote。为 ssh://git@host:2222/o/r.git 与 jdoe@host:o/r.git 补充回归测试。 |
[Suggestion] R1-20(rc:3836860818)——updated 计的是 gh 页面确认过的数字,而非实际重写数 |
已修复 | updateSessionPrStates 返回实际被重写的条目数;扫描累加该值。双绑定测试断言 gh 恰好确认一条变化时 updated: 1。 |
设计文档中描述 shell post-hook 的章节已同步更新为加固后的语义。
推迟到下一轮(线程中已有明确回复)
- [Critical] R1-1(rc:3836860788)——实时绑定从不通知 bridge;[Critical] R1-7(rc:3836860798)——sidecar 跨进程写竞态。这两者共享同一个根因修复,有意放在一起处理:把子进程检测到的绑定经现有
qwen/notify/*extNotification 侧信道(title-update 先例)路由到 daemon 侧处理器,执行 GitDialog 的完整序列(seedSessionPrs→bridge.updateSessionMetadata→upsertSessionPr),让 daemon 成为唯一的串行写入方并 bump catalog revision。该方案横跨core(Config 上的 sink)、cli(ACP Session 接线)、acp-bridge(demux + 处理器),超出本轮预算;R1-7 单独的兜底方案是两层proper-lockfile守卫(mailbox.ts 约定;依赖已存在)。 - Suggestion 类 R1-8、R1-9、R1-10、R1-12、R1-13、R1-14、R1-15、R1-16、R1-17、R1-18、R1-21、R1-22、R1-23——按每轮批量上限推迟;每项都有记录推迟原因的回复。R1-23 的存活变异体清单(alreadyBound 回读、错位配对的 transcript、effectiveEnv fixture、
toHaveBeenCalledWith参数钉死、不可信 primary fixture)就是那些修复的测试计划。 - rv:5000918652(审查正文)——其可执行内容即上述 inline 发现;披露的缺口(集成套件未本地运行、反向审计达到轮数上限)属于流程说明,已由下方的聚焦验证覆盖。
- ic:5381304346(web-shell 可视化预览)——"一个或多个场景渲染失败"(CI mock daemon 环境);没有可用的代码级缺陷证据(链接产物在 CI 侧)。badge/merged 状态接线已独立审计,且由
SessionPrBadge.test.tsx覆盖。若下个 head 预览仍失败,应查看其工作流产物。 - ic:5381345419(serve A/B)——通过,无需处理。
验证
本轮实际执行的命令(修复后,提交 bd968f2b23):
npm run build— 通过(格式化后再次执行;0 错误)npm run typecheck— 通过(0 个 TS 错误)npm run lint— 通过- 对全部 10 个改动的代码文件执行
npx prettier --check— 通过(其中 5 个先--write) cd packages/core && npx vitest run src/services/session-pr-service.test.ts src/utils/github-prs.test.ts src/tools/shell.test.ts— 384 通过(含 5 个 shell 绑定测试、10 个检测器测试、20 个新 helper 测试)cd packages/core && npx vitest run src/services/sessionService.test.ts src/utils/atomicFileWrite.test.ts(相邻套件)— 通过cd packages/cli && npx vitest run src/serve/routes/session-pr-backfill.test.ts src/serve/server/session-pr-refresh.test.ts src/serve/server.test.ts src/serve/process-env-guard.test.ts— 1112 通过(20 个回填 + 11 个刷新测试,含 8 个新增)cd packages/web-shell && npx vitest run client/components/SessionPrBadge.test.tsx client/components/dialogs/GitDialog.test.tsx— 18 通过
变异探针(每个修复的见证测试均已验证:轮次前代码上为红,恢复后为绿):
- R1-2 检测器回退为首个匹配/无 key → 3 个检测器测试失败
- 移除 R1-2 退出码闸门 → "退出码非 0 即使输出带 URL 也不绑定"失败
- 移除 R1-2 回填仓库闸门 → "不恢复来自其他仓库的 transcript URL"失败
- R1-3 顺序回退 → "弱编号溢出上限时保留约定绑定"失败
- R1-4 恢复后写覆盖映射 → "共享 head 分支映射到最新 PR"失败
- R1-5 模式回退为
\d{1,9}→ 两个 zero/前导零测试失败 - 移除 R1-6 仓库限定 → "不给其他仓库的同号码 PR 盖状态"失败
- 移除 R1-11 尝试缓存 → "每次回填运行至多解析一次 git remote"失败
- R1-19 归一化回退(保留端口、仅
git@)→ 两个新 remote 形态测试失败 - R1-20 恢复
states.size计数 → "只统计状态实际被重写的绑定"失败
未运行集成测试:所有变更行为均由上述聚焦 Vitest 套件覆盖(路由测试使用 supertest;扫描/回填经公共函数测试),并非只能通过打包后的 CLI 验证。
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。
Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。
🧠 Handled by Qwen Code · model/模型 qwen3.8-max
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
6 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- timer trust-skip/reentrancy/dispose untested (f7) — named in R1-23 (re-posted on the PR)
- core state validation clause negative test missing (f8) — named in R1-23 (re-posted on the PR)
- draft→open guards untested in both consumers (f9) — named in R1-23 (re-posted on the PR)
- !result.aborted conjunct unexercised (f10) — named in R1-23 (re-posted on the PR)
- backfill gh call-arguments unasserted (g5) — named in R1-23 item (4) (re-posted on the PR)
- sweep archived-state branch untested (g7) — named in R1-23 (re-posted on the PR)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — stopped at the reverse-audit round cap of 5 without converging.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/core/src/tools/shell.ts:3076 — [review] Remote resolved before the cheap detection gate — every exit-0 command pays a git walk + spawnpackages/cli/src/serve/routes/session-pr-backfill.test.ts:653 — [review] Route aggregation test asserts neither the trusted entry nor non-zero reduce totalspackages/cli/src/serve/run-qwen-serve.ts:5164 — [probe] Sweep-starting dynamic import has no .catch — uncaught exception (daemon crash) on the serve fast pathpackages/cli/src/serve/routes/session-pr-backfill.test.ts:66 — [probe] pr() helper hardcodes state:'open' — merged/closed snapshot write path unpinned (flattening mutant 20/20 green)packages/cli/src/serve/server/session-pr-refresh.test.ts:179 — [probe] All-merged fast-path test cannot pin early-return-before-remote-resolution (hoisting mutant 11/11 green)packages/cli/src/serve/server/session-pr-refresh.test.ts:211 — [probe] No malformed-sidecar resilience test although R2-2's poisoning chain makes invalid sidecars reachablepackages/cli/src/serve/routes/session-pr-backfill.ts:142 — [probe] Backfill runs the detector unkeyed — a trailing foreign URL after gh's own loses a binding the live keyed path would makepackages/cli/src/serve/server/session-pr-refresh.test.ts:59 — [probe] Negative-minutes guard untested — deleting it keeps the suite green and yields the 1 ms loop shapepackages/cli/src/serve/server/session-pr-refresh.test.ts:160 — [probe] gh call count unpinned — a per-session-call mutant keeps 11/11 green (probe: committed 1 call vs mutant 2)docs/design/2026-08-20-webshell-session-pr-binding.md:65 — [review] Design doc contradicts itself and the code on the slim field set (says three fields; constant has five)packages/cli/src/serve/server/session-pr-refresh.test.ts:181 — [probe] Multi-target update loop never exercised past the first entry — break-after-first mutant 11/11 greenpackages/core/src/services/session-pr-service.ts:163 — [probe] --dry-run substring gate suppresses a real create when the flag appears in a quoted title/body or a later segment
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 6 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:reverse audit — stopped at the reverse-audit round cap of 5 without converging。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 12 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — PR #9739 (review round 3)Growth auditVerdict: drift ( Feedback dispositions
R2-1 — detector attribution class (fixed)Reproduced all five entrances against the committed code (probe output: failed exit-0 compound → binds 1234; supersede echo → binds 42 instead of created 100; quoted phrase / multi-line Fix — gh is the attribution authority:
Witnesses: new shell tests (attribution positive, corner-A flip, three forged shapes, R2-2 — write-time validation (fixed)Reproduced on committed code: a 2130-char same-repo URL and an ESC-bearing URL persisted through R2-3 — cross-run convention eviction (fixed)Fix: in the backfill loop, an already-bound number with convention authority is re-upserted (moved to the end with a fresh R2-5 — fork-layout gh-list gate (fixed)Fix: while building R1-1 — badge never appears in live-state workspaces (fixed)The child persists the sidecar, then notifies the daemon via a new R1-7 — cross-process sidecar races (fixed)Fix: every sidecar mutation ( Deferred suggestions (13)Batch cap (~8 findings/round, Critical first) deferred R2-6, R2-7, R2-8, R2-9, R1-8, R1-10, R1-12, R1-13, R1-14, R1-15, R1-16, R1-21, R1-23 to the next round; each has a reply on its own thread ( Notes
VerificationCommands actually run this round (results):
中文说明Autofix 轮次总结 — PR #9739(评审第 3 轮)增长审计结论:drift(见 反馈处置
R2-1 — 检测器归因类问题(已修复)已对提交代码实测复现全部五个入口(探针输出:退出码为 0 的失败复合命令 → 绑定 1234;覆盖式 echo → 实际创建 100 却绑定 42;引号内短语 / 多行 修复——以 gh 为归因权威:
见证:新增 shell 测试(归因正向、corner A 翻转、三种伪造形态、 R2-2 — 写入时校验(已修复)已在提交代码上复现:2130 字符同仓库 URL 与含 ESC 的 URL 均可经 R2-3 — 跨运行约定号被挤出(已修复)修复:回填循环中,已绑定且属约定权威的号码仍会重新 upsert(移到末位、刷新 R2-5 — fork 布局 gh 列表闸门(已修复)修复:构建 R1-1 — live-state workspace 中 badge 永不出现(已修复)子进程持久化 sidecar 后,经新增的 R1-7 — 跨进程 sidecar 竞态(已修复)修复:所有 sidecar 变更( 延后的建议(13 条)批次上限(每轮约 8 条、Critical 优先)将 R2-6、R2-7、R2-8、R2-9、R1-8、R1-10、R1-12、R1-13、R1-14、R1-15、R1-16、R1-21、R1-23 延后到下一轮;每条均已在各自线程回复( 备注
验证本轮实际执行的命令(结果):
Deferred non-Critical feedbackCritical-only mode is active: the PR's diff grew src 137 / test 450 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after 5 change-producing rounds). ( 中文说明已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 137 / 测试 450 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- state-rejection 400 message does not name the
stateconstraint (bridge + route) — already reported on the PR in round 1 (comment 3836860820, thread at routes/session.ts:2177); independently re-detected this round by three auditors
Not reviewed: reverse audit — stopped at the reverse-audit round cap of 5 without converging.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): chunk 6: executed run of packages/cli/src/serve/server/session-pr-refresh.test.ts (no node_modules/dist in review worktree; install+build not feasible in budget); chunk 3: executing the test file (no node_modules in the review worktree; npm ci plus the prerequisite npm run build for vitest's dist guard exceeded the remaining t….
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/acp-bridge/src/bridge.ts:9769 — [review] same-binding no-change check ignores statepackages/cli/src/serve/server/session-pr-refresh.ts:161 — [review] env? option on startSessionPrRefreshTimer is a dead switchpackages/acp-bridge/src/bridge.ts:9682 — [review] state validation clauses untested at all three write gatespackages/cli/src/serve/acp-http/dispatch.ts:2946 — [review] ACP update_metadata state passthrough untestedpackages/cli/src/serve/server/session-pr-refresh.ts:157 — [review] startSessionPrRefreshTimer timer lifecycle untestedpackages/cli/src/serve/routes/session-pr-backfill.ts:255 — [probe] alreadyBound+unresolved double count for unresolvable convention numberspackages/cli/src/serve/routes/session-pr-backfill.ts:117 — [review] backfill pagination loop untestedpackages/cli/src/serve/server/session-pr-refresh.test.ts:179 — [probe] sweep merged-skip test vacuouspackages/cli/src/serve/server/session-pr-refresh.test.ts:113 — [probe] sweep archived half untestedpackages/web-shell/client/components/sidebar/SessionDetailsTooltip.tsx:171 — [probe] tooltip state suffix untestedpackages/cli/src/serve/routes/session-pr-backfill.test.ts:203 — [probe] backfill gh query contract unpinnedpackages/cli/src/serve/routes/session-pr-backfill.test.ts:280 — [probe] non-convention alreadyBound skip untestedpackages/cli/src/serve/server/session-pr-refresh.test.ts:113 — [probe] sweep pagination loop untestedpackages/cli/src/serve/routes/session-pr-backfill.test.ts:280 — [review] 'without rewriting the sidecar' test name contradicts behaviorpackages/cli/src/serve/routes/session-pr-backfill.ts:286 — [review] backfill/sweep iterate listAll() including internal live-conversation runtimespackages/cli/src/serve/server/session-pr-refresh.test.ts:46 — [review] draft→open normalization unpinnedpackages/cli/src/serve/server/session-pr-refresh.test.ts:160 — [review] one-gh-call-per-workspace property unpinnedpackages/web-shell/client/components/SessionPrBadge.test.tsx:50 — [review] badge tests never pin the base layout classpackages/cli/src/serve/routes/session-pr-backfill.test.ts:733 — [probe] route aggregation test never asserts the trusted entrypackages/cli/src/serve/server/session-pr-refresh.ts:88 — [probe] sweep read-failure isolation guard unpinned- …and 2 more (see the run report)
Convergence: round 3 posted 25 inline comment(s), 14 of them reported for the first time; the previous round posted 20 (9 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/session-pr-backfill.ts (findings in rounds 1, 2; 5 more now); packages/cli/src/serve/routes/session-pr-backfill.test.ts (findings in rounds 1, 2; 2 more now); packages/core/src/tools/shell.ts (findings in rounds 1, 2; 2 more now), and 2 more file(s). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had none either, so the next review re-reads the whole diff and will keep doing so until a round's marker carries an anchor again. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:reverse audit — stopped at the reverse-audit round cap of 5 without converging。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):chunk 6:executed run of packages/cli/src/serve/server/session-pr-refresh.test.ts (no node_modules/dist in review worktree; install+build not feasible in budget);chunk 3:executing the test file (no node_modules in the review worktree; npm ci plus the prerequisite npm run build for vitest's dist guard exceeded the remaining t…。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 22 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 25 条行内评论,其中 14 条是首次提出;上一轮发布了 20 条(其中 9 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/routes/session-pr-backfill.ts(第 1、2 轮已出过发现,本轮又有 5 条);packages/cli/src/serve/routes/session-pr-backfill.test.ts(第 1、2 轮已出过发现,本轮又有 2 条);packages/core/src/tools/shell.ts(第 1、2 轮已出过发现,本轮又有 2 条),另有 2 个文件。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
… execution cwd Review round 3 for the session PR binding feature: - Backfill and refresh sweep reject transcript-sourced sessionIds that fail isValidSessionId before any sidecar path construction — a planted transcript's first record could otherwise read/write sidecars at an attacker-chosen path (both consumers share the sink). - Backfill fails CLOSED when the workspace repo key is unknown: gh may resolve a default repo that is not the workspace's, so its page must not feed branch mapping (the convention fallback is already disabled in that state). - Backfill stops re-upserting already-bound numbers on every run — that moved convention entries to the end with a fresh createdAt, violating the binding-time order the badge/tooltip render by. An already-bound convention number is restored only when this run's new bindings actually evicted it past the tail cap. - The refresh sweep accepts the repository gh actually queried (the page's own URLs name it) in addition to the origin key — fork-layout bindings carry parent-repo URLs and were frozen at 'open' forever. - The backfill route bumps the bridge session-catalog revision when new bindings were written, so live-state clients refetch instead of waiting for unrelated catalog churn. - The shell binder attributes `gh pr create` against the execution directory (params.directory), not the target dir — a directory parameter pointing at another workspace otherwise bound nothing or a wrong-repo PR.
…er lend their URL On an Aone workspace the `/pull/<N>` capture can only match the fabricated own-remote shape the pre-Aone backfill persisted (real MR links are codereview/<id>), and the two-segment repo key collapses nested groups so a sibling project's form passed the gate. A form is now admitted only when it is exactly this workspace's fabricated shape (full-path equality via isLegacyFabricated) and supplies the number only — the URL always comes from mr view, so a dead /pull/ page is never persisted and nothing needs repairing later (R22-1, R22-2).
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 4)": the scope-boundary bullet's claim that fork-PR worktree removal is declined by the pre-existing hasUnmergedWorktreeCommits guard (unchanged core worktree code….
11 Suggestion(s) were drafted inline past the resolved critical posting floor; the CLI moved them into the deferral list below (floor enforcement).
Deferred under the convergence posture (round 23, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:
packages/cli/src/serve/routes/session-pr-backfill.ts:146 — [probe] Critical [fails-closed] [new-surface] D23-1 REVIEW_COMMAND_PATTERN backtracks quadratically on a spaceless persisted prompt full of https:// anchors without /pull/, stalling…packages/cli/src/serve/routes/session-pr-backfill.ts:620 — [review] R23-2: The isValidSessionPrUrl shape-check disjunct this diff added to URL resolution has a witness only for the /review <url> form leg; the remote-fallback leg — ${re…packages/cli/src/serve/routes/session-pr-backfill.test.ts:1059 — [review] R23-3: This diff deletes strips repo-shifting env when resolving the git remote fallback — the only witness that origin-web-URL resolution strips repo-shifting env.…docs/design/2026-08-20-webshell-session-pr-binding.md:39 — [review] R23-6: The new post-hook bullet attributes the retry-misbinding defense to the OPEN-state gate, but the open gate only declines retries resolving a MERGED/CLOSED PR; a retr…docs/design/2026-08-20-webshell-session-pr-binding.md:113 — [review] R23-7: The rewritten scope-boundary bullet says sessions with no worktree sidecar and no /review command are not covered ("回填无可靠来源,不覆盖"), but on Aone such sessions ARE c…packages/cli/src/serve/routes/session-pr-backfill.ts:692 — [review] R23-8: The added ?? urls.get(entry.number) fallback in plannedFor is provably unreachable: the URL-resolution loop populates urls only after if (existingNumbers.has(…packages/cli/src/serve/routes/session-pr-backfill.test.ts:427 — [review] R23-9: No test exercises the Aone no-lend short-circuit ( if (resolveAoneUrl) return undefined; in lendableFormUrl ) — the Aone describe never combines a bare /revi…packages/cli/src/serve/routes/session-pr-backfill.test.ts:981 — [review] R23-10: The deleted ignores gitBranch keys nested inside structured record values test was the suite's only malformed-transcript-line witness (it seeded a non-JSON l…docs/design/2026-08-20-webshell-session-pr-binding.md:75 — [review] R23-13: The rewritten trim bullet claims entries not re-offered OR not re-resolvable this run become foreign occupants that take slots first ("本轮未再提供、或本轮无法解析的既有条目视为外来占位者先占槽…packages/cli/src/serve/routes/session-pr-backfill.test.ts:2334 — [review] R23-14: ignores /review mentions outside user text records does not isolate the record-type gate: the sole assistant-record fixture places /review 55 mid-prose, w…docs/design/2026-08-20-webshell-session-pr-binding.md:65 — [review] R23-16: The source-1 bullet's parenthetical claims a /review <url> form must be the workspace's own repo ("URL 形态须与 workspace 同仓库"), but admission ( allowedRepoKeys , ses…packages/cli/src/serve/routes/session-pr-backfill.ts:359 — [review] R23-19: The added comment asserts the remote fallback is disabled whenever the workspace repo key is unknown ("the remote fallback is already disabled in that state"), but …packages/cli/src/serve/routes/session-pr-backfill.ts:329 — [review] D23-2 dead BackfillCandidate.transcriptPath field: set on every candidate but never read; its JSDoc falsely describes the resurrect guard, which now uses the write-time re-…packages/cli/src/serve/server/aone-mrs.ts:1 — [review] D23-3 aone-mrs module header still claims mr list feeds session-pr-backfill/session-pr-refresh and mentions 'skip branch mapping' — both false at HEAD after this PR removed the last pro…
Convergence: round 23 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/session-pr-backfill.ts (findings in round 22; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
[Critical] R22-3 Still standing (originally reported round 18 at packages/cli/src/serve/acp-http/dispatch.ts:3082; not re-anchorable in this round's incremental diff). updateSessionMetadata merges the live entry positionally (.slice(-SESSION_PR_LIST_LIMIT)) and publishes session_metadata_updated carrying that list BEFORE the setSessionPrs reconciliation runs — on the ACP dispatch path and both REST metadata routes — while upsertSessionPr caps by provenance authority. Past the cap the two stores evict different entries, so the same mutation broadcasts the diverged list this reconcile exists to eliminate; setSessionPrs publishes nothing and bumps no catalog revision, so event consumers keep the list missing the session's created binding until unrelated catalog churn. Fix constraint: packages/cli/src/serve/routes/session.ts:5776-5780 — the sidecar write must not move ahead of the bridge mutation, so the corrective event belongs after upsertSessionPr/setSessionPrs. Fix witness: extend the replaces this-daemon-lifetime bindings on setSessionPrs suite in packages/acp-bridge/src/bridge.test.ts (~line 27783) — with a capped mixed-provenance live list, bind via updateSessionMetadata, then call setSessionPrs with the authority-capped list and assert the last session_metadata_updated event's prs equals the reconciled list; removing the publish turns it red.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 4)":the scope-boundary bullet's claim that fork-PR worktree removal is declined by the pre-existing hasUnmergedWorktreeCommits guard (unchanged core worktree code…。
11 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论;CLI 已将其移入下方延后清单(下限强制执行)。
收敛姿态下延后(第 23 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 14 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 23 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/routes/session-pr-backfill.ts(第 22 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
[Critical] R22-3 Still standing (originally reported round 18 at packages/cli/src/serve/acp-http/dispatch.ts:3082; not re-anchorable in this round's incremental diff). updateSessionMetadata merges the live entry positionally (.slice(-SESSION_PR_LIST_LIMIT)) and publishes session_metadata_updated carrying that list BEFORE the setSessionPrs reconciliation runs — on the ACP dispatch path and both REST metadata routes — while upsertSessionPr caps by provenance authority. Past the cap the two stores evict different entries, so the same mutation broadcasts the diverged list this reconcile exists to eliminate; setSessionPrs publishes nothing and bumps no catalog revision, so event consumers keep the list missing the session's created binding until unrelated catalog churn. Fix constraint: packages/cli/src/serve/routes/session.ts:5776-5780 — the sidecar write must not move ahead of the bridge mutation, so the corrective event belongs after upsertSessionPr/setSessionPrs. Fix witness: extend the replaces this-daemon-lifetime bindings on setSessionPrs suite in packages/acp-bridge/src/bridge.test.ts (~line 27783) — with a capped mixed-provenance live list, bind via updateSessionMetadata, then call setSessionPrs with the authority-capped list and assert the last session_metadata_updated event's prs equals the reconciled list; removing the publish turns it red.
— qwen3.8-max via Qwen Code /review (v0.22.3)
…the run outranks it The provenance-ranked trim protected a re-offered occupant at max(this-run stamp, persisted source) but wrote it back verbatim, so a `/review`-bound number the session now exists for via the pr-<N> convention stayed at review rank on disk and the next capped upsert evicted the session's own PR first. The planner now promotes such an entry in place — url, createdAt, position untouched, never downgraded, a new object so the no-op check commits — mirroring upsertSessionPrs' same-URL upgrade (R23-1). A pre-provenance convention occupant gets one migration write and is idempotent afterwards; a `/review` re-mention of a source-less entry is not an upgrade and writes nothing.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
7 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R24-1 dead BackfillCandidate.transcriptPath field with misleading doc comment — already reported (round 23 deferral list, review 5057458127, as D23-2)
- R24-2 unreachable urls.get fallback in plannedFor — already reported (round 23 deferral list, review 5057458127, as R23-8)
- R24-3 env-stripping witness deleted; fetchRemoteWebUrl spawn env untested — already reported (round 23 deferral list, review 5057458127, as R23-3)
- R24-4 remote-derived leg of the URL-validity check unwitnessed — already reported (round 23 deferral list, review 5057458127, as R23-2)
- R24-5 record-type gate witness vacuous (mid-prose assistant fixture) — already reported (round 23 deferral list, review 5057458127, as R23-14)
- R24-6 doc misattributes retry-misbind blocking to the open gate — already reported (round 23 deferral list, review 5057458127, as R23-6)
- R24-7 doc self-contradiction on the URL-form repo gate — already reported (round 23 deferral list, review 5057458127, as R23-16)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; the local build-test run also never reached the packages/cli suite (non-incremental builds exhausted the budget before any suite ran; the packages/sdk-typescript build timed out), and its test-efficacy probe was inconclusive (no green baseline). CI is red at the reviewed commit: Test (ubuntu-latest, Node 22.x), Serve A/B (ubuntu-latest, Node 22.x) and ubuntu-latest / Java 11 failing..
Deferred under the convergence posture (round 24, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/routes/session-pr-backfill.test.ts:2819 — [probe] D24-1 zero-number guard witnesses masked by the writer-side filter; the :208 urlNumber guard has no witness at all
Convergence: round 24 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 1 (1 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/session-pr-backfill.ts (findings in round 23; 1 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)
Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (2 Critical(s)), the rate of first-time findings is not falling (this round 1, previous 1), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):
| standing Critical | attack surface | attacker-dependency | blast radius |
|---|---|---|---|
| (each standing Critical) | … | … | … |
Advisory only — it does not block this review.
[Critical] R22-3 Still standing (originally reported round 18 at packages/cli/src/serve/acp-http/dispatch.ts:3082; not re-anchorable in this round's incremental diff). updateSessionMetadata merges the live entry positionally (.slice(-SESSION_PR_LIST_LIMIT)) and publishes session_metadata_updated carrying that list BEFORE the setSessionPrs reconciliation runs — on the ACP dispatch path and both REST metadata routes — while upsertSessionPr caps by provenance authority. Past the cap the two stores evict different entries, so the same mutation broadcasts the diverged list this reconcile exists to eliminate; setSessionPrs publishes nothing and bumps no catalog revision, so event consumers keep the list missing the session's created binding until unrelated catalog churn. Re-verified at HEAD f3e41ac: dispatch.ts:3036-3085 calls bridge.updateSessionMetadata (which publishes the positionally capped list at bridge.ts:10411-10431) before upsertSessionPr + setSessionPrs; both REST metadata routes do the same (session.ts:5780-5801 and 5957-5972); setSessionPrs (bridge.ts:10448-10457) publishes no event and bumps no revision. Witness: not run — this round's re-check is a code trace of the ordering at HEAD; the nearest capability, a probe driving updateSessionMetadata + setSessionPrs with a capped mixed-provenance list, already settled the divergence in the original round-18 probe quoted in the thread.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 7 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; the local build-test run also never reached the packages/cli suite (non-incremental builds exhausted the budget before any suite ran; the packages/sdk-typescript build timed out), and its test-efficacy probe was inconclusive (no green baseline). CI is red at the reviewed commit: Test (ubuntu-latest, Node 22.x), Serve A/B (ubuntu-latest, Node 22.x) and ubuntu-latest / Java 11 failing.。
收敛姿态下延后(第 24 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 24 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/routes/session-pr-backfill.ts(第 23 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)
残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 2 条 Critical),首次发现的速率没有下降(本轮 1,上一轮 1),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。
[Critical] R22-3 Still standing (originally reported round 18 at packages/cli/src/serve/acp-http/dispatch.ts:3082; not re-anchorable in this round's incremental diff). updateSessionMetadata merges the live entry positionally (.slice(-SESSION_PR_LIST_LIMIT)) and publishes session_metadata_updated carrying that list BEFORE the setSessionPrs reconciliation runs — on the ACP dispatch path and both REST metadata routes — while upsertSessionPr caps by provenance authority. Past the cap the two stores evict different entries, so the same mutation broadcasts the diverged list this reconcile exists to eliminate; setSessionPrs publishes nothing and bumps no catalog revision, so event consumers keep the list missing the session's created binding until unrelated catalog churn. Re-verified at HEAD f3e41ac: dispatch.ts:3036-3085 calls bridge.updateSessionMetadata (which publishes the positionally capped list at bridge.ts:10411-10431) before upsertSessionPr + setSessionPrs; both REST metadata routes do the same (session.ts:5780-5801 and 5957-5972); setSessionPrs (bridge.ts:10448-10457) publishes no event and bumps no revision. Witness: not run — this round's re-check is a code trace of the ordering at HEAD; the nearest capability, a probe driving updateSessionMetadata + setSessionPrs with a capped mixed-provenance list, already settled the divergence in the original round-18 probe quoted in the thread.
— qwen3.8-max via Qwen Code /review (v0.22.3)
Plan membership inherits plannedFor's same-PR check, which fails open on GitHub when the number cannot be resolved this run — fine for the reversible trim, not for a permanent provenance stamp: a foreign same-numbered occupant at the convention number would have become the session's highest-authority binding. The promotion now requires attested identity, the bar upsertSessionPrs sets for its upgrade: the entry's canonical url is the one gh resolved for the number, or this workspace's own <remote>/pull/<N> shape (a url names its repository by path, so a pre-provenance convention binding still migrates offline), or on Aone the exact detailUrl shape. Anything else stays untouched until a later run can attest it (R23-1 follow-up).
…eate Resolves sessionService with QwenLM#10300 (cleanup ownership): main's moveArchiveSidecars helper and assertCleanupOwned naming are kept, and the PR sidecar leg inside it runs the locked moveSessionPrSidecar this branch introduced instead of the unlocked private movePrSidecar (which does not come back). The ownership tests written against the private method now assert the fence is handed to the locked move and that a SessionWriterLostError rejection from it is not downgraded to a warning; the split-pair merge cases stay pinned in session-pr-service.test.ts.
|
@qwen-code /triage |
|
Sandboxed verification: The verification job was cancelled before producing a report. 中文 — 判定:
|
|
Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs. |
|
@qwen-code /triage |
|
Sandboxed verification: The verification job was cancelled before producing a report. 中文 — 判定:
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally (the local build-test run also never reached any test suite: the shared budget was exhausted by non-incremental workspace builds before the test phase began, and the test-efficacy probe was inconclusive — no green baseline).
Not reviewed: chunk 10 — execute packages/cli/src/serve/routes/session-pr-backfill.test.ts (new backfillWorkspaceSessionPrs cases) to confirm they pass — stopped at the agent tool budget.
Not explored to full depth (tool budget reached): chunk 10: execute packages/cli/src/serve/routes/session-pr-backfill.test.ts (new backfillWorkspaceSessionPrs cases) to confirm they pass — worktree lacks node_modules and…; "agent test-matrix": none — all assigned diff ranges were read in full and every candidate was verified against the worktree before filing..
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
Deferred under the convergence posture (round 25, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/fast-path-open.test.ts:113 — [review] vi.waitFor flake fix applied to 3 of 7 identical sitespackages/cli/src/serve/routes/session-pr-backfill.ts:329 — [review] dead BackfillCandidate.transcriptPath fieldpackages/core/src/services/sessionService.ts:2461 — [review] listAllProjectSessionIds silently truncates at 10000packages/core/src/utils/github-prs.ts:28 — [review] mapEntry comment contradicts the added state fieldpackages/cli/src/serve/routes/session.ts:5807 — [review] source:'create' route stamps asserted nowherepackages/core/src/utils/github-prs.ts:490 — [review] fetchRemoteWebUrl env sanitization has no witnesspackages/cli/src/serve/routes/session-pr-backfill.ts:257 — [review] spawns run before the first assertGenerationOpen()packages/cli/src/acp-integration/session/Session.ts:8990 — [review] Session pr-bound callback link has no testpackages/core/src/services/session-pr-service.ts:723 — [review] ownership fence in moveSessionPrSidecar unwitnessedpackages/core/src/tools/shell.test.ts:297 — [review] fixed 20ms sleep hides delayed wrong bindingspackages/cli/src/serve/routes/session-pr-backfill.ts:798 — [review] fork-layout attested promotion unreachabledocs/design/2026-08-20-webshell-session-pr-binding.md:39 — [review] doc omits headRefName from the gh pr view quotedocs/design/2026-08-20-webshell-session-pr-binding.md:39 — [review] doc misattributes retry blocking to the open gatepackages/cli/src/serve/routes/session-pr-backfill.test.ts:194 — [review] orphaned origin/HEAD symref seeding in three seederspackages/cli/src/serve/routes/session-pr-backfill.ts:146 — [review] REVIEW_COMMAND_PATTERN rejects the ?review prefixpackages/core/src/tools/shell.test.ts:722 — [review] promote-scope test cannot detect a post-run legpackages/core/src/tools/shell.ts:2367 — [review] pre-spawn snapshot uninterruptible up to ~10s on cancelpackages/core/src/services/session-pr-service.ts:181 — [review] gate admits VAR=value after sudo/nohup/commandpackages/core/src/utils/github-prs.test.ts:581 — [review] spawn opts (timeout/cwd) unasserted on new sitespackages/core/src/utils/github-prs.ts:408 — [review] JSDoc says without a port; implementation keeps http(s) ports- …and 5 more (see the run report)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally (the local build-test run also never reached any test suite: the shared budget was exhausted by non-incremental workspace builds before the test phase began, and the test-efficacy probe was inconclusive — no green baseline)。
未审查:chunk 10 — execute packages/cli/src/serve/routes/session-pr-backfill.test.ts (new backfillWorkspaceSessionPrs cases) to confirm they pass — stopped at the agent tool budget。
未探索到全部深度(达到工具调用预算):chunk 10:execute packages/cli/src/serve/routes/session-pr-backfill.test.ts (new backfillWorkspaceSessionPrs cases) to confirm they pass — worktree lacks node_modules and…;"agent test-matrix":none — all assigned diff ranges were read in full and every candidate was verified against the worktree before filing.。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
收敛姿态下延后(第 25 轮,非阻断)——已记录,本轮不要求修改:共 25 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.3)
…ion changes the live pr list Past the cap the bridge's positional merge and the sidecar's provenance-ranked cap evict different entries, so the mutation's own session_metadata_updated event carried the pre-reconcile list while setSessionPrs installed the authoritative one silently — event-stream consumers kept the diverged list until unrelated catalog churn, and a revision-gated refetch landing in the bump→rewrite window cached it with no re-trigger. setSessionPrs now compares the incoming list with the live entry and, only when membership actually changed, publishes a corrective session_metadata_updated (displayName echoed, full prs) and advances the catalog revision. All reconcile call sites — both metadata routes, the ACP dispatch path, and backfill's live-entry sync — get the corrective event centrally; the matching-list case stays silent, so below-cap flows publish exactly as before (R22-3/R18-2).
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): chunk 13: none — no checks were cut short.; "agent invariant-b (packages/core/src/utils/github-prs.ts)": none — the walk completed within budget..
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
Deferred under the convergence posture (round 26, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/routes/session-pr-backfill.ts:329 — [review] dead transcriptPath field with misleading commentpackages/cli/src/serve/routes/session-pr-backfill.ts:692 — [review] plannedFor's urls fallback is unreachablepackages/cli/src/serve/routes/session-pr-backfill.ts:801 — [review] fork-parent page URLs can never attest promotionpackages/core/src/services/sessionService.ts:2458 — [review] silent 10k truncation in session enumerationpackages/cli/src/serve/acp-http/dispatch.ts:3097 — [review] route source:'create' stamp unpinnedpackages/cli/src/acp-integration/session/Session.ts:9302 — [review] Session pr-binding notification leg untestedpackages/core/src/utils/github-prs.ts:484 — [review] GIT_DIR stripping lost its spawn-level witnesspackages/core/src/services/session-pr-service.ts:347 — [review] mergeSessionPrLists authority cap unexercisedpackages/acp-bridge/src/bridge.ts:11508 — [review] setSessionPrs displayName echo unpinnedpackages/cli/src/serve/acp-http/dispatch.ts:3115 — [review] rename-only metadata replies serve stale prs listpackages/core/src/services/session-pr-service.test.ts:264 — [review] lock canonicalization witness platform-dependentpackages/core/src/services/session-pr-service.test.ts:742 — [review] batch upsert poison-URL guard untestedpackages/core/src/services/session-pr-service.ts:257 — [review] env -i misses command-established GH credentialspackages/core/src/services/sessionService.test.ts:2626 — [review] moveSessionPrSidecar ownership fence untestedpackages/core/src/tools/shell.test.ts:188 — [review] active archive arm exercised only via catchpackages/core/src/tools/shell.ts:2366 — [review] GH_REPO-redirected creates can never bind livepackages/core/src/utils/github-prs.test.ts:534 — [review] new fetcher tests never assert spawn cwdpackages/core/src/services/session-pr-service.ts:190 — [review] quote-blind split fails open on quoted separatorspackages/core/src/tools/shell.test.ts:695 — [review] promoted-scope binding test vacuouspackages/core/src/tools/shell.test.ts:895 — [review] archived re-resolution ordering unpinned- …and 1 more (see the run report)
Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (2 Critical(s)), the rate of first-time findings is not falling (this round 2, previous 0), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):
| standing Critical | attack surface | attacker-dependency | blast radius |
|---|---|---|---|
| (each standing Critical) | … | … | … |
Advisory only — it does not block this review.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):chunk 13:none — no checks were cut short.;"agent invariant-b (packages/core/src/utils/github-prs.ts)":none — the walk completed within budget.。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
收敛姿态下延后(第 26 轮,非阻断)——已记录,本轮不要求修改:共 21 条(原文未翻译,列表见上方英文部分)。
残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 2 条 Critical),首次发现的速率没有下降(本轮 2,上一轮 0),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。
— qwen3.8-max via Qwen Code /review (v0.22.3)
mergeSessionPrLists kept the freshest same-number entry wholesale — the one binding writer left that could downgrade provenance: the live shell binder stamps create on one half of a split pair, a later /review backfill re-binds the number as review on the other half, and the next archive transition's merge handed the authority cap a rank-0 create binding to evict. The dedup now carries the stronger source across a same-canonical-url pair (freshest createdAt still wins the slot; a different url is another PR and carries nothing), mirroring upsertSessionPr (R26-27).
… url forms URL-form review numbers were appended after every bare number, so the trim's same-rank position tie-break — the age proxy mirroring the sidecar cap's list-order rule — evicted the second-oldest review while the genuinely oldest (a url form typed first) survived at the newest position, permanently re-derived on every run. collectReviewedPrNumbers now emits one ordered mention stream (first occurrence wins the slot, bare-ness sticks), the plan follows it, and a number mentioned only through forms still plans only when a form passed the gate (R26-1).
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — build-cli was skipped in CI and the local packages/cli build timed out on a shrunken whole-call budget (the cli suites themselves ran green via vitest, 100/100).
Not explored to full depth (tool budget reached): chunk 7: executing session-pr-backfill.test.ts to confirm the suite is green — the review worktree has no node_modules or built workspace-package dist/ , and a full…; chunk 12: executing session-pr-service.test.ts under vitest — the shared review worktree has no node_modules installed and vitest cannot start; every test in my chunk….
Deferred under the convergence posture (round 27, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/routes/session-pr-backfill.ts:719 — [review] Dead ?? urls.get() fallback in plannedFor (R27-2)packages/core/src/utils/github-prs.ts:490 — [review] fetchRemoteWebUrl env-stripping lost its only witness (R27-4)packages/core/src/services/sessionService.ts:2461 — [review] Silent 10,000-session truncation in listAllProjectSessionIds, unpinned, doc contradicts (R27-6)packages/core/src/services/session-pr-service.ts:669 — [review] upsertSessionPrs invalid-entry decline branch unwitnessed (R27-7)packages/cli/src/serve/routes/session-pr-backfill.ts:349 — [review] Dead BackfillCandidate.transcriptPath field (R27-8)packages/cli/src/serve/routes/session-pr-backfill.test.ts:2625 — [review] functionResponse record gate never exercised with /review content (R27-9)packages/cli/src/serve/routes/session-pr-backfill.test.ts:3090 — [review] First-mention-wins position retention unpinned under cap pressure (R27-12)packages/cli/src/serve/routes/session-pr-backfill.ts:520 — [review] Typed www-form URLs admitted by repo key, persisted verbatim, never match canonically (R27-17)packages/core/src/services/session-pr-service.test.ts:504 — [review] Batch writer's no-state-carry-over boundary unwitnessed (R27-19)packages/core/src/services/session-pr-service.test.ts:620 — [review] sessionPrSourceAuthority default-rank upper half unwitnessed (R27-20)packages/core/src/services/session-pr-service.ts:181 — [review] Shell command model diverges from bash: six probed entrances (class finding, R27-28)packages/cli/src/serve/routes/session-pr-backfill.test.ts:1478 (+3 locations) — [review] Three stale branch-mapping comments in the cap tests (pattern x3, R27-29)
Convergence: round 27 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/session-pr-backfill.ts (findings in round 26; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:build-and-test — build-cli was skipped in CI and the local packages/cli build timed out on a shrunken whole-call budget (the cli suites themselves ran green via vitest, 100/100)。
未探索到全部深度(达到工具调用预算):chunk 7:executing session-pr-backfill.test.ts to confirm the suite is green — the review worktree has no node_modules or built workspace-package dist/ , and a full…;chunk 12:executing session-pr-service.test.ts under vitest — the shared review worktree has no node_modules installed and vitest cannot start; every test in my chunk…。
收敛姿态下延后(第 27 轮,非阻断)——已记录,本轮不要求修改:共 12 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 27 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/routes/session-pr-backfill.ts(第 26 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.3)
…eate Resolves the session-pr surface with QwenLM#10425 (issue bindings derived from the closing references of bound PRs). Union merges: SessionPr carries both this branch's provenance and main's issue snapshot (the validators check both; upsertSessionPr keeps the snapshot on the same-PR condition next to the authority cap and source upgrade), updateSessionPrStates takes main's fetched-snapshot form, the bridge projects through toSessionPrInfo with the corrective publish comparing issues too, and the metadata routes keep this branch's setSessionPrs reconcile after main's shared projection. mergeSummaryPrs unifies the two models: sidecar supplies order and the cap gate, a canonical-equal live spelling wins with the sidecar's state/issues overlaid, a canonically different url is another PR and the sidecar row wins wholesale with its own snapshot (sidecar-only writers never touch the live entry; the route window reconciles in-request), and a live binding attaches to the url-matched entry when a hand-edited sidecar duplicates a number. Two pre-existing tests were re-pinned to the unified semantics; main's new canonical-equal and duplicate-number tests pass unchanged.
…date In the fork layout the workspace's own numbers live on the CONFIRMED parent page: numberToUrl is gated to the fork's repo key and the remote shape is the fork's url, so the provenance promotion's attestation could never succeed there and the session's own convention binding stayed at its weak persisted rank forever. attested() gains the third identity form — the trusted page's url for the number, canonical-compared, gated on pageMapTrusted so a divergent page still attests nothing (R27-1).
|
Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs. |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 5187 passed · 0 failed · 5187 total Flakiness gate: 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:5187 通过 · 0 失败 · 5187 总计 抖动门: Verification reportPR 9739 — deep verificationVerdict: 中文摘要
Central claim + A/BCentral claim: a foreground Harness: real
Head arm 38/38; base arm 28/28 — on base the two positive cells do not bind and a pre-written sidecar survives untouched without a Secondary claims verified: detector grammar sweep 41/41 ( FindingsF1 (high) — the new execution gate regex has catastrophic backtracking; a ~365-char command freezes the agent process, unrecoverable by SIGTERM
Reachability: the command string is model-authored; prompt-injected content (repo files, issue/PR bodies) is the realistic writer. The input is fully grammar-plausible ( Measured fix (scratch copy of the built module, not applied to the repo): make the flag's optional value refuse an assignment, removing the ambiguity — - (?:sudo|env|nohup|command)\s+(?:-\S+(?:\s+\S+)?\s+|[A-Za-z_][A-Za-z0-9_]*=\S+\s+){0,3})*
+ (?:sudo|env|nohup|command)\s+(?:-\S+(?:\s+(?![A-Za-z_][A-Za-z0-9_]*=)\S+)?\s+|[A-Za-z_][A-Za-z0-9_]*=\S+\s+){0,3})*On the patched build: 24/26/40/100 reps and 20 000 chars all 0 ms ( F2 (suggestion) — the description and Reviewer Test Plan describe code that was removed during review
F3 (low) —
|
| mutant | hunk | suite | result |
|---|---|---|---|
| A1 | bind call site disabled (if (false)) |
shell.test.ts -t "gh pr create binding" |
killed — 7 red, intended assertion (expected "spy" to be called … Number of calls: 0) |
| A2 | output gate inverted (if (output.includes(url)) return) |
same | killed — 8 red |
| B1 | no-downgrade guard >= → < |
session-pr-service.test.ts | killed — exactly the 2 pinning tests red (05-mutation-b1.png) |
| B2 | create authority 2 → 0 |
same | killed — 5 red (cap/eviction/split-pair tests) |
No survivors; no coverage gaps to classify. Unmutated control green (5004/5004 below).
Targeted gates
| workspace | files | result |
|---|---|---|
| core | shell, session-pr-service, github-prs, sessionService, gitWorktreeService, exit-worktree, config tests | 1392/1392 |
| cli | session-pr-backfill, server | 1271/1271 |
| cli | Session×3, transport, fast-path-open, aone-mrs, session-pr-refresh, worktreeStartup, AppContainer | 1455/1455 |
| acp-bridge | bridge.test.ts | 886/886 |
Not covered
- Full daemon E2E (badge render / ~2 s refetch): wiring verified statically (
Session.ts:9320→qwen/notify/session/pr-binding); the emit side was exercised live in the harness. Driving a real daemon + client was beyond budget. - Windows spellings (
gh.exe,C:\…) verified at detector level only; no Windows shell spawn in this container. - Per-commit attribution: shallow checkout (depth 2) — 70 commits in metadata, 1 reachable; verified the aggregate
HEAD^1..HEADdiff only. - Archived-session sidecar write path not driven by the harness (active only);
moveSessionPrSidecarcovered by its suite. - Aone MR surface (
aone-mrs.ts) covered by its test suite only, not behaviorally. - Base control compiled base core against head's root
node_modules(lockfile diff is a reorder only — no dependency-version confound); base build logged one benign TS7016 (@lydell/node-ptyd.ts via a tsconfigpathspointing outside the worktree) — JS still emitted and verified pre-PR (grep -c bindGhPrCreate = 0).
Methodology
CI merge-ref checkout (HEAD merge, HEAD^1 base, HEAD^2 head). Harnesses live in harness/ (ab-harness.mjs, fake-gh.mjs, detector-sweep.mjs, redos-rung.mjs) and are rerunnable; raw logs in logs/. The A/B drove the real built ShellToolInvocation with a fake gh on PATH and real git repos; oracles are the sidecar JSON, the sessionPrBound callback, and the gh call journal. Assertion tally: A/B head 38 + base 28 + sweep 41 + ReDoS original 6 + fix sweep 41 + fix ladder 6 + fix cells 19 + mutant predictions 4 + vitest gates 5004 = 5187 pass, 0 fail.
Flakiness gate log
rounds=5 files=19 skipped=0
file packages/acp-bridge/src/bridge.test.ts: (cd packages/acp-bridge) npx --no-install vitest run ./src/bridge.test.ts
file packages/cli/src/acp-integration/session/Session.review-lease.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.review-lease.test.ts
file packages/cli/src/acp-integration/session/Session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.test.ts
file packages/cli/src/acp-integration/session/Session.worktree.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.worktree.test.ts
file packages/cli/src/serve/acp-http/transport.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/acp-http/transport.test.ts
file packages/cli/src/serve/fast-path-open.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/fast-path-open.test.ts
file packages/cli/src/serve/routes/session-pr-backfill.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/routes/session-pr-backfill.test.ts
file packages/cli/src/serve/server.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server.test.ts
file packages/cli/src/serve/server/aone-mrs.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/aone-mrs.test.ts
file packages/cli/src/serve/server/session-pr-refresh.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/session-pr-refresh.test.ts
file packages/cli/src/startup/worktreeStartup.test.ts: (cd packages/cli) npx --no-install vitest run ./src/startup/worktreeStartup.test.ts
file packages/cli/src/ui/AppContainer.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/AppContainer.test.tsx
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/services/gitWorktreeService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/gitWorktreeService.test.ts
file packages/core/src/services/session-pr-service.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/session-pr-service.test.ts
file packages/core/src/services/sessionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.test.ts
file packages/core/src/tools/exit-worktree.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/exit-worktree.test.ts
file packages/core/src/tools/shell.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/shell.test.ts
file packages/core/src/utils/github-prs.test.ts: (cd packages/core) npx --no-install vitest run ./src/utils/github-prs.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/acp-bridge/src/bridge.test.ts: PP
packages/cli/src/acp-integration/session/Session.review-lease.test.ts: PP
packages/cli/src/acp-integration/session/Session.test.ts: PP
packages/cli/src/acp-integration/session/Session.worktree.test.ts: PP
packages/cli/src/serve/acp-http/transport.test.ts: PP
packages/cli/src/serve/fast-path-open.test.ts: PP
packages/cli/src/serve/routes/session-pr-backfill.test.ts: PP
packages/cli/src/serve/server.test.ts: PP
packages/cli/src/serve/server/aone-mrs.test.ts: P
packages/cli/src/serve/server/session-pr-refresh.test.ts: P
packages/cli/src/startup/worktreeStartup.test.ts: P
packages/cli/src/ui/AppContainer.test.tsx: P
packages/core/src/config/config.test.ts: P
packages/core/src/services/gitWorktreeService.test.ts: P
packages/core/src/services/session-pr-service.test.ts: P
packages/core/src/services/sessionService.test.ts: P
packages/core/src/tools/exit-worktree.test.ts: P
packages/core/src/tools/shell.test.ts: P
packages/core/src/utils/github-prs.test.ts: P
verdict: timeout
summary: the 15-minute budget elapsed before two full rounds completed (1 done) — no flakiness signal either way
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/acp-bridge/src/bridge.test.ts: P (exit 0)
round 1 · packages/cli/src/acp-integration/session/Session.review-lease.test.ts: P (exit 0)
round 1 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 1 · packages/cli/src/acp-integration/session/Session.worktree.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/fast-path-open.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/routes/session-pr-backfill.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/aone-mrs.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/session-pr-refresh.test.ts: P (exit 0)
round 1 · packages/cli/src/startup/worktreeStartup.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/AppContainer.test.tsx: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/services/gitWorktreeService.test.ts: P (exit 0)
round 1 · packages/core/src/services/session-pr-service.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 1 · packages/core/src/tools/exit-worktree.test.ts: P (exit 0)
round 1 · packages/core/src/tools/shell.test.ts: P (exit 0)
round 1 · packages/core/src/utils/github-prs.test.ts: P (exit 0)
round 2 · packages/acp-bridge/src/bridge.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.review-lease.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.worktree.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/fast-path-open.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/routes/session-pr-backfill.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qqqys
left a comment
There was a problem hiding this comment.
APPROVE — 独立复验 @ 0a3bfcaa:最后的站立 Critical R27-1 已在精确 head 核实闭合
Reviewed independently at head 0a3bfcaa65414f7502cf68b6646bf04c9a124f9e。「线程回复 ≠ 修复」——以下全部按当前代码读码判定。
历史阻塞项(仅验证站立项,不重复追已收口的旧轮)
- R27-1(round-27 CHANGES_REQUESTED 于
a9bc0e06提出的唯一 Critical:fork 布局下attested()对 convention 占位恒假 → 来源提升永不落地、会话自身 PR 仍可被任意 capped writer 驱逐)——已修复于本 head。fix commit0a3bfcaa65(「let the trusted fork-parent page attest a promotion candidate」,+96 行)给attested()精确加上 finding 草拟的第三合取支pageMapTrusted && pageUrl !== undefined && canonical === canonicalSessionPrUrl(pageUrl)(session-pr-backfill.ts :834-840),并保持其仅在确认父仓库页可信时生效(pageMapTrusted门与 :373-376 的注释纪律一致;pageUrlByNumber在 repo 门之前录制、fork 布局下归因到父页 url,:360-367)。新增 86 行测试同时钉住正臂(source:'review'→ 提升为'worktree',written:1)与负臂(发散页——attribution 未确认父仓——绝不 attest、条目原样不动);移除新合取支即打红,符合该轮的 fix-witness 要求。 - round-26 的两条 Critical(backfill:508、session-pr-service:423)在
a9bc0e06收口:round-27 全量重测时未再列站立案,其评审体仅新开 R27-1 一条;本轮读 head 亦确认两处修复形态在位(:505-520 的 legacy-fabricated 全路径比对等)。 - round-27 deferred 清单的 12 条均为 Suggestion 级测试见证/死代码类,按流程不构成门禁,留作后续。
当前扫描(Critical-only)
除上述 fix commit 逐行读过外,复核了本 head 的权限序与驱逐方向:sessionPrSourceAuthority worktree 3 > create 2 > 无来源 1 > review 0,capSessionPrListByAuthority 升序先逐出最弱、同级保持原位——与文档「created/convention bindings 优先存活」一致,无翻转错误。未见可证明的新阻塞缺陷。
CI(非门禁,仅陈述)
本 head 24 success / 37 skip / 1 failure = review-pr(评审工作流自身状态,按规则不构成卡点);无失败证据指向本 PR 引入的阻塞性缺陷。
✅ APPROVE — 最后站立的历史 Critical 已在精确 head 读码闭合,当前扫描干净;批准。
— 衍星 · read-only PR review (posted as qqqys)





What this PR does
Closes the last binding-source gap of the session↔PR feature: sessions whose PR was created by the agent running
gh pr createin the shell (instead of the Web Shell Git dialog) now get bound too. Two complementary paths share one detector. Live: after a foreground shell command completes, the shell tool recognizesgh pr create(excluding--dry-run), extracts the PR URL that gh prints on success, and writes the session's PR sidecar directly with stateopen— the same tool-process-writes-sidecar pattern as worktree sessions, so it works in both CLI and daemon modes and shows up in the sidebar within the existing ~2s list refresh. Retroactive: the on-demand backfill route gains a third source that pairs eachrun_shell_commandcall with its response in persisted transcripts (by part id) and applies the same detector, recovering PRs created before the live hook existed. Failed or dry-run creates print no URL, which is the false-positive gate on both paths.Why it's needed
Operators whose agents create PRs from the shell saw no PR badge on those sessions and could not search them by PR number — the original feature only bound GitDialog creations, and the branch-based backfill misses PRs whose head branch never appeared as a session git branch. The printed-URL source is authoritative for exactly this flow, live and historically.
Reviewer Test Plan
How to verify
gh.exe, non-create commands, dry-run/failure), the shell post-hook (mocked execution resolving with a PR URL writes the sidecar withstate: 'open'; failure output writes nothing), and the backfill retroactive source (a transcript with a pairedgh pr createcall/response binds the printed URL even when gh is unavailable).gh pr createagainst a scratch repo); the sidebar badge for that session appears within ~2s. Re-runPOST /sessions/backfill-prson a workspace with old transcripts containinggh pr createruns and confirmboundcounts them.Evidence (Before & After)
Before: a session that ran
gh pr createcarried noprsbinding (badge absent, search by PR number missed it) unless its git branch happened to be a PR head. After: the binding is written at create time (live) or by backfill (historical), with the same badge/search/tooltip behavior as GitDialog-bound sessions.Tested on
Environment
Repo vitest suites (core 33 + shell 305 + cli 27 targeted) plus build/typecheck/lint; macOS local.
Risk & Scope
gh pr createplus a PR-looking URL in output would bind; the URL requirement keeps this near-impossible in practice, and bindings are append-only/bounded (cap 10) with idempotent re-runs. The shell hook is best-effort and can never alter the tool result.Linked Issues
Follow-up to #9543 (session↔PR binding); stacked on #9729.
中文说明
这个 PR 做了什么
补齐会话↔PR 绑定的最后一个来源缺口:agent 在 shell 里
gh pr create创建的 PR(而非 Web Shell GitDialog)现在也能绑定。两条互补路径共享一个检测器。实时:前台 shell 命令完成后,shell 工具识别gh pr create(排除--dry-run),提取 gh 成功时打印的 PR URL,直接以 stateopen写会话 PR sidecar——与 worktree 会话相同的"工具进程直写 sidecar"模式,CLI/daemon 双模生效,约 2s 内在侧栏展示。回填:按需 backfill 路由新增第三来源,按 part id 配对 transcript 里run_shell_command的 call/response 并复用同一检测器,恢复 live hook 存在之前创建的 PR。失败/dry-run 不打印 URL,是两条路径天然的防误报闸门。为什么需要
agent 从 shell 创建 PR 的操作者看不到这些会话的 PR badge,也无法按 PR 号搜索——原功能只绑 GitDialog 创建,基于分支的回填又漏掉 head 分支从未作为会话 gitBranch 出现的 PR。"创建时打印的 URL"对这一流程是权威来源,实时与存量皆然。
审查者测试计划
如何验证
state:'open'写 sidecar;失败输出不写)、backfill 回填源(gh 不可用时也能按 transcript 里配对 call/response 的打印 URL 绑定)。gh pr create(或模拟其输出),约 2s 内出现 badge;对含历史gh pr create痕迹的 workspace 重跑POST /sessions/backfill-prs,确认bound计数。证据(前后对比)
之前:跑过
gh pr create的会话无prs绑定(badge 缺、按号搜索漏),除非其 git 分支恰为 PR head。之后:创建时(实时)或 backfill(存量)写入绑定,badge/搜索/tooltip 行为与 GitDialog 绑定一致。测试平台
macOS ✅;Windows/Linux⚠️ 未本地验证(无 OS 特有路径)。
环境
仓库 vitest 套件(core 33 + shell 305 + cli 27 定向)+ build/typecheck/lint;macOS 本地。
风险与范围
gh pr create字样且输出恰有 PR 样式 URL 会误绑;URL 必要条件使其实践中几乎不可能,且绑定 append-only/有界(cap 10)、重跑幂等。shell hook best-effort,绝不改变工具结果。关联
#9543(会话↔PR 绑定)后续;叠在 #9729 之上。
设计更新(最终形态,覆盖上文旧描述)
经多轮 review 迭代,绑定来源与闸门已重构,上文"检测器启发式/转录 call-response 配对"等描述作废:
/review <N|#N|url>命令(优先读 slash_command 原始命令记录)与 worktreepr-<N>约定(slug 已保留该命名空间)。转录中的gh pr create痕迹与 session git 分支两个来源已移除(前者无法归因、存在伪造向量;后者实测纯噪声)。2026-08-29 接手更新(HEAD 0a3bfca)
session-pr-backfill上的结构性冲突——Aone 沿用本 PR 的来源模型(/review <N|#N|url>+ worktreepr-<N>约定,任何平台都不用 transcript 分支映射),约定号与/review N经有上限、带缓存的a1 repo mr view解析、绝不从 remote 拼 URL,Aone 化之前伪造的<origin>/pull/<N>绑定在计划里原地修复;注入的AoneMrBackendseam 收为 view-only(mr list不再在 backfill/refresh 的调用面上,exec 层原语保留)。响应字段:platform,ghAvailable仅 GitHub。b6e750c247)、R19-2(/review <url>过 sidecar 形状校验,replaceSessionPrs在写边界拒绝读侧会拒绝的条目)、R19-3(planner 溢出裁剪按 provenance 排序,约定号保护成为同一规则而非特判,51acfb3c8c)、R20-7(未信任 gh 页仓库的/review <url>只绑自己命名的 PR,其 URL 不借给同号的裸号/约定号)、R20-15(upsertSessionPr与批量写入同样按 provenance 封顶,bridge reconcile 测试改为真实分歧)、R20-16(跨 URL 重绑只盖 candidate 的 source)、R14-1 第三半(--worktree pr-42对已注册的 PR-backed worktree 按字面 slug 重新附着,不存在时拒绝并提示--worktree=#42,22dbcf533c);另修 corenormalizeRemoteToWebUrl丢 https 端口的问题并删除 backfill 的孤儿副本(c332e99872)。docs/design/2026-08-20-webshell-session-pr-binding.md范围边界,f0eba2cb34):R13-2(promote 成功 /is_background的gh pr create不实时绑定,经/review <N>或约定绑定;docstring + 两条负向测试钉住)、R16-2(执行闸门语法收口为文档化的封闭集合——只做「任意路径限定的 gh」与「嵌套 wrapper 链」两处封闭式放宽,inline env 采集改为按命令顺序、建模unset/env -u/env -i删除侧、${VAR:-…}保持字面量;语法之外 fail-closed,不再逐形状扩展)、R20-8(fork PR worktree 经exit_worktree删除受既有hasUnmergedWorktreeCommits守卫拒绝——合并基线即如此,本 PR 只放行 slug 形状,删除语义豁免属独立后续)。source的重提占位者按本轮 stamp 排序,会让 provenance 记录之前的 GitDialog 绑定被新 review 挤出;已改为沿用 sidecar 阶梯自身的 rank(无 source 高于 review),新增用例钉住,五个封顶夹具改为显式source:'review'种子(198c733be2)。顺带清掉延后清单里的一行级项:单数 upsert 跨 URL 重绑丢显式 source、/review #N正向测试、过时注释、bridgeClient 重复 import、设计文档三处与代码不符。/review <url>形态:Aone 上/pull/N形态只可能是历史伪造 URL,却被直接借出绕过mr view(R22-1);两段仓库 key 折叠嵌套 group,兄弟项目的形态能过闸(R22-2)。修法:Aone 上 URL 形态只当号源、绝不借 URL(URL 唯一来源mr view),且只承认恰为本仓库 remote 伪造形状的形态(全路径精确匹配);两条正向/负向用例钉住(166a639275)。max(本轮 stamp, 持久化 source)保护重提占位者,却把条目原样写回——先/review 100再获得pr-100关联的条目磁盘上仍是 review 级,其它按 authority 封顶的写入会最先挤掉它。修成kept原地提升 source(不动 url/createdAt/位置、绝不降级、只对通过同 PR 身份判定的 plan 成员),pre-provenance 约定占位者一次迁移写入后幂等;两条用例 + 变异验证(f3e41ac2e7)。worktree。修成只在身份可证明时提升(gh 解析 URL 规范化相等 / 本 workspace<remote>/pull/N形状 / Aone detailUrl 形状),裁剪的 fail-open 不动;负向用例 + 变异验证(d57f5ff593)。该轮 Test/Serve A/B 为 runner 超时 cancelled(无测试失败),本次推送重跑。522cb3a2af):解决与 fix(core): Preserve ownership during session cleanup #10300(会话清理所有权)在sessionService的冲突——保留 main 的moveArchiveSidecars助手与assertCleanupOwned命名,助手内 PR sidecar 一腿改走本分支的带锁moveSessionPrSidecar(无锁私有movePrSidecar不复活);main 针对私有方法写的所有权用例改为断言 fence 传入带锁搬移、SessionWriterLostError拒绝不被降级为 warning;split pair 合并语义仍由session-pr-service.test.ts钉住。合并后对 main 的净差异只剩本分支的三块。d57f5ff593 上 Test/Serve A/B/E2E 三个 job 均为 runner 超时 cancelled(无断言失败),本次 merge head 重跑。updateSessionMetadata的事件先带着位置封顶的错列表发出,setSessionPrs调和后既不发事件也不 bump revision。修成setSessionPrs中心化补发:成员真实变化时才发 correctivesession_metadata_updated+markSessionCatalogChanged(),三个路由/dispatch 调用点与 backfill live-entry 同步一次覆盖,below-cap 场景零额外事件(975c2862de,变异验证)。同时 UpdateBranch 合入 38 个 main commit(含 ECS runner 事故修复 ci: allow full test jobs to finish on ECS runners #10558/test(ci): stabilize shared-runner budgets and cron interactive checks #10648,合并 CLEAN),merge headd8767e032f;bridge 882/882、server 1162/1162、transport/backfill/startup 479 全绿。b07933d075、a9bc0e06d7,皆有变异验证):R26-27mergeSessionPrLists是最后一个能在同 PR 去重时降级 provenance 的写入方(split pair 合并把 create 降成 review 后被 authority cap 挤出),改为同规范化 URL 下保留更强 source、最新 createdAt 仍占槽;R26-1 URL 形态号被排在全部裸号之后、跨形态丢失转录年龄序,收集器改为按转录顺序发出统一 mention 流(首次出现占位、bare 性粘附),同级裁剪的位置 tie-break 恢复为真实年龄代理。@tanstack/react-table后 3/3 全过,CI 端失败与 runner 池强相关——其它 PR 成功例全在 hk4/hk5 新池,本 PR 三次全被调度到 sg-5/sg-7/hk3 降级池。无分支缺陷;随本次推送重跑。attested()两个析取支都够不到父仓库 URL(numberToUrl 按 fork key 闸门、remote 是 fork 形状),约定占位者永远无法被证明身份、提升永不落地。修法按建议加第三支——pageMapTrusted门控下与受信页 URL 规范化相等;fork 正例 + divergent 负例 + 变异验证(0a3bfcaa65)。708efb3a65):解决与 feat(web-shell): derive session issue bindings from the closing references of bound PRs #10425(issue 快照)在整个 session-pr 面的冲突。并集:SessionPr同时携带本分支的source与 main 的issues(校验、同 PR 重绑保留快照、authority cap 共存);updateSessionPrStates取 main 的 fetched 形态;bridge 投影走toSessionPrInfo、校正事件的 unchanged 比较纳入 issues;metadata 路由保留本分支的setSessionPrs调和。mergeSummaryPrs统一两模型:sidecar 供序与封顶门;规范化相等 → live 拼写 + sidecar state/issues;规范化不等(另一个 PR)→ sidecar 行整体获胜、快照永不跨挂;重号 sidecar 按 URL 匹配。两条旧测试按统一语义重钉,main 新增的 canonical-equal / 重号测试原样通过;server 全量 1170/1170、bridge 886/886、core 99、backfill 101。