Skip to content

feat(core): bind PRs created via gh pr create in the session shell - #9739

Open
wenshao wants to merge 43 commits into
QwenLM:mainfrom
wenshao:feat/session-pr-gh-create
Open

feat(core): bind PRs created via gh pr create in the session shell#9739
wenshao wants to merge 43 commits into
QwenLM:mainfrom
wenshao:feat/session-pr-gh-create

Conversation

@wenshao

@wenshao wenshao commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

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 create in 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 recognizes gh pr create (excluding --dry-run), extracts the PR URL that gh prints on success, and writes the session's PR sidecar directly with state open — 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 each run_shell_command call 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

  • Unit coverage: the detector (success URL, wrapped commands, gh.exe, non-create commands, dry-run/failure), the shell post-hook (mocked execution resolving with a PR URL writes the sidecar with state: 'open'; failure output writes nothing), and the backfill retroactive source (a transcript with a paired gh pr create call/response binds the printed URL even when gh is unavailable).
  • Live: in a daemon session, ask the agent to run a command whose output mimics a successful create (or run a real gh pr create against a scratch repo); the sidebar badge for that session appears within ~2s. Re-run POST /sessions/backfill-prs on a workspace with old transcripts containing gh pr create runs and confirm bound counts them.

Evidence (Before & After)

Before: a session that ran gh pr create carried no prs binding (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

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment

Repo vitest suites (core 33 + shell 305 + cli 27 targeted) plus build/typecheck/lint; macOS local.

Risk & Scope

  • Main risk or tradeoff: the detector is a heuristic on the command string, so a command merely containing gh pr create plus 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.
  • Not validated / out of scope: Windows/Linux not exercised locally (no OS-specific code paths); backgrounded shells are not scanned live (their output streams to files) — backfill covers historical ones.
  • Breaking changes / migration notes: none; purely additive sources writing the existing sidecar schema.
  • Stacked on feat(serve): backfill session PR bindings and refresh their merge state #9729 (backfill + state refresh): this branch includes those commits until feat(serve): backfill session PR bindings and refresh their merge state #9729 merges; the PR diff shrinks automatically afterwards.

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,直接以 state open 写会话 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"对这一流程是权威来源,实时与存量皆然。

审查者测试计划

如何验证

  • 单测:检测器(成功 URL、包装命令、gh.exe、非 create 命令、dry-run/失败)、shell post-hook(mock 执行返回含 PR URL 的输出时以 state:'open' 写 sidecar;失败输出不写)、backfill 回填源(gh 不可用时也能按 transcript 里配对 call/response 的打印 URL 绑定)。
  • 真实验证:daemon 会话里让 agent 跑 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,绝不改变工具结果。
  • 未验证/超出范围:Windows/Linux 未本地跑;后台 shell 不做实时扫描(输出流式入文件)——backfill 覆盖存量。
  • 破坏性变更/迁移:无;纯增量来源,写既有 sidecar schema。
  • 叠在 feat(serve): backfill session PR bindings and refresh their merge state #9729(backfill + state 刷新)之上:feat(serve): backfill session PR bindings and refresh their merge state #9729 合入前本分支包含其提交,合入后 PR diff 自动收缩。

关联

#9543(会话↔PR 绑定)后续;叠在 #9729 之上。

设计更新(最终形态,覆盖上文旧描述)

经多轮 review 迭代,绑定来源与闸门已重构,上文"检测器启发式/转录 call-response 配对"等描述作废:

  • 实时绑定:shell post-hook 以 gh 本身为归因权威——命令须通过执行闸门(仅作"是否执行了 gh pr create"的判断),且 gh 事后解析出该分支的 OPEN PR、命令输出携带该 URL、pre-run 快照证明该 PR 非运行前已存在、仓库身份(自身或已确认的 fork 父仓库)匹配。仅凭命令/输出文本不能伪造绑定。
  • 存量回填:来源仅两个——用户输入的 /review <N|#N|url> 命令(优先读 slash_command 原始命令记录)与 worktree pr-<N> 约定(slug 已保留该命名空间)。转录中的 gh pr create 痕迹与 session git 分支两个来源已移除(前者无法归因、存在伪造向量;后者实测纯噪声)。
  • 接受的已知限制:执行闸门对引号/表达式盲(quote-aware tokenizer 在 tools/shell.ts,不能进入 serve 快速路径闭包),残余过匹配因 gh 归因闸门而无害;纯后台 shell 的 create 不实时绑定、也不从转录恢复(/review 与 worktree 约定可覆盖);backfill 为按需管理路由,整读转录。

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.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Re-run on @qwen-code /triage — gate re-checked against the current head, which has moved far since the last pass (24 commits, eleven review-loop rounds, multiple autofix pushes).

Template looks good ✓

Problem: real, but narrower than the description says. The live half holds: the merged session↔PR feature (#9543) has no writer for PRs the agent creates via gh pr create in the shell, so those sessions get no badge and miss PR-number search. The retroactive half was removed during the review rounds: the transcript-pairing backfill source is gone — the design doc now explicitly declines it (text alone can forge a binding) and a test pins its absence — so pre-hook history is no longer recovered by this PR. The body still describes the removed source and claims historical recovery; it needs a rewrite before merge (detailed in Stage 2).

Direction: aligned — the natural completion of the session↔PR binding stream (#9543#9729 → this), and the design doc is kept in sync with the removal rather than drifting from it. No CHANGELOG reference, but the area is the merged #9543's declared follow-up.

Stack note — changed since the last pass: the branch has diverged from #9729 (21 commits ahead, 29 behind its head). The body's "the PR diff shrinks automatically afterwards" no longer holds — this branch now carries its own copy of the stacked scope, and after #9729 lands it needs a rebase with expected conflicts in the shared files. Merge order stays #9729 first.

Size: the divergence changes the size picture too. The displayed diff against main is now 43 files, +5256/−101 ≈ 1.7k production / 3.5k test / ~40 docs lines. For a feat touching core that is past the 500-production-line maintainer-awareness mark and the 1000-line advisory — informational, not blocking, but it means this gate will not auto-approve regardless of review outcome; #9729's own review continues to cover the shared scope it duplicates.

Approach: the core design remains right — one execution gate plus gh-side attribution (pre/post gh pr view snapshots) feeding a best-effort sidecar write, backfill limited to sources with real attribution (/review commands, worktree convention). Scope discipline is otherwise good: the only drive-bys are a few prettier reflows (geminiChat.ts, agent-core.ts, one test) and a blockquote fix in an unrelated design doc.

Risk: packages/core/src/tools/shell.ts matches the revert-correlated high-risk path list — full Stage 2 enrichment and CI evidence apply before approval. (packages/core/src/core/geminiChat.ts also matches by name, but its change is a 3-line prettier reflow — no behavioral surface.)

Moving on to code review. 🔍

中文说明

@qwen-code /triage 触发的 re-run——门禁在当前 head 上重新检查;距上次审查已移动很多(24 个提交、11 轮 review-loop、多次 autofix 推送)。

模板完整 ✓

问题:真实存在,但比描述所说的更窄。实时一半成立:已合入的会话↔PR 功能(#9543)对 agent 在 shell 里 gh pr create 创建的 PR 没有写入方,这些会话拿不到 badge、也搜不到 PR 号。存量一半已在评审轮次中被移除:transcript 配对回填源已删除——设计文档现在明确拒绝它(纯文本可伪造绑定),并有测试钉住其缺席——因此本 PR 不再恢复 hook 之前的历史。PR 正文仍在描述这个已删除的源并声称历史恢复;合入前需要重写正文(详见 Stage 2)。

方向:对齐——会话↔PR 绑定系列(#9543#9729 → 本 PR)的自然收尾,设计文档与删除保持同步而非漂移。CHANGELOG 无直接引用,但该领域是已合入 #9543 声明的后续。

堆叠提示——与上次审查相比有变化:本分支已与 #9729 分叉(领先 21 个提交、落后其 head 29 个)。正文中"#9729 合入后 diff 自动收缩"的说法不再成立——本分支现在携带堆叠范围的独立副本,#9729 合入后需要 rebase,共享文件预期会冲突。合入顺序仍是先 #9729

规模:分叉也改变了规模图景。对 main 的展示 diff 现为 43 个文件,+5256/−101 ≈ 生产 1.7k / 测试 3.5k / 文档约 40 行。对触及核心的 feat,这已超过 500 生产行的维护者关注线与 1000 行大 PR 提示——仅提示、不阻断,但意味着无论评审结果如何本门禁都不会自动批准;#9729 自身的评审继续覆盖此处复制的共享范围。

方案:核心设计仍然正确——一个执行闸门 + gh 侧归因(运行前后 gh pr view 快照)喂尽力而为的 sidecar 写入,backfill 限定在有真实归因的源(/review 命令、worktree 约定)。范围纪律其余部分良好:唯一的顺手改动是几处 prettier 重排(geminiChat.tsagent-core.ts、一个测试)和一个无关设计文档的引用块修复。

风险:packages/core/src/tools/shell.ts 命中与 revert 相关的高风险路径——批准前执行完整 Stage 2 增强与 CI 证据检查。(packages/core/src/core/geminiChat.ts 按名字也命中,但其改动是 3 行 prettier 重排——无行为面。)

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at c7f92387247d15dd09407306c405bf7c0c66067d · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 34033d1. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Code review

Re-reviewed at the current head. Because the branch has diverged from stacked #9729 (21 ahead / 29 behind its head), this pass covers the whole displayed diff — 43 files, ≈1.7k production lines — not an isolated delta. The wiring quality is high: payload validation on every boundary, URL-identity rules on state inheritance, catalog-clock propagation, cross-process locking. The test suite is substantial (≈3.5k lines). But this pass cannot clear the PR: the Critical findings the review loop posted on this exact head are genuine — I verified the load-bearing ones against the code below, and they stand unresolved here.

1. The PR body describes a feature the code removed. The description says backfill gains a third source pairing run_shell_command calls and responses to recover pre-hook creates. The code says the opposite — "Transcript gh pr create traces are deliberately NOT a source" — the design doc rejects it, and a test pins the absence ("does not bind PRs from transcript gh pr create traces (source removed)"). The removal was the right call (an echo-shaped command mentioning gh pr create plus any same-repo URL would forge a binding), but the body's "Why it's needed" and before/after still claim historical recovery. Whoever merges reads the body; right now it misstates what lands.

2. Live-hook attribution bypasses — one root cause, three standing findings. The pre-run snapshot carries no repo or branch identity; fetchCurrentBranchPullRequest returns none when there is no repo at cwd even though its own contract defines none as proved absence (gh answered); and the decline compares PR numbers only. Verified instances on this head:

  • branch switch inside the command (git checkout b2 && gh pr create --fill || gh pr view …): the snapshot was taken on b1 with no PR, so the number decline cannot fire; b2's pre-existing PR binds as this session's creation;
  • git clone … . && gh pr create in a non-repo directory: pre-snapshot is none because gh was never asked, the create fails on the existing PR, the view fallback exits 0 printing the URL, and the binding lands;
  • git remote set-url / gh repo set-default during the run retargets the post-run resolution onto another repo's open PR — backfill and the refresh sweep both repo-key-gate their URLs, only this live path lacks the check.
    All three stamp a foreign or pre-existing PR source: 'create' with a fresh createdAt and broadcast it. The fix direction is identity in the snapshot (repo key + branch, compared across the window), not more number comparisons — the loop's convergence note already says the cluster regenerates because instances get fixed one at a time.

3. Backfill re-runs rotate once the sidecar is occupied (standing). Offered candidates are trimmed to the full cap without accounting for persisted occupants backfill never re-offers (live create bindings, pre-provenance entries). Once offered + occupants exceeds 10, each re-run evicts the weakest, re-appends with a fresh createdAt, reports bound >= 1, and bumps the catalog clock on every POST — live clients refetch forever and the binding-time order the badge renders by gets rewritten each run. The "keeps re-runs idempotent" comment does not hold in that state.

4. Metadata surfaces downgrade provenance (standing). The three REST PATCH metadata routes and the ACP session/update_metadata all stamp source: 'create' unconditionally, and upsertSessionPr lets an explicit source win over the persisted one — so re-binding an already-bound number demotes a worktree convention binding (eviction rank 3) to create (rank 2) in place, removing exactly the protection this PR's design and its own "keeps the convention binding when a create lands on a full list" test exist to guarantee.

5. Also standing, mechanisms verified: SessionService.movePrSidecar (pre-existing archive/unarchive path) renames, merges, and unlinks the same sidecar paths outside the new lock while bindGhPrCreate pins the active path across its awaited gh round-trip; and backfill's pageUrlByNumber fallback passes through unfiltered whenever the workspace key is merely known, while gh repo set-default can point gh's list resolution at a repo that is neither the workspace nor its fork parent.

Minor, non-blocking: the prettier reflows in geminiChat.ts / agent-core.ts / one config test and the blockquote fix in the unrelated image-drag-and-drop design doc are drive-bys relative to the stated goal — harmless, but they serve nothing here.

sequenceDiagram
    participant P1 as Shell tool foreground path
    participant P2 as Execution gate
    participant P3 as gh pr view attribution
    participant P4 as PR sidecar file
    participant P5 as Daemon catalog clock
    participant P6 as Sidebar badge
    P1->>P2: command completed with exit 0
    P2->>P3: pre-run snapshot, post-run fetch
    P3-->>P1: open PR whose URL is in the output, or nothing
    P1->>P4: locked upsert with source create, best-effort
    P1->>P5: pr-binding notification marks catalog
    P5-->>P6: clients refetch within about 2s
Loading
Files changed — grouped overview (43 files)
Area Key files What changed
Core detector and binder packages/core/src/services/session-pr-service.ts, packages/core/src/utils/github-prs.ts, packages/core/src/tools/shell.ts (+ tests) Execution gate, provenance/state-aware upserts behind a two-tier lock, gh-side branch-PR snapshot, shell post-hook
Serve routes and sweep packages/cli/src/serve/routes/session-pr-backfill.ts, packages/cli/src/serve/server/session-pr-refresh.ts, routes/session.ts, server/session-list.ts, server.ts, run-qwen-serve.ts (+ tests) On-demand backfill sources, merge-state refresh timer, metadata routes, sidecar enrichment of the session list
Bridge, ACP, SDK packages/acp-bridge/src/bridge.ts, bridgeClient.ts, bridgeTypes.ts, serve/acp-http/dispatch.ts, acp-integration/session/Session.ts, sdk-typescript/daemon/session-pr.ts (+ tests) State field and pr-binding notification carried across every boundary
Web Shell SessionPrBadge.tsx + css + test, GitDialog.tsx, SessionDetailsTooltip.tsx, i18n.tsx Badge merged/closed states and tooltip labels
Core plumbing sessionService.ts, config.ts, agent-core.ts, geminiChat.ts Enumeration helper and bound-callback carry-over; the last two are prettier reflows
Docs session-pr-binding design doc; image-drag-and-drop doc Design sync for hook and source removal; the second is a blockquote fix

Testing

Unattended CI run — no local build or execution of PR code (triage rules). Evidence below is the PR's own CI on the reviewed commit, fetched via the API; the finalize workflow updates the table region in place if checks move.

CI is fully settled and green on this head: every pull_request-event workflow completed successfully — Qwen Code CI, Serve A/B, Web-shell Visuals, Qwen Live Host CI, Security Checks, SDK Java — including the ubuntu unit suite (Test (ubuntu-latest, Node 22.x)) that landed red on the previously reviewed commit and went green after the last autofix push. One standing gap, same as every prior round: Integration Tests (CLI, No Sandbox) is skipped — fork PRs cannot run that lane — so the CLI integration surface is unexercised by CI here. Not verified: live attribution against real gh output and sidecar→badge propagation — the suite mocks shell execution and gh responses end to end; the author's macOS summary in the body is their claim, not independent evidence.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Live Host (macos-latest) ✅ success
Real daemon E2E / Java 11 ✅ success
SDK Java matrix (ubuntu 11/17/21, macOS 21, windows 21) ✅ success
Secret scan (TruffleHog) ✅ success
Dependency CVE audit ✅ success

Skipped checks omitted; Integration Tests (CLI, No Sandbox) is skipped on fork PRs. / 省略 skipped;Integration Tests (CLI, No Sandbox) 在 fork PR 上跳过。

Sandboxed verification would settle this: @qwen-code /verify — the mock-based suite proves wiring, not that the attribution gates hold against real gh output shape, and with the integration lane skipped a wire-oracle A/B is the only way to exercise the CLI surface end to end; @qwen-code /tmux would cover the sidebar-badge TUI surface the same way.

中文说明

代码审查

在当前 head 重新审查。由于分支已与堆叠的 #9729 分叉(领先 21 / 落后其 head 29),本次覆盖整个展示 diff——43 个文件、约 1.7k 生产行——而非隔离增量。接线质量高:每个边界都有载荷校验、状态继承有 URL 同一性规则、目录时钟传播、跨进程锁。测试套件可观(约 3.5k 行)。但本次审查不能放行该 PR:review loop 在此 head 上发布的 Critical 发现是真实的——我对照代码验证了其中承重部分,它们在此 head 上仍未解决。

1. PR 正文描述了一个代码已删除的功能。 正文说 backfill 新增第三源(按 part id 配对 run_shell_command 调用/响应)以恢复 hook 之前的创建。代码恰恰相反——"Transcript gh pr create traces are deliberately NOT a source"——设计文档拒绝它,测试钉住其缺席。删除是正确的(echo 形状命令提及 gh pr create 加任意同仓库 URL 即可伪造绑定),但正文的"为什么需要"与前后对比仍声称历史恢复。合入者读的是正文,目前正文与落地内容不符。

2. 实时 hook 归因绕过——一个根因、三条在案发现。 运行前快照不携带仓库或分支身份;fetchCurrentBranchPullRequest 在 cwd 无仓库时返回 none,而其自身契约定义 none已证明的缺席(gh 已应答);拒绝逻辑只比较 PR 号。在此 head 上验证的实例:命令内切分支(快照在 b1 上无 PR,数字拒绝无法触发,b2 既有 PR 被绑为本会话的创建);非仓库目录里 git clone … . && gh pr create(快照为 none 因为 gh 从未被询问,create 失败、view 兜底打印 URL,绑定落地);运行中 git remote set-url / gh repo set-default 把运行后解析重定向到另一仓库的开放 PR——backfill 与刷新轮都有仓库键闸门,唯独此实时路径没有。三者都会把外来/既有 PR 以 source:'create'、全新 createdAt 写入并广播。修复方向是快照携带身份(仓库键 + 分支,跨窗口比较),而不是更多数字比较——loop 的收敛说明已指出该簇之所以再生,是因为在逐个修实例。

3. sidecar 已有占用时,backfill 重跑会轮转(在案)。 候选按满上限裁剪,未考虑 backfill 从不重新提供的既有占用(实时 create 绑定、无来源标记的旧条目)。一旦"提供集 + 占用"超过 10,每次重跑都驱逐最弱者、以新 createdAt 重新追加、报告 bound >= 1、并在每次 POST 都 bump 目录时钟——客户端永远在重取,badge 渲染所依赖的绑定时间顺序每次都被改写。"保持重跑幂等"的注释在该状态下不成立。

4. 元数据写入面降级来源标记(在案)。 三个 REST PATCH 元数据路由与 ACP session/update_metadata 都无条件打 source:'create',而 upsertSessionPr 让显式来源覆盖持久化来源——于是对已绑定号码的重新绑定会把 worktree 约定绑定(驱逐权重 3)就地降为 create(权重 2),恰好移除了本 PR 设计与自身测试("create 落在满列表时保留约定绑定")所要保证的保护。

5. 其余在案、机制已验证:SessionService.movePrSidecar(既有归档/取消归档路径)在新锁之外对相同 sidecar 路径做重命名/合并/删除,而 bindGhPrCreate 在其等待的 gh 往返期间钉住 active 路径;以及当工作区键仅"已知"时,backfill 的 pageUrlByNumber 兜底未经过滤直通,而 gh repo set-default 可以让 gh 的列表解析指向第三个仓库。

次要、不阻断:geminiChat.ts / agent-core.ts / 一个 config 测试的 prettier 重排与无关图文拖放设计文档的引用块修复属于顺手改动——无害,但对本目标无贡献。

时序图展示实时路径:shell 前台命令完成 → 执行闸门 → gh 归因(运行前快照 + 运行后解析)→ 加锁写 sidecar(source create)→ 通知 daemon 目录时钟 → 客户端约 2s 内 refetch → 侧栏 badge。

测试

无人值守 CI 运行——不本地构建或执行 PR 代码(triage 规则)。以下为审查提交上 PR 自身 CI 的 API 证据;若检查变动,finalize 工作流会就地更新表格区域。

此 head 上 CI 已全部落定且为绿:所有 pull_request 事件工作流成功完成——Qwen Code CI、Serve A/B、Web-shell Visuals、Qwen Live Host CI、Security Checks、SDK Java——包括上一审查提交上为红、最后一次 autofix 推送后转绿的 ubuntu 单测。一个持续缺口,与以往各轮相同:Integration Tests (CLI, No Sandbox)跳过——fork PR 无法运行该车道——因此 CLI 集成面在 CI 上未被执行。未验证:对真实 gh 输出的实时归因与 sidecar→badge 传播——套件对 shell 执行与 gh 响应全程 mock;正文中的 macOS 测试摘要是作者声明,不是独立证据。

沙箱验证可以落定此事:@qwen-code /verify——基于 mock 的套件证明的是接线,而非归因闸门对真实 gh 输出形状的成立;在集成车道被跳过的前提下,wire-oracle A/B 是端到端执行 CLI 面的唯一途径;@qwen-code /tmux 以同样方式覆盖侧栏 badge 的 TUI 面。

Qwen Code · qwen3.8-max

Reviewed at c7f92387247d15dd09407306c405bf7c0c66067d · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the design is right, the wiring is careful, and CI is green on this head; but verified Critical findings stand unresolved on this exact head, and the description no longer matches the code.

Stepping back: my independent proposal from the first pass — one execution gate, gh-side attribution via a pre/post snapshot, a best-effort sidecar write, backfill limited to sources with real attribution — is still what is on the branch, and the loop's removal of the forgeable transcript source was the right tightening, not a scope loss I'd contest. I did not find a simpler path the implementation missed. What I cannot sign:

  1. The review loop's Critical findings posted on this exact head are genuine. I verified the load-bearing ones against the code: the three live-hook attribution bypasses (branch switch, clone-then-create, mid-run repo retarget) all trace to one root cause — the snapshot carries no repo or branch identity, none is used where gh was never asked, and the decline compares numbers only. The backfill re-run rotation and the provenance downgrade through the metadata surfaces are confirmed mechanisms too, not hypotheticals. The autofix loop timed out mid-round and will retry, but as of this head the findings stand, and I won't approve against my own verified findings.
  2. The PR body misdescribes what lands. It still claims the removed transcript backfill source and historical recovery. A maintainer merging on the description would be misled about the feature's actual scope.
  3. The stack diverged. 21 ahead / 29 behind feat(serve): backfill session PR bindings and refresh their merge state #9729's head: the "diff shrinks automatically" promise in the body is dead, this branch carries its own copy of the stacked scope, and a rebase with expected conflicts follows feat(serve): backfill session PR bindings and refresh their merge state #9729's merge. Merge order stays feat(serve): backfill session PR bindings and refresh their merge state #9729 first.
  4. Policy, stated for the record: a feat touching core at ≈1.7k production lines does not get auto-approved from this gate regardless of review outcome.

Verdict: defer — comment, no approval. This is not a rejection: the direction and structure are sound, CI is green, and most of what stands is mechanical once the root cause (snapshot identity) is addressed instead of its instances. @wenshao — three things unblock this: the standing findings closed at the root cause (or the autofix loop converging on them), a body rewrite matching the shipped design, and the post-#9729 rebase. Re-run @qwen-code /triage after that. The outstanding CHANGES_REQUESTED review state blocks merge in the meantime, which matches reality.

中文说明

置信度:3/5 —— 设计正确、接线谨慎、此 head 上 CI 为绿;但有已验证的 Critical 发现在此 head 上仍未解决,且描述已与代码不符。

退一步看:我在首轮写下的独立方案——一个执行闸门、经运行前后快照的 gh 侧归因、尽力而为的 sidecar 写入、backfill 限定在有真实归因的源——仍是分支上的实现;loop 删除可伪造的 transcript 源是正确的收紧,不是我要反对的范围损失。我没有找到实现漏掉的更简路径。我不能签字的部分:

  1. review loop 在此 head 上发布的 Critical 发现是真实的。 我对照代码验证了承重部分:三条实时 hook 归因绕过(切分支、克隆后创建、运行中重定向仓库)同源于一个根因——快照不携带仓库或分支身份、在 gh 从未被询问处使用了 none、拒绝逻辑只比较数字。backfill 重跑轮转与元数据面的来源降级同样是已确认的机制,而非假设。autofix loop 超时未完成本轮、将重试,但截至此 head 发现在案;我不会在对自己已验证的发现视而不见的情况下批准。
  2. PR 正文与落地内容不符。 仍声称已删除的 transcript 回填源与历史恢复。按描述合入的维护者会被功能实际范围误导。
  3. 堆叠已分叉。 领先 21 / 落后 feat(serve): backfill session PR bindings and refresh their merge state #9729 head 29:正文"diff 自动收缩"的承诺已失效,本分支携带堆叠范围的独立副本,feat(serve): backfill session PR bindings and refresh their merge state #9729 合入后需要带预期冲突的 rebase。合入顺序仍是先 feat(serve): backfill session PR bindings and refresh their merge state #9729
  4. 政策,记录在案:触及核心、约 1.7k 生产行的 feat,无论评审结果如何,本门禁都不自动批准。

结论:暂缓——评论、不批准。 这不是拒绝:方向与结构健全,CI 为绿,且在案发现大多在根因(快照身份)被处理而非逐个修实例后即可机械性闭合。@wenshao——三件事可以解锁:以根因闭合在案发现(或 autofix loop 收敛)、正文重写以匹配落地的设计、#9729 合入后的 rebase。完成后重新 @qwen-code /triage。未决的 CHANGES_REQUESTED 评审状态在此期间阻止合入——这与现实一致。

Qwen Code · qwen3.8-max

Reviewed at c7f92387247d15dd09407306c405bf7c0c66067d · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 34033d1, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

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.
@wenshao

wenshao commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 22, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 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/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment thread packages/core/src/tools/shell.ts Outdated
Comment thread packages/core/src/services/session-pr-service.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/server/session-pr-refresh.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session.ts
Comment thread packages/cli/src/serve/routes/session-pr-backfill.test.ts
@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

⚠️ AutoFix round 1 ended without publishing a reportview run.

中文说明

⚠️ AutoFix 第 1 轮结束但未发布报告 —— 查看运行

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.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Review round summary — PR #9739 (commit bd968f2b23)

This round implemented all five bounded Critical findings (R1-2 … R1-6) plus the three Suggestions entangled with them (R1-11, R1-19, R1-20), each with a regression test verified to fail on the pre-round code (mutation probes below). Per the ~8-findings-per-round budget, the remaining findings are deferred to the next round with explicit replies on their threads (comment-replies.json).

Resolved in code

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 (seedSessionPrsbridge.updateSessionMetadataupsertSessionPr), making the daemon the sole serialized writer and bumping the catalog revision. This spans core (sink on Config), cli (ACP Session wiring), and acp-bridge (demux + handler) and exceeds this round's budget; the fallback for R1-7 alone is a two-tier proper-lockfile guard (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, toHaveBeenCalledWith arg 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 buildpassed (also re-run after formatting; 0 errors)
  • npm run typecheckpassed (0 TS errors)
  • npm run lintpassed
  • npx prettier --check on all 10 changed code files — passed (after --write on 5)
  • cd packages/core && npx vitest run src/services/session-pr-service.test.ts src/utils/github-prs.test.ts src/tools/shell.test.ts384 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) — passed
  • 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.ts1112 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.tsx18 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.size counting 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.gitjdoe@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 的完整序列(seedSessionPrsbridge.updateSessionMetadataupsertSessionPr),让 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.ts384 通过(含 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.ts1112 通过(20 个回填 + 11 个刷新测试,含 8 个新增)
  • cd packages/web-shell && npx vitest run client/components/SessionPrBadge.test.tsx client/components/dialogs/GitDialog.test.tsx18 通过

变异探针(每个修复的见证测试均已验证:轮次前代码上为红,恢复后为绿):

  • 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 qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + spawn
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:653 — [review] Route aggregation test asserts neither the trusted entry nor non-zero reduce totals
  • packages/cli/src/serve/run-qwen-serve.ts:5164 — [probe] Sweep-starting dynamic import has no .catch — uncaught exception (daemon crash) on the serve fast path
  • packages/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 reachable
  • packages/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 make
  • packages/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 shape
  • packages/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 green
  • packages/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)

Comment thread packages/core/src/services/session-pr-service.ts Outdated
Comment thread packages/core/src/tools/shell.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/core/src/tools/shell.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts
Comment thread packages/cli/src/serve/routes/session-pr-backfill.test.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 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 audit

Verdict: drift (growth-audit.json). KISS axis failed on the accumulated per-corner text gates around the gh pr create detector (exit-code gate, last-URL-wins, origin repo key) — R2-1's reproduced entrances prove no text-gate composition can attribute a printed URL to gh's own execution. The named simpler alternative (make gh the attribution authority; shrink text matching to an execution gate) was implemented FIRST this round, and it is also the R2-1 fix. Minimal-change axis passed: every hunk traces to the PR's feature or an accepted finding. The round stayed subtractive where possible: the heuristic detector, the transcript-URL backfill source, and the per-corner gates were deleted rather than extended.

Feedback dispositions

Finding Severity Disposition
R2-1 (rc:3837318249) Critical Fixed — structural attribution
R2-2 (rc:3837318251) Critical Fixed — write-time validation
R2-3 (rc:3837318255) Critical Fixed — convention reposition
R2-4 (rc:3837318256) Critical Resolved by design — raw map deleted
R2-5 (rc:3837318258) Critical Fixed — gh-page repo-key gate
R1-1 (rc:3837318261) Critical Fixedpr-binding notify → catalog mark
R1-7 (rc:3837318262) Critical Fixed — cross-process sidecar lock
R2-6…R2-9, R1-8, R1-10, R1-12…R1-16, R1-21, R1-23 Suggestion Deferred to next round (batch cap: 7 Criticals first; each answered on its thread)

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 --help / commented-out gh → bind 42 although gh never created anything).

Fix — gh is the attribution authority:

  • Live path (shell.ts): the execution gate (commandRunsGhPrCreate, now exported) is the only remaining text match; the binding is then the PR gh pr view --json number,url resolves for the working branch (fetchCurrentBranchPullRequest in github-prs.ts), accepted only when that URL also appears in the command's output. Text-matched URLs never bind on their own; when gh cannot resolve, nothing binds (fail-closed). This closes the whole forged-URL class at once — including corner A, where the created PR 100 now binds instead of the echoed 42 — and subsumes the deleted --dry-run/last-URL/repo-key gates. Side effect for fork layouts: gh pr view resolves the repo gh actually targets, so live binds work there by construction (noted on the deferred R2-8 thread).
  • Backfill: the transcript-URL source (collectGhPrCreateBindings + gating loop + URL fallback) is deleted entirely. A printed historical URL cannot be attributed to the session's own create, and persisting it is exactly the retroactive forging R2-1 names; what gh cannot vouch for stays unbound (branch mapping + convention still recover history). This also dissolves R2-4: the raw ungated candidate.direct map no longer exists, so no rejected URL can be resurrected.

Witnesses: new shell tests (attribution positive, corner-A flip, three forged shapes, --help gate) and the replaced/added backfill tests; mutation probes: a text-only-binding mutant and an output-gate removal both flip the suite; all restored green.

R2-2 — write-time validation (fixed)

Reproduced on committed code: a 2130-char same-repo URL and an ESC-bearing URL persisted through upsertSessionPr; readSessionPrs then returned null for the WHOLE list and the next upsert rebuilt from [] (prior entry lost). Fix: upsertSessionPr builds the entry and declines it through the same isValidSessionPr the reader uses — one validator, both boundaries. Witnesses: two new tests (over-long URL, control char); probe: removing the check flips both.

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 createdAt) while still counting alreadyBound; branch-mapped numbers keep the plain skip. Witness: new two-run test (run 1 binds convention 42 with gh unavailable; run 2 binds 11 branch-mapped PRs) reads the sidecar back and pins 42 at the tail; probe: restoring the plain skip evicts 42 and fails the test.

R2-5 — fork-layout gh-list gate (fixed)

Fix: while building numberToUrl/numberToState/branchToNumber, entries whose URL repo key differs from the workspace origin key are skipped (fork layout: gh pr list resolves the parent repo; unknown remote admits entries as before). Witness: new fork-shape test (parent-repo PR with a colliding head branch binds nothing); on the committed code it binds the stranger's PR (red); probe: removing the gate flips it.

R1-1 — badge never appears in live-state workspaces (fixed)

The child persists the sidecar, then notifies the daemon via a new qwen/notify/session/pr-binding ext-notification (wired in Session.ts through a new SessionService callback seam, mirroring the automatic-title pattern). The BridgeClient demux validates the payload and invokes the EXISTING onSessionCatalogChanged seam — the same catalog-clock mark automatic titles use — so the catalog revision bumps and version-watching clients refetch the binding within the ~2s live-state poll. Witnesses: bridge.test.ts (revision bump on a valid notification; malformed payloads — missing/invalid number/url/sessionId/v — drop without marking); shell.test.ts asserts the emit; probe: removing the catalog mark flips the bridge test.

R1-7 — cross-process sidecar races (fixed)

Fix: every sidecar mutation (upsertSessionPr/updateSessionPrStates share the choke point) now runs under a two-tier lock — the existing in-process queue inside, a proper-lockfile file lock outside — mirroring the mailbox precedent (same retry/stale options shape). Covers the child live binder vs daemon GitDialog/backfill/sweep writers in both directions; the lock targets the sidecar path via its sibling .lock directory, so atomicWriteJSON's rename swap never disturbs it. Witness: new test holds the file lock externally and proves a mutation waits for release instead of interleaving; probe: removing the lock resolves immediately and fails the test.

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 (comment-replies.json), including interaction notes where this round's restructure changed the seam under the finding (R2-7, R2-8, R1-10, R1-15, R1-23).

Notes

  • Design doc updated to match: gh-attribution mechanism, transcript-URL source removal, fork-layout gh-page gate, convention reposition.
  • Full core suite shows 82 failures in unrelated files (logger, ide-client, editor, storage paths, …) that appear only in the full-parallel run, pass in isolation, and have zero import overlap with this diff — pre-existing concurrency flakes of this runner, not regressions (evidence: logger.test.ts green in isolation; no failing file imports session-pr/shell/github-prs code).
  • Failed checks in the feedback were all Signal the reviewed fork PR: CANCELLED (workflow signal superseded by new pushes), not code-check failures.

Verification

Commands actually run this round (results):

  • npm run build — passed (0 TS errors), re-run after every edit batch
  • npm run typecheck — passed (0 errors)
  • npm run lint — passed (0 errors/warnings)
  • npx prettier --check on all 14 changed source/test files — passed
  • Focused Vitest (core): session-pr-service.test.ts + shell.test.ts + github-prs.test.ts + sessionService.test.ts — 563 passed
  • Focused Vitest (cli): session-pr-backfill.test.ts + session-pr-refresh.test.ts + acp-http/transport.test.ts — 373 passed; Session.test.ts — 667 passed; Session.worktree.test.ts + Session.review-lease.test.ts — 12 passed; server.test.ts + multi-workspace-sessions.test.ts — 1199 passed; full src/acp-integration — 1602 passed
  • Focused Vitest (acp-bridge): bridge.test.ts + bridgeClient.test.ts — 888 passed
  • Reproduction probes vs committed code: R2-1 entrances 1/3/4/5 + corner A all bind forged numbers; R2-2 poison → whole-list null + prior entry lost (probe output quoted above)
  • Mutation probes (each flipped its witness, then restored green): R2-3 reposition guard, R2-5 repo-key gate, R2-2 write validation, shell output-presence gate, gh-attribution vs text-only binding, R1-7 file lock, R1-1 catalog mark, shell emit
  • Integration tests: not run — the touched behavior is exercised by the focused unit suites above, not only through the bundled CLI/integration harness.
中文说明

Autofix 轮次总结 — PR #9739(评审第 3 轮)

增长审计

结论:drift(见 growth-audit.json)。KISS 轴在 gh pr create 检测器周围累积的按角落文本闸门(退出码闸门、最后一个 URL 优先、origin 仓库 key)上判为不通过——R2-1 实测的各入口证明任何文本闸门组合都无法把打印出的 URL 归因到 gh 自身的执行。本轮优先实现了命名的更简替代方案(以 gh 为归因权威、文本匹配收窄为执行闸门),它同时就是 R2-1 的修复。最小变更轴通过:每个 hunk 均可追溯到本 PR 的功能或已接受的发现。本轮尽可能做减法:删除了启发式检测器、transcript URL 回填源和按角落闸门,而不是继续叠加。

反馈处置

发现 严重级 处置
R2-1 (rc:3837318249) Critical 已修复 — 结构化归因
R2-2 (rc:3837318251) Critical 已修复 — 写入时校验
R2-3 (rc:3837318255) Critical 已修复 — 约定号重排
R2-4 (rc:3837318256) Critical 已从设计上消除 — 原始 map 已删除
R2-5 (rc:3837318258) Critical 已修复 — gh 页仓库 key 闸门
R1-1 (rc:3837318261) Critical 已修复pr-binding 通知 → catalog 标记
R1-7 (rc:3837318262) Critical 已修复 — 跨进程 sidecar 锁
R2-6…R2-9、R1-8、R1-10、R1-12…R1-16、R1-21、R1-23 Suggestion 延后到下一轮(批次上限:7 条 Critical 优先;每条均已在各自线程回复)

R2-1 — 检测器归因类问题(已修复)

已对提交代码实测复现全部五个入口(探针输出:退出码为 0 的失败复合命令 → 绑定 1234;覆盖式 echo → 实际创建 100 却绑定 42;引号内短语 / 多行 --help / 注释掉的 gh → gh 什么都没创建却绑定 42)。

修复——以 gh 为归因权威:

  • 实时路径shell.ts):执行闸门(commandRunsGhPrCreate,已导出)是唯一保留的文本匹配;绑定对象改为 gh pr view --json number,urlgithub-prs.tsfetchCurrentBranchPullRequest)为工作分支解析出的 PR,且仅当该 URL 同时出现在命令输出中才绑定。文本匹配出的 URL 永不单独成绑;gh 无法解析时一律不绑(fail-closed)。这一次性关闭了整个伪造 URL 类别——包括 corner A:现在绑定实际创建的 100 而不是被 echo 的 42——并取代了已删除的 --dry-run/最后 URL/仓库 key 闸门。对 fork 布局的附带效果:gh pr view 解析的是 gh 实际 targets 的仓库,因此实时绑定在 fork 场景按构造即可生效(已在延后的 R2-8 线程中注明)。
  • 回填:transcript URL 源(collectGhPrCreateBindings + 闸门循环 + URL 兜底)整体删除。历史打印的 URL 无法归因到会话自身的创建,持久化它正是 R2-1 点名的追溯固化伪造;gh 无法背书的保持不绑(分支映射 + 约定源仍负责恢复存量)。这同时消解了 R2-4:未闸门过滤的原始 candidate.direct map 已不存在,被拒 URL 无从复活。

见证:新增 shell 测试(归因正向、corner A 翻转、三种伪造形态、--help 闸门)与替换/新增的回填测试;变异探针:纯文本绑定变异体与输出去闸门变异体均使套件翻转;恢复后全绿。

R2-2 — 写入时校验(已修复)

已在提交代码上复现:2130 字符同仓库 URL 与含 ESC 的 URL 均可经 upsertSessionPr 持久化;随后 readSessionPrs 对整个列表返回 null,下一次 upsert 从 [] 重建(已有条目丢失)。修复:upsertSessionPr 先构造条目,再用读取侧同款 isValidSessionPr 拒绝不合法条目——一个校验器,两处边界共用。见证:两条新测试(超长 URL、控制字符);探针:移除该校验两条测试均翻转。

R2-3 — 跨运行约定号被挤出(已修复)

修复:回填循环中,已绑定且属约定权威的号码仍会重新 upsert(移到末位、刷新 createdAt),同时保留 alreadyBound 计数;分支映射号码保持普通跳过。见证:新的双运行测试(RUN1 在 gh 不可用时绑定约定 42;RUN2 绑定 11 个分支映射 PR)回读 sidecar 并钉住 42 位于末位;探针:恢复普通跳过会使 42 被挤出、测试失败。

R2-5 — fork 布局 gh 列表闸门(已修复)

修复:构建 numberToUrl/numberToState/branchToNumber 时,URL 仓库 key 与 workspace origin key 不一致的条目一律跳过(fork 布局下 gh pr list 解析的是父仓库;远端未知时仍允许条目通过,与原行为一致)。见证:新的 fork 形态测试(父仓库 PR + 撞名 head 分支,绑定数为 0);在提交代码上该测试为红(会绑定陌生人的 PR);探针:移除闸门使测试翻转。

R1-1 — live-state workspace 中 badge 永不出现(已修复)

子进程持久化 sidecar 后,经新增的 qwen/notify/session/pr-binding ext-notification 通知 daemon(在 Session.ts 中通过 SessionService 新增的回调缝隙接线,复刻自动标题的模式)。BridgeClient 的 demux 校验载荷后调用既有onSessionCatalogChanged 缝隙——与自动标题相同的 catalog 时钟标记——使 catalog revision 递增,版本监听客户端在 ~2s 的 live-state 轮询内 refetch 到绑定。见证:bridge.test.ts(合法通知使 revision 递增;畸形载荷——缺失/非法 number/url/sessionId/v——一律丢弃且不标记);shell.test.ts 断言 emit;探针:移除 catalog 标记使 bridge 测试翻转。

R1-7 — 跨进程 sidecar 竞态(已修复)

修复:所有 sidecar 变更(upsertSessionPr/updateSessionPrStates 共享同一咽喉点)现在运行在两级锁之下——内层为既有的进程内队列,外层为 proper-lockfile 文件锁——复刻 mailbox 先例(重试/stale 参数同形)。覆盖子进程实时绑定与 daemon 侧 GitDialog/回填/扫描写入者的双向竞态;锁经由同级 .lock 目录作用于 sidecar 路径,atomicWriteJSON 的 rename 换文件不会扰动锁。见证:新测试在外部持有文件锁,证明变更会等待释放而不是穿插;探针:移除锁后变更立即完成、测试失败。

延后的建议(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 延后到下一轮;每条均已在各自线程回复(comment-replies.json),其中对本轮重构改变了发现所依赖缝隙的条目(R2-7、R2-8、R1-10、R1-15、R1-23)附加了交互说明。

备注

  • 设计文档已同步更新:gh 归因机制、transcript URL 源移除、fork 布局 gh 页闸门、约定号重排。
  • core 全量套件中有 82 条失败位于无关文件(logger、ide-client、editor、storage 路径等),仅在全量并行运行时出现、单独运行均通过、且与本 diff 零导入重叠——属于本 runner 既有的并行抖动,并非回归(证据:logger.test.ts 单独运行通过;失败文件均不导入 session-pr/shell/github-prs 代码)。
  • 反馈中的失败检查全部为 Signal the reviewed fork PR: CANCELLED(工作流信号被新推送取代),并非代码检查失败。

验证

本轮实际执行的命令(结果):

  • npm run build — 通过(0 个 TS 错误),每批改动后重跑
  • npm run typecheck — 通过(0 错误)
  • npm run lint — 通过(0 错误/警告)
  • 对全部 14 个改动的源码/测试文件执行 npx prettier --check — 通过
  • 聚焦 Vitest(core):session-pr-service.test.ts + shell.test.ts + github-prs.test.ts + sessionService.test.ts — 563 通过
  • 聚焦 Vitest(cli):session-pr-backfill.test.ts + session-pr-refresh.test.ts + acp-http/transport.test.ts — 373 通过;Session.test.ts — 667 通过;Session.worktree.test.ts + Session.review-lease.test.ts — 12 通过;server.test.ts + multi-workspace-sessions.test.ts — 1199 通过;src/acp-integration 全目录 — 1602 通过
  • 聚焦 Vitest(acp-bridge):bridge.test.ts + bridgeClient.test.ts — 888 通过
  • 针对提交代码的复现探针:R2-1 入口 1/3/4/5 + corner A 全部绑出伪造号码;R2-2 投毒 → 整表 null + 已有条目丢失(探针输出见上文)
  • 变异探针(每项均使其见证翻转、随后恢复全绿):R2-3 重排守卫、R2-5 仓库 key 闸门、R2-2 写入校验、shell 输出在场闸门、gh 归因 vs 纯文本绑定、R1-7 文件锁、R1-1 catalog 标记、shell emit
  • 集成测试:未运行——本轮触及的行为由上述聚焦单测覆盖,并非仅经打包 CLI/集成 harness 行使。

Deferred non-Critical feedback

Critical-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). (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 137 / 测试 450 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 @qwen-code /retry 可开启新的计数窗口。)

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 qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 state constraint (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 state
  • packages/cli/src/serve/server/session-pr-refresh.ts:161 — [review] env? option on startSessionPrRefreshTimer is a dead switch
  • packages/acp-bridge/src/bridge.ts:9682 — [review] state validation clauses untested at all three write gates
  • packages/cli/src/serve/acp-http/dispatch.ts:2946 — [review] ACP update_metadata state passthrough untested
  • packages/cli/src/serve/server/session-pr-refresh.ts:157 — [review] startSessionPrRefreshTimer timer lifecycle untested
  • packages/cli/src/serve/routes/session-pr-backfill.ts:255 — [probe] alreadyBound+unresolved double count for unresolvable convention numbers
  • packages/cli/src/serve/routes/session-pr-backfill.ts:117 — [review] backfill pagination loop untested
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:179 — [probe] sweep merged-skip test vacuous
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:113 — [probe] sweep archived half untested
  • packages/web-shell/client/components/sidebar/SessionDetailsTooltip.tsx:171 — [probe] tooltip state suffix untested
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:203 — [probe] backfill gh query contract unpinned
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:280 — [probe] non-convention alreadyBound skip untested
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:113 — [probe] sweep pagination loop untested
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:280 — [review] 'without rewriting the sidecar' test name contradicts behavior
  • packages/cli/src/serve/routes/session-pr-backfill.ts:286 — [review] backfill/sweep iterate listAll() including internal live-conversation runtimes
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:46 — [review] draft→open normalization unpinned
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:160 — [review] one-gh-call-per-workspace property unpinned
  • packages/web-shell/client/components/SessionPrBadge.test.tsx:50 — [review] badge tests never pin the base layout class
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:733 — [probe] route aggregation test never asserts the trusted entry
  • packages/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)

Comment thread packages/core/src/tools/shell.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts
Comment thread packages/cli/src/serve/server/session-pr-refresh.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts
Comment thread packages/core/src/tools/shell.ts Outdated
Comment thread packages/core/src/tools/shell.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts Outdated
Comment thread packages/cli/src/serve/routes/session-pr-backfill.ts
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

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: 2436 passed · 0 failed · 2436 total

Flakiness gate: ⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:2436 通过 · 0 失败 · 2436 总计

抖动门:⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

PR #9739 Deep Verification — feat(core): bind PRs created via gh pr create in the session shell

Verdict: findings — 2436 scripted assertions executed, 2436 passed, 0 failed. Central claim proven load-bearing by A/B; two non-blocking findings (a stale PR description/Reviewer Test Plan, and a gh pr new alias gap in the live hook).
Verified head: c7f92387247d15dd09407306c405bf7c0c66067d (git rev-parse HEAD^2), merged over base tip 22bb5e8b9f.

中文摘要

结论:findings(2436 项脚本断言全部通过,0 失败;两个非阻塞发现)。

  • A/B 结论:中心主张成立。在受控假 gh(真实进程、真实文件系统、编译后的 dist)驱动下,head 构建执行 gh pr create 后会以 state:'open', source:'create' 写入会话 PR sidecar 并发出 pr-binding 通知;base 构建同场景零写入(见 01-ab-live-hook-head-vs-base.png)。六类敌对单元(重试既有 PR、预取快照出错、dry-run、创建失败、grep 短语、输出重定向)在 head 上全部正确不绑定;回填路由 21 项检查全过(/review 与 worktree 约定两源、fork 布局取父仓库 URL、无 origin 时 fail-closed、provenance 分级驱逐、伪造 traversal sessionId 拒绝、base 不存在该路由),见 02-backfill-sources-and-gates.png
  • 变异矩阵:5/5 守卫被同文件测试杀死,未变异对照绿(03-mutation-matrix-all-killed.png);定向门禁 7 个套件共 2386 测试全绿(04-targeted-gates-green.png)。
  • 发现(均非阻塞)
    1. PR 描述与 Reviewer Test Plan 过期:正文仍宣称回填路由含"transcript gh pr create 痕迹"第三来源,且测试计划第 3 步要求验证它——该来源在后续评审轮被有意移除(代码注释、设计文档、以及名为 "source removed" 的测试都钉住了移除)。按现代码该测试步骤不可能通过。仅需更新描述,无需改代码。
    2. gh pr new(gh 文档别名)不触发实时绑定:执行闸门只匹配 pr create;同文件的归因改写器却同时处理两者。实测 gh pr new 成功创建(输出含 URL)但无绑定——假阴性,无错误数据。建议修复((?:create|new))已在 scratch 构建实测:新形状绑定、旧形状逐字节不变、grep 防误报不受影响;现有测试对该轴未钉住,需补一条 gh pr new 夹具。
  • 未覆盖:逐提交归因(depth-2 浅克隆本地仅 3 个提交,元数据列 22 个);daemon 端到端(badge ~2s 渲染、SSE 传播——两半分别验证:子进程写 sidecar+发通知、bridge 校验+标记 catalog 各有测试/线级证据,但未串成真实 daemon 会话);刷新定时器真实 5 分钟轮询(仅单测门禁);Windows/Linux 专属路径(作者声明无);Session.test.ts 全量(仅 +3 行 mock 桩,跑了同批次的两个姊妹套件)。

Central claim + A/B

Claim: a gh pr create run in the session shell binds the created PR to the session — at head, via a shell-tool post-hook for which gh itself is the attribution authority (pre-run gh pr view snapshot, post-run resolution must be OPEN, its URL must appear in the captured output, and it must not be the branch's pre-existing PR); at base no such path exists.

Harness: run-cell.mjs drives the compiled ShellToolInvocation.execute() with the real ShellExecutionService (real bash spawn), a real SessionService (real fs sidecar paths under a scratch git repo whose origin is github.com/test-owner/test-repo), and a gh shim implementing real CLI semantics (pr create persists the PR; pr view resolves it afterwards — absent/error otherwise). Base arm: HEAD^1 rebuilt in a scratch worktree (core only, ~40 s; no dependency-tree changes in the PR, so reusing the root node_modules is a clean control — asserted: base dist/src/tools/shell.js contains no bindGhPrCreate, and compiled core modules carry no cross-workspace imports). Raw per-cell observations: logs/cell-*.json. Witness: evidence/01-ab-live-hook-head-vs-base.png.

# Cell Arm Oracle (sidecar file / notification) Result
1 gh pr create --title x --body y head [{number:42, url:…/pull/42, state:'open', source:'create'}], 1 notification bound
1 same base no sidecar, no notification absent ✅
2 retry: gh pr create --fill || gh pr view … with PR 41 pre-existing head no sidecar (output carried …/pull/41, exit 0) declined ✅
2 same base no sidecar absent ✅
3 pre-run snapshot errors (rate-limit shape), create succeeds head no sidecar — fail closed despite a real PR declined ✅
4 gh pr create --dry-run (URL-shaped output, nothing created) head no sidecar declined ✅
5 create exits 1 head no sidecar (non-zero exit gate) declined ✅
6 grep -rn 'gh pr create' notes.txt with a PR URL in output head no sidecar (phrase is an argument, not a segment start) declined ✅
7 gh pr view 41 (non-create) head no sidecar declined ✅
8 create succeeds but > /dev/null hides the URL head no sidecar (output must carry gh's URL) declined ✅
9 static: base dist shell.js base no bindGhPrCreate/commandRunsGhPrCreate symbols absent ✅

16/16 scripted checks passed (ab-matrix.mjs).

Secondary claim — backfill route (new at head; absent at base by git cat-file): 21/21 checks passed (backfill-harness.mjs): /review 42source:'review', state:'open'; /review <same-repo url> binds; worktree slug pr-7source:'worktree', state:'merged'; cap/eviction (11 reviews + convention → 10 persisted, convention survives, oldest reviews evicted first; re-run idempotent with alreadyBound); fork layout binds the PARENT repo URL, not a synthesized fork URL, and accepts /review <parent-url> while rejecting a third repo's URL; no origin → fail closed (bound:0, unresolved:2); transcript gh pr create call/response pairs bind nothing (source removed); forged traversal sessionId in a transcript's first record is rejected before path construction; pr-0/pr-007 slugs rejected; base tree has neither the route file nor the backfill-prs registration.

Corrections (to the PR description)

  • The body's "Retroactive" paragraph and Reviewer Test Plan step "Re-run POST /sessions/backfill-prs on a workspace with old transcripts containing gh pr create runs and confirm bound counts them" describe a source that does not exist at head. The backfill route header states transcript gh pr create traces are "deliberately NOT a source" (no gh-side attribution per historical command; text alone could forge a binding), the design doc records the removal, and session-pr-backfill.test.ts pins it ("does not bind PRs from transcript gh pr create traces (source removed)"). The removal is a deliberate security hardening across review rounds — but the description still advertises the old behavior and the test plan's step is structurally unsatisfiable. This is a description correction, not a code-change request.
  • Likewise, the body's "extracts the PR URL that gh prints on success" understates the final mechanism: attribution is gh-resolved (fetchCurrentBranchPullRequest before and after the run), with an errored pre-run snapshot failing closed and pre-existing PRs declined — strictly stronger than the text says.

Findings

F1 — gh pr new (documented alias) bypasses the live binding hook — Low

commandRunsGhPrCreate gates on gh(?:\.exe)?\s+pr\s+create\b; gh pr new — which this same file's attribution rewriter explicitly treats as an alias of gh pr create (shell.ts:894-895, 4939) — never enters the hook. Measured on head with a working create (URL in output, exit 0): no sidecar, no notification. Consequence is a false negative only (missing badge; no bad data), and the retroactive path no longer exists to recover it.

Reproduce:

node tmp/pr9739-verify-20260825-091359/run-cell.mjs   # CELL_COMMAND='gh pr new --title x --body y', see logs/ (cell printed sidecar: null)
Measured candidate fix (one line)

packages/core/src/services/session-pr-service.ts:

 const GH_PR_CREATE_SEGMENT_PATTERN =
-  /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*gh(?:\.exe)?\s+pr\s+create\b/;
+  /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*gh(?:\.exe)?\s+pr\s+(?:create|new)\b/;

Applied in a scratch worktree, rebuilt core, re-driven through the same wire harness:

  • gh pr new --title x --body ybinds [{number:42, url:…/pull/42, state:'open', source:'create'}]
  • gh pr create --title x --body y → byte-identical binding shape (zero collateral)
  • grep -rn 'gh pr create' … with a PR URL in output → still no binding (anchor intact)
  • commandRunsGhPrCreate suite: 4/4 green both with and without the patch — the suite pins nothing along the alias axis, so the fix should ship with a fixture such as expect(commandRunsGhPrCreate('gh pr new --fill')).toBe(true) beside the existing bare-segment case.

F2 — PR body / Reviewer Test Plan describe the removed transcript backfill source — Medium (docs)

See Corrections. A maintainer following the test plan would spend time on step 3 and conclude a regression where there is an intentional removal. Recommend updating the PR body before merge (the design doc is already correct).

Not covered

  • Per-commit attribution: the merge-ref checkout is depth 2 — git rev-list HEAD^1..HEAD^2 returns 1 locally while the metadata snapshot lists 22 commits, so individual review-round commits were not exercised separately; the aggregate HEAD^1..HEAD diff is what was verified.
  • Full daemon E2E (badge appearing in a live Web Shell within ~2 s): the two halves were verified separately — agent-side sidecar write + qwen/notify/session/pr-binding emission (wire harness) and bridge-side payload validation + catalog mark (784-test bridge.test.ts gate, incl. +226 new pr-binding tests) — but no real daemon + client session was driven.
  • Refresh sweep timing (5-minute timer, first-run delay): unit-gated (session-pr-refresh.test.ts, incl. env-knob resolution and overflow clamp), not observed against a live clock.
  • Session.test.ts full suite (huge; its +3 lines are a setSessionPrBoundCallback mock stub) — the sibling Session.review-lease / Session.worktree suites with the identical stub change were run green.
  • Repo-wide lint/typecheck/test gates were not claimed; the environment's pre-verification build (tsc emit) at head is the type evidence, and only the affected workspaces' suites were run. One base-side rebuild emitted under a pre-existing @lydell/node-pty TS7016 type-declaration notice (environmental: same import on both arms; emit unaffected, modules load).
  • Windows/Linux-specific paths (author declares none; no OS conditionals found in the changed surface).

Methodology

All harnesses ran against compiled dist output (head: CI-built at the merge commit; base: packages/core rebuilt in a tmp/base-tree worktree at HEAD^1, reusing root node_modules — clean control because the PR changes no package.json/lockfile, and the base dist was asserted free of the PR's symbols and cross-workspace imports). The live-hook harness (run-cell.mjs) drives the real ShellToolInvocation/ShellExecutionService/SessionService against a scratch git repo per cell; gh is a shim (fake-gh) implementing real semantics (create persists state; view resolves it; error/absent modes), so pre/post attribution calls go through the actual execFile path. Config stubs exist only at the telemetry/permission boundary surface the foreground cells never exercise. The backfill harness drives the real backfillWorkspaceSessionPrs with real transcript/worktree fixtures and a real gh pr list shim; membership, path layout, and isValidSessionId gates all run unmocked. Mutations were applied as single-hunk source edits in a separate worktree and run through the repo's vitest (mutation-matrix.mjs), each reverted afterwards (worktree clean asserted). Gates: npx vitest run per affected file from each package. Raw logs in logs/; rerunnable harnesses: ab-matrix.mjs, backfill-harness.mjs, mutation-matrix.mjs, gates-summary.sh.

Flakiness gate log

rounds=5 files=16 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/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/process-env-guard.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/process-env-guard.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/session-pr-refresh.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/session-pr-refresh.test.ts
file packages/core/src/config/config-session-env.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config-session-env.test.ts
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/session-pr-service.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/session-pr-service.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
file packages/web-shell/client/components/SessionPrBadge.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/SessionPrBadge.test.tsx
file packages/web-shell/client/components/dialogs/GitDialog.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/dialogs/GitDialog.test.tsx


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/acp-bridge/src/bridge.test.ts: PPP
  packages/cli/src/acp-integration/session/Session.review-lease.test.ts: PPP
  packages/cli/src/acp-integration/session/Session.test.ts: PPP
  packages/cli/src/acp-integration/session/Session.worktree.test.ts: PPP
  packages/cli/src/serve/fast-path-open.test.ts: PPP
  packages/cli/src/serve/process-env-guard.test.ts: PPP
  packages/cli/src/serve/routes/session-pr-backfill.test.ts: PPP
  packages/cli/src/serve/server.test.ts: PPP
  packages/cli/src/serve/server/session-pr-refresh.test.ts: PPP
  packages/core/src/config/config-session-env.test.ts: PPP
  packages/core/src/config/config.test.ts: PPP
  packages/core/src/services/session-pr-service.test.ts: PPP
  packages/core/src/tools/shell.test.ts: PPP
  packages/core/src/utils/github-prs.test.ts: PPP
  packages/web-shell/client/components/SessionPrBadge.test.tsx: PP
  packages/web-shell/client/components/dialogs/GitDialog.test.tsx: PP

verdict: timeout
summary: only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- 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/fast-path-open.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/process-env-guard.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/session-pr-refresh.test.ts: P (exit 0)
round 1 · packages/core/src/config/config-session-env.test.ts: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/services/session-pr-service.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 1 · packages/web-shell/client/components/SessionPrBadge.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/dialogs/GitDialog.test.tsx: 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/fast-path-open.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/process-env-guard.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)
round 2 · packages/cli/src/serve/server/session-pr-refresh.test.ts: P (exit 0)
round 2 · packages/core/src/config/config-session-env.test.ts: P (exit 0)
round 2 · packages/core/src/config/config.test.ts: P (exit 0)
round 2 · packages/core/src/services/session-pr-service.test.ts: P (exit 0)
round 2 · packages/core/src/tools/shell.test.ts: P (exit 0)
round 2 · packages/core/src/utils/github-prs.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/SessionPrBadge.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/dialogs/GitDialog.test.tsx: P (exit 0)
round 3 · packages/acp-bridge/src/bridge.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.review-lease.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.worktree.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/fast-path-open.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/process-env-guard.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/routes/session-pr-backfill.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server/session-pr-refresh.test.ts: P (exit 0)
round 3 · packages/core/src/config/config-session-env.test.ts: P (exit 0)
round 3 · packages/core/src/config/config.test.ts: P (exit 0)
round 3 · packages/core/src/services/session-pr-service.test.ts: P (exit 0)
round 3 · packages/core/src/tools/shell.test.ts: P (exit 0)
round 3 · packages/core/src/utils/github-prs.test.ts: P (exit 0)

Evidence images

01-ab-live-hook-head-vs-base

02-backfill-sources-and-gates

03-mutation-matrix-all-killed

04-targeted-gates-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

The bot already has a review of its own on c7f92387247d15dd09407306c405bf7c0c66067d, which still stands.

机器人在 c7f92387247d15dd09407306c405bf7c0c66067d 上已有自己的评审,且仍然有效。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary

Budget warning in effect: the previous round expired without completing, so this round addressed the smallest blocking subset — the outstanding Critical findings verified at the current head — committed as soon as complete, and deferred every other unresolved item explicitly (see comment-replies.json, one reply per still-open thread).

Resolved this round (commit 2c2c22c)

All seven are round-11 Criticals re-verified at this head; each fix has a focused witness test, and every guard was mutation-probed (removed → witness fails → restored):

Finding Fix Witness
R10-4 — pre-run snapshot had no branch identity; an in-command branch switch bound the new branch's existing PR Snapshot now requests headRefName; the binder captures the pre-run branch (git branch --show-current) and declines on mismatch or when the branch is unproven does not bind when the command switched branches mid-run, does not bind when the pre-run branch could not be captured
R10-6 — backfill re-runs rotated the sidecar once non-re-offered occupants existed Backfill pre-reads the sidecar and offers only FREE slots (strongest last); a full sidecar reports alreadyBound and skips the write entirely stays idempotent when non-re-offered occupants hold slots (byte-stable across runs); two cap tests updated to the new contract
R10-7 — divergent gh page (gh repo set-default/remaining remotes) fed bare-number bindings Page URL/state maps are consumed only when the page is the workspace repo or a CONFIRMED fork parent (gh repo view --json url,parent); /review <url> forms bind the user-named URL itself does not resolve bare numbers through a divergent gh page, binds the user-named URL of a /review url form without a trusted page
R10-11 — archive/unarchive moved PR sidecars outside the lock and queue New moveSessionPrSidecar serializes on both in-process queues and holds cross-process locks on BOTH endpoints (path-sorted lock order); the shell binder re-resolves the session's archive location immediately before its locked mutation waits for a held sidecar lock before moving, waits for a lock held on the destination before moving, writes the binding to the archived sidecar when the session was archived mid-run; sessionService suite re-pinned to the delegation contract
R11-1 — metadata surfaces stamping source:'create' downgraded worktree convention bindings upsertSessionPr now keeps the higher-authority source: an explicit source upgrades (review→create) but never downgrades (worktree survives) never downgrades the persisted provenance on a weaker explicit source
R11-2 — no repo at cwd returned none (proved absence) instead of error fetchCurrentBranchPullRequest fails closed with error; both call sites already declined on it reports error outside a git repository without spawning gh
R11-3 — repo identity mutable inside the snapshot window (git remote set-url, gh repo set-default) New fetchAttributionRepoKeys captures the pre-run repo identity (resolved repo + fork parent — fork creates legitimately carry the parent's key); the binder declines any post-run PR outside that set, failing closed when unresolvable does not bind a PR outside the pre-run repo identity, does not bind when the pre-run repo identity could not be resolved, fork layout still binds: binds a fork-layout create attributed to the parent repo

Also landed: the R10-12 JSDoc correction (the binder doc no longer claims "the number did not already exist before the run"; it states the per-session scope of the proof and discloses the shared-checkout window).

Re-verified as already fixed by prior commits (resolved, no new code)

  • rc:3847988962 — bare-number digit-run alternative now closes with (?![\w./-]) (blocks file paths).
  • rc:3847988976 — commandRunsGhPrCreate segment split includes \n.
  • rc:3847988994 — the snapshot distinguishes error from none and the binder declines on error.

Partially addressed

  • R10-12 (rc:3847989002, Critical) — the cross-session race on a shared checkout is real. The doc claim was corrected this round; the race fix (serializing the snapshot→spawn→bind window per git-root+branch, or declining when the create segment's success is unproven) is deferred to the next round — see the thread reply.

Deferred to the next round

122 unresolved comments (Critical and Suggestion findings from rounds 1–10 that round 11 did not re-post) each received a thread reply deferring them to the next round for re-verification against the new head. Two pre-existing cap-contract tests were updated because R10-6's fix supersedes their old "evict seeded occupants to land every new binding" expectation — the new contract never evicts persisted occupants from backfill.

Conflict notes

None — --conflict false; origin/main was not merged.

Verification

Commands actually run this round (all after the final code state unless noted):

  • npm run build — passed (0 TS errors)
  • npm run typecheck — passed (0 TS errors)
  • npm run lint — passed
  • npx prettier --check on all 10 changed files — passed
  • cd packages/core && npx vitest run src/services/session-pr-service.test.ts src/utils/github-prs.test.ts src/tools/shell.test.ts src/services/sessionService.test.ts — 4 files, 609 passed
  • cd packages/cli && npx vitest run src/serve/routes/session-pr-backfill.test.ts src/serve/server/session-pr-refresh.test.ts — 2 files, 55 passed
  • 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/acp-http/transport.test.ts — 4 files, 1476 passed
  • Mutation probes (guard removed → focused witness fails → guard restored → green), one per new guard: R11-2 fail-closed, R11-1 no-downgrade, R10-4 branch guard (2 witnesses), R11-3 repo guard (2 witnesses), R10-11 move destination lock, R10-11 binder archive re-resolve, R10-6 free-slot trim, R10-7 page gate — all 8 probes failed on the mutant and pass restored
  • Debugging a hang in the binding suite exposed a pre-existing race in the local runShell test helper (stale resolver on repeat invocations once the pre-spawn snapshot awaits); the helper now waits for a NEW service call by call count

Integration tests were not run: the touched behavior is covered by the unit suites above, matching the skipped integration lanes of prior pushes. Settings sources were not changed, so no schema regeneration was needed.

中文说明

Autofix 审查轮次总结

预算警告生效中:上一轮超时未完成,因此本轮处理了最小的阻塞子集——在当前 HEAD 上已核实的未解决 Critical 发现——完成后立即提交,并将其余所有未解决项逐一显式推迟(见 comment-replies.json,每个仍开启的线程一条回复)。

本轮已解决(提交 2c2c22c

以下七项均为第 11 轮在当前 HEAD 上重新核实的 Critical;每项修复都有针对性的见证测试,且每个守卫都做了变异探测(移除守卫 → 见证测试失败 → 恢复守卫):

发现 修复 见证
R10-4 —— 运行前快照缺少分支身份;命令中途切换分支会绑定新分支已有的 PR 快照现在请求 headRefName;绑定端捕获运行前分支(git branch --show-current),不匹配或分支无法证实时拒绝绑定 does not bind when the command switched branches mid-rundoes not bind when the pre-run branch could not be captured
R10-6 —— 一旦存在不会被重新提供的占用项,回填重跑会使 sidecar 轮换 回填先读 sidecar,仅提供空闲槽位(最强者靠后);sidecar 已满时报告 alreadyBound 并完全跳过写入 stays idempotent when non-re-offered occupants hold slots(各轮之间字节级稳定);两个上限测试按新契约更新
R10-7 —— 分歧的 gh 页面(gh repo set-default/剩余 remote)为纯编号绑定提供数据 仅当页面是工作区仓库本身或已确认的 fork 父仓库(gh repo view --json url,parent)时才消费页面 URL/状态映射;/review <url> 形式直接绑定用户指明的 URL does not resolve bare numbers through a divergent gh pagebinds the user-named URL of a /review url form without a trusted page
R10-11 —— 归档/取消归档在锁与队列之外移动 PR sidecar 新增 moveSessionPrSidecar:在两个进程内队列上串行,并对两个端点同时持有跨进程文件锁(按路径排序的加锁顺序);shell 绑定端在被锁变更前立即重新解析会话的归档位置 waits for a held sidecar lock before movingwaits for a lock held on the destination before movingwrites the binding to the archived sidecar when the session was archived mid-run;sessionService 套件重新固定到委托契约
R11-1 —— 元数据接口无条件盖 source:'create',会降级 worktree 约定绑定 upsertSessionPr 现在保留更高权威的来源:显式来源可升级(review→create)但永不降级(worktree 得以保留) never downgrades the persisted provenance on a weaker explicit source
R11-2 —— cwd 处无仓库时返回 none(已证实不存在)而非 error fetchCurrentBranchPullRequesterror 失败关闭;两个调用方本就会拒绝该状态 reports error outside a git repository without spawning gh
R11-3 —— 快照窗口内仓库身份可变(git remote set-urlgh repo set-default 新增 fetchAttributionRepoKeys 捕获运行前仓库身份(解析仓库 + fork 父仓库——fork 创建合法地携带父仓库的键);绑定端拒绝该集合之外的运行后 PR,无法解析时失败关闭 does not bind a PR outside the pre-run repo identitydoes not bind when the pre-run repo identity could not be resolved;fork 布局仍可绑定:binds a fork-layout create attributed to the parent repo

同时落地:R10-12 的 JSDoc 更正(绑定端文档不再声称"该编号在运行前不存在",而是说明证明的按会话范围并披露共享 checkout 的窗口局限)。

经重新核实已由先前提交修复(已解决,无新代码)

  • rc:3847988962 —— 纯编号的数字串备选分支现以 (?![\w./-]) 收尾(阻止文件路径)。
  • rc:3847988976 —— commandRunsGhPrCreate 的分段拆分已包含 \n
  • rc:3847988994 —— 快照已区分 errornone,绑定端对 error 拒绝。

部分处理

  • R10-12(rc:3847989002,Critical)——共享 checkout 上的跨会话竞争确实存在。文档表述已在本轮更正;竞争本身的修复(按 git 根+分支串行化快照→spawn→绑定窗口,或在 create 段成功无法证实时拒绝)推迟到下一轮——见线程回复。

推迟到下一轮

122 条未解决评论(第 1–10 轮的 Critical 与 Suggestion 发现,第 11 轮未重新张贴)各收到一条线程回复,推迟到下一轮针对新的 HEAD 重新验证。两个既有的上限契约测试已更新,因为 R10-6 的修复取代了它们旧的"驱逐已播种占用项以容纳全部新绑定"的预期——新契约永不在回填中驱逐已持久化的占用项。

冲突说明

无——--conflict false;未合并 origin/main

验证

本轮实际运行的命令(除注明外均为最终代码状态):

  • npm run build —— 通过(0 个 TS 错误)
  • npm run typecheck —— 通过(0 个 TS 错误)
  • npm run lint —— 通过
  • 对全部 10 个改动文件运行 npx prettier --check —— 通过
  • cd packages/core && npx vitest run src/services/session-pr-service.test.ts src/utils/github-prs.test.ts src/tools/shell.test.ts src/services/sessionService.test.ts —— 4 个文件,609 通过
  • cd packages/cli && npx vitest run src/serve/routes/session-pr-backfill.test.ts src/serve/server/session-pr-refresh.test.ts —— 2 个文件,55 通过
  • 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/acp-http/transport.test.ts —— 4 个文件,1476 通过
  • 变异探测(移除守卫 → 针对性见证失败 → 恢复守卫 → 全绿),每个新守卫一次:R11-2 失败关闭、R11-1 不降级、R10-4 分支守卫(2 个见证)、R11-3 仓库守卫(2 个见证)、R10-11 移动目标端锁、R10-11 绑定端归档重解析、R10-6 空槽修剪、R10-7 页面门控——全部 8 个探测在变异体上失败、恢复后通过
  • 绑定套件中的一个挂起暴露了本地 runShell 测试助手的既有竞争(运行前快照引入 await 后,重复调用会解析陈旧的 resolver);该助手现按调用计数等待新的服务调用

未运行集成测试:本次触及的行为已由上述单元测试覆盖,与先前推送跳过的集成通道一致。未改动设置源,因此无需重新生成 schema。

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

…eate

# Conflicts:
#	.github/workflows/.size-baseline
… feat/session-pr-gh-create

# Conflicts:
#	packages/core/src/services/sessionService.ts
Merge of main's persisted-session lifecycle (QwenLM#9626/QwenLM#9764) with the locked sidecar-move refactor: moveSessionPrSidecar gains the optional generation fence (threaded to writeSessionPrs assertCanCommit), conflict-repair paths merge split PR sidecars under the lock, and the fence tests assert the fence rides on the locked move.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — Test (windows-latest, Node 22.x) was skipped in CI; the Windows-specific parsing this review probed (drive-letter remotes) is not exercised on that OS here.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": verifying whether teammate-bound envelope text delivered via TeamManager.flushNextMessage reaches the recorded user-role request unwrapped (decides reachability…; "agent reverse-audit (round 2)": verifying ACP-session transcripts persist to the same workspace chats dir backfill scans (storage root of the Session.ts recording service).

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Deferred under the convergence posture (round 12, not a blocker) — recorded, not requested in this round:

  • docs/design/2026-08-20-webshell-session-pr-binding.md:39 — [probe] design doc attributes retry-misbinding protection to the open gate; the snapshot comparison is what blocks it
  • packages/acp-bridge/src/bridge.ts:9869 (+2 locations) — [review] state-validation rejection messages omit the state constraint
  • packages/acp-bridge/src/bridge.ts:9961 — [probe] no-op suppression is latest-only; byte-identical re-bind of a non-latest entry still bumps the catalog
  • packages/cli/src/acp-integration/session/Session.ts:8246 (+13 locations) — [probe] 13 new behavior branches have no mutation-pinning test (notification wiring, source stamping, failure isolation, state rejection/round-trip, read-side enum g…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:174 — [review] PR description still claims the removed transcript-pairing backfill source; test-plan step will fail
  • packages/cli/src/serve/routes/session-pr-backfill.ts:390 — [probe] free-slot offer sizing ignores already-bound candidates; fresh numbers permanently unbound, re-runs never converge
  • packages/cli/src/serve/routes/session.ts:2262 (+2 locations) — [review] explicit state:null now rejected (previously ignored); generated clients' combined metadata update dropped
  • packages/cli/src/serve/run-qwen-serve.ts:5191 — [probe] fire-and-forget dynamic import without .catch; rejection kills the daemon under Node >=22
  • packages/cli/src/serve/server/session-list.ts:535 — [probe] at-cap live-only drop heuristic misses the bind-route pre-sidecar-write window; fresh binding invisible at the cap
  • packages/cli/src/serve/server/session-pr-refresh.ts:47 — [probe] interval resolver has no lower bound; sub-minute values spawn the gh busy loop the upper guard exists to prevent
  • packages/cli/src/serve/server/session-pr-refresh.ts:85 — [review] sweep enumerates every transcript to find the handful with sidecars; enumerate *.pr.json instead
  • packages/cli/src/serve/server/session-pr-refresh.ts:161 — [probe] sweep has no per-session failure isolation; one bad sidecar aborts the workspace and loses the catalog bump for rewrites already applied
  • packages/core/src/services/session-pr-service.test.ts:154 (+2 locations) — [probe] foreign-lock test doesn't pin read-inside-lock; batch concurrency test's scenario is structurally unreachable
  • packages/core/src/services/session-pr-service.ts:155 — [probe] class-level: quote-blind hand-rolled shell segmentation — wrappers hide real creates (binding permanently lost), quoted separators forge matches
  • packages/core/src/services/session-pr-service.ts:156 — [probe] execution gate misses gh's documented gh pr new alias while the sibling attribution rewriter handles it
  • packages/core/src/services/session-pr-service.ts:339 — [probe] fresh binding with explicit source:'review' loses provenance to the phantom undefined rank (1 > 0)
  • packages/core/src/services/session-pr-service.ts:340 — [probe] source inheritance crosses a URL change — a different repo's PR inherits the replaced binding's provenance rank
  • packages/core/src/services/sessionService.ts:2023 — [probe] enumerateSessionIdsForArchiveState silently truncates at 10000 despite the FULL-set docstring; sibling surfaces a truncated flag
  • packages/core/src/tools/shell.ts:2758 — [review] binding hook fires only on the foreground settle path; background and promoted creates never bind (background git commit IS refused for this exact reason)
  • packages/core/src/tools/shell.ts:3136 — [review] new comment promises 'backfill recovers the rest' but this diff removed the only source that could
  • …and 3 more (see the run report)

Convergence: round 12 posted 7 inline comment(s), 7 of them reported for the first time; the previous round posted 7 (3 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/session-pr-backfill.ts (findings in round 10; 2 more now); packages/core/src/services/session-pr-service.ts (findings in round 10; 2 more now); packages/core/src/utils/github-prs.ts (findings in round 11; 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.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the Windows-specific parsing this review probed (drive-letter remotes) is not exercised on that OS here。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"verifying whether teammate-bound envelope text delivered via TeamManager.flushNextMessage reaches the recorded user-role request unwrapped (decides reachability…"agent reverse-audit (round 2)"verifying ACP-session transcripts persist to the same workspace chats dir backfill scans (storage root of the Session.ts recording service)

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

收敛姿态下延后(第 12 轮,非阻断)——已记录,本轮不要求修改:共 23 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 12 轮发布了 7 条行内评论,其中 7 条是首次提出;上一轮发布了 7 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/routes/session-pr-backfill.ts(第 10 轮已出过发现,本轮又有 2 条);packages/core/src/services/session-pr-service.ts(第 10 轮已出过发现,本轮又有 2 条);packages/core/src/utils/github-prs.ts(第 11 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +9975 to +9979
entry.prs = (
known && known.url === bound.url
? existing.map((p) => (p.number === bound.number ? merged : p))
: [...existing.filter((p) => p.number !== bound.number), merged]
).slice(-SESSION_PR_LIST_LIMIT);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The in-memory catalog cap here stays positional (.slice(-SESSION_PR_LIST_LIMIT)), while this PR rewrites the persisted sidecar cap from the same positional slice to capSessionPrListByAuthority (provenance-ranked). Both stores are maintained by the same metadata request, so once 11 distinct PR numbers accumulate they evict different entries and diverge for the daemon entry's lifetime — SessionPrInfo carries no source, so this live list cannot reproduce the authority cap. Concrete trigger: a session accumulates its own created PR #1 (source:'create', rank 2) at position 0 plus 9 reviewed PRs #2#10 (rank 0); an 11th number bound via PATCH /session/:id/metadata or ACP update_metadata makes this merge evict #1 positionally (live=[#2..#11]) while upsertSessionPr evicts #2 by authority (persisted keeps #1). Every later session_metadata_updated event publishes prs: entry.prs containing #2 (gone from disk) and missing #1 (present); rename-only PATCH responses echo the same diverged list; seedSessionPrs only hydrates EMPTY entries, so nothing reconciles until daemon restart — event-stream SDK consumers and rename responses serve the wrong bindings indefinitely, directly defeating the new cap's documented invariant that created and convention bindings survive an accumulation of reviewed numbers.

witness (probe driving the real bridge + real upsertSessionPr over a seeded list [create #1 at pos 0, reviews #2#10] plus an 11th bind):

LIVE evicted=[1]; numbers=[2..11]
PERSIST evicted=[2]; numbers=[1,3..11]
DIVERGED: true

Flip (carry source, cap by authority): LIVE evicted=[2]; numbers=[1,3..11], DIVERGED: false.

Make the two stores share one eviction policy: after upsertSessionPr succeeds in the REST/dispatch metadata routes, replace the bridge entry's prs with the authoritative persisted list (a seed variant that overwrites), or carry source into SessionPrInfo and apply capSessionPrListByAuthority here.

中文说明

此处内存 catalog 的上限仍是按位置截断(.slice(-SESSION_PR_LIST_LIMIT)),而本 PR 已把持久化 sidecar 的上限从同样的按位置截断改为按来源权威排序的 capSessionPrListByAuthority。两个存储由同一个元数据请求维护,一旦累积超过 11 个不同的 PR 编号,两者会逐出不同的条目并在 daemon 生命周期内永久分叉——SessionPrInfo 不携带 source,此处的活列表无法复现权威上限。具体触发:会话先积累自己创建的 PR #1source:'create',rank 2,位置 0)和 9 个 review 绑定的 PR #2#10(rank 0);第 11 个编号经 PATCH /session/:id/metadata 或 ACP update_metadata 绑定时,此合并按位置逐出 #1live=[#2..#11]),而 upsertSessionPr 按权威逐出 #2(持久化保留 #1)。此后每个 session_metadata_updated 事件都会发布包含 #2(磁盘上已不存在)、缺失 #1(磁盘上存在)的 prs: entry.prs;仅改名的 PATCH 响应也回显同一分叉列表;seedSessionPrs 只填充空条目,直到 daemon 重启前没有任何机制能对账——事件流 SDK 消费者与改名响应会无限期提供错误绑定,直接破坏新上限“创建与约定绑定应在 review 编号积累中幸存”的既定不变式。探针驱动真实 bridge 与真实 upsertSessionPr(播种 [创建 #1 位于位置 0,review #2#10] 后绑定第 11 个):LIVE evicted=[1]; numbers=[2..11]PERSIST evicted=[2]; numbers=[1,3..11]DIVERGED: true;修复(携带 source 并按权威截断)后 DIVERGED: false。建议让两个存储共享同一逐出策略:REST/dispatch 元数据路由在 upsertSessionPr 成功后,用权威的持久化列表覆盖 bridge 条目的 prs;或让 SessionPrInfo 携带 source 并在此处应用 capSessionPrListByAuthority

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +316 to +320
const { parent } = await fetchRepoKeys(
runtime.workspaceCwd,
runtime.env.effectiveEnv,
);
pageMapTrusted = parent === pageRepoKey;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] This fork-parent confirmation for a divergent gh page ignores resolved, so a gh resolution diverged to ANY fork of the page's repo marks an unrelated page trusted. fetchAttributionRepoKeys returns resolved (the repo gh itself resolved), but production never reads it — the comment above this gate says divergent resolution must never feed bindings. Concrete trigger: workspace origin resolves to repo W (key known); a prior gh repo set-default someone/their-fork in this checkout makes gh's resolution diverge to an unrelated repo R that is a fork of P. gh pr list then lists parent P's PRs (pageRepoKey=P≠W); fetchRepoKeys returns {resolved:R, parent:P}; parent === pageRepoKey is true → pageMapTrusted=true. A worktree-convention session (slug pr-42) then binds P's PR 42 — a stranger's PR via pageUrlByNumber, the exact outcome the numberToUrl fail-closed gate exists to prevent; a /review https://W/pull/5 form also gets P's PR-5 state stamped, and the refresh sweep keys stamps by the binding-URL repo key, so the wrong state is never corrected. The fork-layout test passes only because its mock supplies resolved matching the workspace key — a field production never consumes.

witness (probe, workspace origin https://github.com/me/workspace, mocked fetchAttributionRepoKeys → {resolved:'github.com/someone/their-fork', parent:'github.com/parent/repo'}, page listing https://github.com/parent/repo/pull/42):

persisted binding url = "https://github.com/parent/repo/pull/42"   (expected fail-closed fallback https://github.com/me/workspace/pull/42)

Flip: requiring resolved === workspaceRepoKey makes the probe pass and the whole existing session-pr-backfill.test.ts suite (incl. the fork-layout test) still passes.

Suggested change
const { parent } = await fetchRepoKeys(
runtime.workspaceCwd,
runtime.env.effectiveEnv,
);
pageMapTrusted = parent === pageRepoKey;
const { resolved, parent } = await fetchRepoKeys(
runtime.workspaceCwd,
runtime.env.effectiveEnv,
);
pageMapTrusted =
workspaceRepoKey !== undefined &&
resolved === workspaceRepoKey &&
parent === pageRepoKey;
中文说明

此处分叉页面(divergent page)的“fork 父仓库确认”忽略了 resolved,导致 gh 解析偏移到页面仓库的任意 fork 时,一个无关页面也会被标记为可信。fetchAttributionRepoKeys 返回 resolved(gh 自身解析出的仓库),但生产代码从未读取它——而此门上方的注释明确说偏移的解析绝不能用于绑定。具体触发:工作区 origin 解析为仓库 W(key 已知);本检出中先前的 gh repo set-default someone/their-fork 使 gh 解析偏移到无关仓库 R,而 R 恰好是 P 的 fork。gh pr list 列出父仓库 P 的 PR(pageRepoKey=P≠W);fetchRepoKeys 返回 {resolved:R, parent:P}parent === pageRepoKey 为真 → pageMapTrusted=true。随后一个 worktree 约定会话(slug pr-42)会经 pageUrlByNumber 绑定 P 的 PR 42——陌生人的 PR,这正是 numberToUrl fail-closed 门要防止的结果;/review https://W/pull/5 形式也会被打上 P 的 PR 5 的状态,且刷新扫描按绑定 URL 的仓库 key 打标,错误状态永不会纠正。fork 布局测试之所以通过,仅因其 mock 提供了与工作区 key 一致的 resolved——而生产代码根本不消费该字段。探针(工作区 origin https://github.com/me/workspace,mock fetchAttributionRepoKeys → {resolved:'github.com/someone/their-fork', parent:'github.com/parent/repo'},页面列出 https://github.com/parent/repo/pull/42):持久化绑定 url = https://github.com/parent/repo/pull/42(期望 fail-closed 回退 https://github.com/me/workspace/pull/42);修复(要求 resolved === workspaceRepoKey)后探针通过且现有 backfill 全套测试仍绿。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +445 to +447
const formUrlByNumber = new Map<number, string>(
candidate.reviewedUrlForms.map((form) => [form.number, form.url]),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] formUrlByNumber is built from ALL transcript URL forms, not the repo-gated subset, so a gate-rejected foreign-repo form can supply the URL for a legitimately bound number (it is consulted in the resolution chain before the remote fallback, and the Map constructor keeps the LAST entry per number). Concrete trigger: transcript has /review 42 (bare, legitimate) and /review https://github.com/other-org/repoB/pull/42 (foreign). Number 42 enters numbers via the bare form; the foreign form fails the allowedRepoKeys gate so adds no number — but formUrlByNumber.get(42) still returns the foreign URL. With gh unavailable, the number outside the 500-window, or a fork layout (numberToUrl empty), the binding persists {number:42, url:repoB's URL} instead of the correct ${remote}/pull/42 fallback — a stranger's PR linked under the workspace's number, contradicting the comment above this map ("already repo-gated"). Variant: two same-number forms where only the first passes the gate — the Map keeps LAST, so the foreign URL wins. The existing test covers only the foreign-form-only case.

witness (probe, gh unavailable kind:'cli_unavailable', transcript /review 42 + /review https://github.com/other-org/repoB/pull/42 --comment):

persisted url = "https://github.com/other-org/repoB/pull/42"   (expected workspace fallback https://github.com/o/r/pull/42)
variant (own-repo form first, foreign second, same number): foreign URL wins via Map last-entry

Flip: passing only the repo-gated form subset makes both probes pass with the full existing backfill suite still green.

Gate the URL map: build formUrlByNumber only from forms whose repoKeyFromWebUrl(form.url) is in the allowed set — pass allowedRepoKeys into bindCandidateNumbers via sources, or store the gated form URL alongside the number when the candidate loop accepts it.

中文说明

formUrlByNumber 由全部 transcript URL 形式构建,而非仅经仓库门筛选的子集,因此被门拒绝的外仓库形式仍可为合法绑定的编号提供 URL(它在解析链中先于 remote 回退被查询,且 Map 构造器对同一 key 保留最后一个条目)。具体触发:transcript 中有 /review 42(裸编号,合法)和 /review https://github.com/other-org/repoB/pull/42(外仓库)。编号 42 经裸形式进入 numbers;外形式未过 allowedRepoKeys 门故不贡献编号——但 formUrlByNumber.get(42) 仍返回外仓库 URL。当 gh 不可用、编号超出 500 窗口或 fork 布局(numberToUrl 为空)时,绑定会持久化为 {number:42, url:repoB 的 URL} 而非正确的 ${remote}/pull/42 回退——工作区编号下挂了陌生人的 PR,与此 map 上方注释(“已按仓库过滤”)矛盾。变体:同号两个形式且仅第一个过门——Map 保留最后一个,外仓库 URL 胜出。现有测试仅覆盖“只有外形式”的情形。探针(gh 不可用,上述 transcript):持久化 url = https://github.com/other-org/repoB/pull/42(期望工作区回退 https://github.com/o/r/pull/42);修复(仅传过门的子集)后两个探针均通过且现有 backfill 全套测试仍绿。建议:只用 repoKeyFromWebUrl(form.url) 在允许集合内的形式构建 formUrlByNumber——经 sourcesallowedRepoKeys 传入 bindCandidateNumbers,或在候选循环接受编号时一并保存过门的形式 URL。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +520 to +525
return liveEntry
? {
...liveEntry,
...(p.state !== undefined ? { state: p.state } : {}),
}
: p;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] This merge overlays the persisted sidecar state onto the live entry keyed on number alone, with no p.url === liveEntry.url check — re-introducing exactly the cross-repo state poisoning this diff's three writers each guard against (upsertSessionPr conditions state carry-over on URL identity; updateSessionPrStates verifies the URL inside the lock; the bridge preserves state only when the URL is unchanged — each documenting "the same number in another repository is another PR"). Concrete trigger: a session binds repoA #5; the refresh sweep stamps state:'merged' in the sidecar. The user re-binds #5 to repoB's PR (fork layouts share numbers — this diff's own comment says so). The bind route bumps the catalog BEFORE awaiting the sidecar write; a list render in that window — or a sidecar write that throws after the bridge mutation was published — merges live {5, repoB-url} with persisted {state:'merged'} resolved for repoA #5: repoB's OPEN PR renders dimmed/"Merged". The sweep treats 'merged' as terminal and never re-queries the entry, so when the write failed the poisoning persists for the daemon lifetime.

witness (probe through the real listWorkspaceSessionsForResponse, persisted {5, repo-a, state:'merged'}, live {5, repo-b}):

PR: MERGED prs=[{"number":5,"url":"https://github.com/repo-b/r/pull/5","state":"merged"}]
   — repoA's terminal state stamped onto repoB's URL
flip (state overlay conditioned on p.url === liveEntry.url):
prs=[{"number":5,"url":"https://github.com/repo-b/r/pull/5"}]   — no state
Suggested change
return liveEntry
? {
...liveEntry,
...(p.state !== undefined ? { state: p.state } : {}),
}
: p;
return liveEntry
? {
...liveEntry,
...(p.state !== undefined && p.url === liveEntry.url
? { state: p.state }
: {}),
}
: p;
中文说明

此合并仅按编号把持久化 sidecar 的 state 覆盖到活条目上,没有 p.url === liveEntry.url 检查——重新引入了本 diff 三个写入方各自防范的跨仓库状态污染(upsertSessionPr 的状态继承以 URL 一致为前提;updateSessionPrStates 在锁内校验 URL;bridge 仅在 URL 不变时保留状态——三处都写明“另一仓库的同编号是另一个 PR”)。具体触发:会话绑定 repoA #5;刷新扫描在 sidecar 中打上 state:'merged'。用户把 #5 重绑到 repoB 的 PR(fork 布局下同编号常见——本 diff 注释自己也这么说)。绑定路由先 bump catalog 再 await sidecar 写入;该窗口内的一次列表渲染——或 bridge 变更已发布后 sidecar 写入抛错——会把活 {5, repoB-url} 与为 repoA #5 解析的持久 {state:'merged'} 合并:repoB 的 OPEN PR 被渲染成灰暗/“Merged”。扫描把 'merged' 视为终态不再查询该条目,写入失败时污染将持续整个 daemon 生命周期。探针经真实 listWorkspaceSessionsForResponse(持久 {5, repo-a, state:'merged'},活 {5, repo-b}):合并结果 prs=[{number:5, url:repo-b..., state:'merged'}]——repoA 的终态被盖到 repoB 的 URL 上;修复(状态覆盖以 p.url === liveEntry.url 为前提)后状态消失。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +414 to +417
for (const candidate of candidates) {
if (existingNumbers.has(candidate.number)) {
alreadyBound.push(candidate.number);
continue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] upsertSessionPrs dedups candidates by bare PR number (existingNumbers.has(candidate.number)), ignoring URL and source — a gh-verified 'create' binding whose number collides with another repo's already-bound PR is silently dropped, contradicting the singular upsertSessionPr's own documented rule ("The same number in another repository is another PR") and the repo-identity gating used everywhere else in this diff. The shell hook feeds exactly this function and gates its notification on applied.added, and the new test blesses the number-only semantics. Concrete trigger: the sidecar holds repoA/pull/5 (source:'review', bound via a /review <url> form — bindCandidateNumbers explicitly binds the named URL itself). The user runs gh pr create in repoB within the same session; gh resolves repoB's new PR, which is also #5 (PR numbers are per-repo; low numbers collide routinely in the fork layouts this diff itself calls routine). All shell-hook attribution gates pass, then the number-only skip hits: added is empty, no binding, no emitSessionPrBound. The session's own created PR is permanently unbound — no badge, no 'create' eviction protection, no recovery (backfill deliberately declines transcript gh pr create traces; the sweep only rewrites states). Same-repo variant: a number first bound 'review' is never upgraded to 'worktree' by a later backfill run.

witness (probe against the real functions, sidecar seeded {5, repo-a, source:'review'}, candidate {5, repo-b/pull/5, state:'open', source:'create'}):

PLURAL (unmodified): added=[] alreadyBound=[5]; persisted=[{5, repo-a, review}]   — dropped
SINGULAR (same file):  persisted=[{5, repo-b, create}]                             — binds
flip (URL-aware skip mirroring the singular semantics): PLURAL added=[5]; persisted=[{5, repo-b, create}]

When the existing entry with the same number has a DIFFERENT url, treat it as a re-bind the way upsertSessionPr does (replace the entry, fresh createdAt, apply the source-upgrade rule); restrict the "untouched" path to same-URL candidates. Add a test offering a candidate whose number is bound to another repo's URL.

中文说明

upsertSessionPrs 仅按 PR 编号去重(existingNumbers.has(candidate.number)),忽略 URL 与来源——一个经 gh 验证的 'create' 绑定,若其编号与另一仓库已绑定的 PR 相同,会被静默丢弃;这与单数版 upsertSessionPr 自身写明的规则(“另一仓库的同编号是另一个 PR”)及本 diff 其他各处的仓库身份门相矛盾。shell 钩子恰恰调用此函数,并以 applied.added 作为发通知的门;新增测试还认可了“仅按编号”的语义。具体触发:sidecar 已有 repoA/pull/5source:'review',经 /review <url> 形式绑定——bindCandidateNumbers 明确绑定用户指名的 URL 本身)。用户在同一会话中于 repoB 执行 gh pr create;gh 解析出 repoB 的新 PR 也是 #5(PR 编号按仓库计,本 diff 自己也说 fork 布局下低编号冲突是常态)。shell 钩子所有归属门都通过后,命中“仅编号”跳过:added 为空、不绑定、不发 emitSessionPrBound。会话自己创建的 PR 永久失去绑定——无徽章、无 'create' 逐出保护、无恢复途径(backfill 刻意不回收 transcript 中的 gh pr create 痕迹;扫描只刷状态)。同仓库变体:先以 'review' 绑定的编号,之后的 backfill 永远不会把它升级为 'worktree'。探针(真实函数,播种 {5, repo-a, source:'review'},候选 {5, repo-b/pull/5, state:'open', source:'create'}):PLURAL(未改)added=[] alreadyBound=[5],持久仍为 repoA/review——被丢弃;单数版同文件则持久 {5, repo-b, create};修复(按 URL 感知、对齐单数语义)后 added=[5] 且持久为 repoB/create。建议:同号但 URL 不同时按 upsertSessionPr 的方式视为重绑(替换条目、新 createdAt、应用来源升级规则);“保持不变”路径仅限同 URL 候选。并新增“候选编号已被另一仓库 URL 占用”的测试。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +518 to +520
if (!existsSync(sourcePath)) return;
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
if (!existsSync(destinationPath)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] withSidecarLock materializes its lock target before locking (await fs.appendFile(filePath, ''), ~line 265), and moveSessionPrSidecar routes BOTH endpoints through it — so both files always exist before this guarded body runs: the source-absent guard is dead code, the rename fast path never fires, and every no-op move creates a stray empty sidecar. The removed predecessor movePrSidecar checked existence BEFORE creating anything — this is a regression introduced by this diff. No-op mutations through other paths (backfill with all-unresolved candidates, validation-declined binds) materialize stray sidecars at the same root. Concrete trigger: sessionService.archiveSessions/unarchiveSessions call this unconditionally for every session, so every archive/restore of a session that never bound a PR — the common case — leaves a stray empty <sessionId>.pr.json ping-ponging between chats/ and chats/archive/ forever.

witness (probe on unmodified code — moveSessionPrSidecar(src, dst) with neither file existing):

observed: { dstExists: true, dstSize: 0, srcExists: false }
documented contract: "only a still-present file moves" — destination must not appear
flip (guard !existsSync(sourcePath) before the lock chain): probe passes; existing lock-coverage tests unaffected

The committed test "does nothing when the source is absent" passes only because readSessionPrs reads an empty file as null — it never checks existence.

Stop materializing paths a mutation does not write: give withSidecarLock a createIfMissing flag (default true) and do a non-materializing existence check in moveSessionPrSidecar before enqueuing, so an absent source returns without touching either endpoint; only the destination — the path that can be written — should be pre-created.

中文说明

withSidecarLock 在加锁前会把锁目标实体化(await fs.appendFile(filePath, ''),约 265 行),而 moveSessionPrSidecar 把两个端点都交给它——因此受保护主体运行时两个文件必然已存在:源缺失守卫成为死代码、rename 快路径永不触发、每次空操作移动都会创建一个多余的空 sidecar。被删除的前身 movePrSidecar 是在创建任何东西之前先检查存在性——这是本 diff 引入的回归。其他路径的空操作变更(全部候选未解析的 backfill、被校验拒绝的绑定)也会在同一根因处实体化出多余 sidecar。具体触发:sessionService.archiveSessions/unarchiveSessions 对每个会话无条件调用它,因此任何从未绑定 PR 的会话(常见情形)每次归档/恢复都会留下一个在 chats/chats/archive/ 之间来回乒乓的空 <sessionId>.pr.json。探针(未改代码,两个文件都不存在时调用 moveSessionPrSidecar(src, dst)):观察到 { dstExists: true, dstSize: 0, srcExists: false };文档契约为“仅仍存在的文件才被移动”——目标不应出现;修复(锁链之前先守卫 !existsSync(sourcePath))后探针通过、现有锁覆盖测试不受影响。已提交的测试“源缺失时不做事”之所以通过,仅因 readSessionPrs 把空文件读成 null——它从未检查存在性。建议:不要为不会被写入的路径做实体化——给 withSidecarLock 增加 createIfMissing 标志(默认 true),并在 moveSessionPrSidecar 入队前做不实体化的存在性检查,使源缺失时不触碰任何端点直接返回;只有可能被写入的目标路径才应预创建。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +418 to +420
} else if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(input)) {
// scp-style [user@]host:path — any user, not only `git`.
const scp = /^(?:[^@\s/]+@)?([^:\s/]+):(.+)$/.exec(input);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] normalizeRemoteToWebUrl mis-parses a Windows drive-letter local-path origin (C:\src\origin.git) as an scp-style remote: the drive letter matches the scp host, WHATWG URL folds the backslashes, and the function fabricates https://c//src/origin plus repo key c/src/origin instead of returning undefined like every other non-web branch — backfill's workspaceRepoKey === undefined fail-closed guard is then bypassed by the fabricated key. Concrete trigger: a Windows workspace cloned from a local path (git clone C:\src\origin.gitgit remote get-url origin returns the drive path). In backfill the workspace key is defined-but-garbage: every real gh-page entry fails the key gate (numberToUrl stays empty; the undefined-key fail-closed path never fires), and every convention//review number falls to the ${remote}/pull/${number} fallback persisting URLs like https://c//src/origin/pull/5 into sidecars — dead badge links, systematically for every binding in that layout.

witness (probe on unmodified code):

normalizeRemoteToWebUrl('C:\\src\\origin.git')  -> "https://c//src/origin"
normalizeRemoteToWebUrl('D:/repos/origin.git')  -> "https://d//repos/origin"   (expected undefined)
repoKeyFromWebUrl(...)                          -> "c/src/origin"
flip (drive-letter guard): returns undefined; all 53 existing github-prs tests still pass

Reject drive-letter "hosts" in the scp branch before building the URL — real hostnames are never a single character; Windows drive letters always are:

const scp = /^(?:[^@\s/]+@)?([^:\s/]+):(.+)$/.exec(input);
if (scp && /^[A-Za-z]$/.test(scp[1])) return undefined;
中文说明

normalizeRemoteToWebUrl 会把 Windows 盘符本地路径 origin(C:\src\origin.git)误判为 scp 风格远端:盘符匹配到 scp 的 host,WHATWG URL 把反斜杠折叠,函数凭空造出 https://c//src/origin 及仓库 key c/src/origin,而不是像其他非 web 分支那样返回 undefined——backfill 的 workspaceRepoKey === undefined fail-closed 守卫因此被这个伪造 key 绕过。具体触发:Windows 上从本地路径克隆的工作区(git clone C:\src\origin.gitgit remote get-url origin 返回盘符路径)。backfill 中工作区 key 有值但是垃圾值:所有真实 gh 页面条目都过不了 key 门(numberToUrl 为空;未定义 key 的 fail-closed 路径永不触发),每个约定//review 编号都落到 ${remote}/pull/${number} 回退,把 https://c//src/origin/pull/5 这类 URL 持久化进 sidecar——该布局下所有绑定的徽章链接系统性失效。探针(未改代码):normalizeRemoteToWebUrl('C:\\src\\origin.git')https://c//src/origin'D:/repos/origin.git'https://d//repos/origin(期望 undefined);repoKeyFromWebUrl(...)c/src/origin;修复(盘符守卫)后返回 undefined,现有 53 个 github-prs 测试全部仍通过。建议:在 scp 分支构建 URL 前拒绝盘符“host”——真实主机名从不只有一个字符,Windows 盘符永远是。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Round summary — PR #9739 (same-run verification repair of the rejected commit)

Context

  • Same-run verification repair: the previous commit was rejected by deterministic verification with tests failed in packages/cli. The supplied gate log shows all 20771 tests passing but 40 Unhandled Rejection errors, which drive vitest to exit 1 (dangerouslyIgnoreUnhandledErrors is false on Linux). Per the repair instruction, the rejected commit (dddbd698a9, preserved through merge tip 56574e112f) is kept unchanged; this round adds one verified follow-up commit on top.
  • Reproduced on the pre-round tree with the gate's exact command (cd packages/cli && npx vitest run --changed origin/main --passWithNoTests): 40 unhandled rejections, exit 1.
  • Root cause: packages/cli/src/ui/AppContainer.test.tsx installs a Partial<SubagentManager> mock that is missing getAvailableModelGrades. AgentTool's constructor calls the un-awaited refreshSubagents(); its first updateDescriptionAndSchema() throws on the missing method, the catch block calls it again and throws a second time — so the fire-and-forget promise rejects with TypeError: this.subagentManager.getAvailableModelGrades is not a function (agent.ts:1034 via :887). The call was added on main by d44030a4c0 without updating this mock; the defect is latent on main and surfaced here only because this PR's changed core files pull AppContainer.test.tsx into vitest's --changed origin/main set.
  • Second instance of the same hazard class: once the TypeError path stops rejecting first, one rejection remains — the rewind test surfaces unexpected outer errors through history installs a throwing getGeminiClient spy mid-test, which the still-in-flight un-awaited initialize() IIFE hits from AgentTool.refreshSubagents' finally (agent.ts:890) → Error: client exploded unhandled rejection.
  • Growth audit (required before any edit): verdict sound, both axes pass (growth-audit.json in the workdir). The repair is test-only: 6 added lines in one file that was already failing the gate; no source lines, no new machinery.

Feedback dispositions

Review body

  • [rv:5021281993] CHANGES_REQUESTED — "Partially reviewed — gaps disclosed": no code action requested. It discloses unreviewed areas (skipped CI suites, reverse-audit rounds cut by the time budget) and lists 23 items explicitly "recorded, not requested in this round". Its convergence observation (findings recurring on the same files) is addressed in substance by the preserved commit, which consolidated the two shared root causes behind the recurring clusters (dual eviction policies across the two stores; number-only identity where repo identity is the rule). The disclosed unreviewed areas remain for CI/future rounds.

Inline Critical findings — all 7 resolved in code by the preserved commit, re-verified this round

The fixes were implemented by the preserved rejected commit dddbd698a9; this round re-verified every one still holds at HEAD — by direct content inspection (all markers present) and by re-running their witness suites green (see Verification).

Finding Fix in preserved commit Re-verification this round
[rc:3854989813] bridge live catalog caps positionally while the sidecar caps by provenance setSessionPrs overwrites the live entry with the authoritative persisted list; called by both REST metadata routes and the ACP dispatch marker at bridge.ts:10027, session.ts:5508/5679, dispatch.ts:2979; bridge suite 787 passed; server.test.ts 1083 + transport.test.ts 345 passed
[rc:3854989832] fork-parent confirmation ignores resolved pageMapTrusted now also requires resolved === workspaceRepoKey marker at session-pr-backfill.ts:325; session-pr-backfill.test.ts 38 passed
[rc:3854989864] formUrlByNumber built from ungated transcript URL forms form-URL map built only from allowedRepoKeys-gated forms, threaded via sources markers at session-pr-backfill.ts:336-353/421/441; backfill suite green
[rc:3854989875] list merge overlays persisted state by number alone state overlay conditioned on p.url === liveEntry.url marker at session-list.ts:526; server.test.ts green
[rc:3854989892] upsertSessionPrs dedups by bare PR number plural upsert is URL-aware, mirroring the singular's documented rule markers at session-pr-service.ts:438-465; session-pr-service.test.ts green
[rc:3854989897] moveSessionPrSidecar materializes both endpoints absent-source pre-check returns before the lock chain; materialized-but-unwritten files removed markers at session-pr-service.ts:549/563; sessionService.test.ts green
[rc:3854989907] normalizeRemoteToWebUrl mis-parses drive-letter origins scp branch rejects single-character "hosts" marker at github-prs.ts:422-424; github-prs.test.ts green

Deferred non-Critical feedback

Critical-only mode is active; the Deferred non-Critical feedback section was treated as the workflow's audit record — no code changes, thread resolutions, or replies for it.

Failed checks

Signal the reviewed fork PR: CANCELLED and review-address: CANCELLED are superseded workflow runs of this same automation, not code-check failures; no code change applies.

Changes this round

One file, packages/cli/src/ui/AppContainer.test.tsx, +6 lines, test-only:

  1. Added getAvailableModelGrades: vi.fn().mockReturnValue(new Map()) to the existing Partial<SubagentManager> mock — the exact mock shape packages/core/src/tools/agent/agent.test.ts already uses for the same manager. Removes the TypeError unhandled rejections (measured on the full --changed run: 40 errors → 1).
  2. Added the scoped initialize stub (vi.spyOn(mockConfig, 'initialize').mockResolvedValue(undefined)) to surfaces unexpected outer errors through history, with a comment — the exact convention this same file already documents and uses for this hazard class (the announcement-latch tests). Removes the remaining client exploded unhandled rejection (1 → 0 errors).

Mutation probes: each guard's absent state was actually run and was red — the pre-round tree (no hunk 1) produced 40 errors with exit 1; the tree with hunk 1 only (no hunk 2) produced the 1 client exploded error. Both guards restored → final run has 0 unhandled errors.

Commit: 394c039469fix(cli): stop unhandled test rejections from incomplete SubagentManager mock (#9739) — additive on top of the preserved 56574e112f; no history rewrite.

Environment-specific local failures (disclosed, out of scope)

On this self-hosted runner the full --changed suite also shows 33 test failures in files this PR never touches (settings/config env-loading, live-host daemon startup, sandbox image resolution, Footer snapshots, AuthDialog/cd/directory/docs/extensions/ide command tests) — identical before and after this round's change (verified by diffing the failure lists). They are environment-specific to this machine: the gate's own run of this same commit passed all of them (Test Files 679 passed; Tests 20771 passed | 90 skipped). They did not block repairing the supplied deterministic rejection, whose signature (the unhandled errors) is fully resolved: the final run's summary carries no Errors line.

Verification

Commands actually run this round:

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check packages/cli/src/ui/AppContainer.test.tsx — passed
  • cd packages/cli && npx vitest run --changed origin/main --passWithNoTests (the gate's command):
    • pre-fix — 40 unhandled rejections, exit 1 (reproduces the supplied rejection)
    • after hunk 1 only — 1 unhandled rejection (the client exploded second class)
    • after hunks 1+2 (final) — 0 unhandled errors; AppContainer.test.tsx 157 passed; PR witness files green inside the run (server.test.ts 1083, transport.test.ts 345, session-pr-backfill.test.ts 38, session-pr-refresh.test.ts 19); the only remaining failures are the 33 environment-specific ones listed above, identical pre/post fix
  • cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx — 157 passed (the edited rewind test still asserts Rewind failed: client exploded)
  • cd packages/core && npx vitest run src/services/session-pr-service.test.ts src/utils/github-prs.test.ts src/services/sessionService.test.ts src/tools/shell.test.ts — 4 files, 632 passed (re-verification of the preserved commit)
  • cd packages/acp-bridge && npx vitest run src/bridge.test.ts — 787 passed (an earlier attempt failed collection because a concurrent npm run build was rewriting core's dist/; the clean rerun is green)
  • Preserved-commit content markers re-verified at HEAD via git grep (all seven fixes present; see table)
  • npm run generate:settings-schema — not needed (no settings source changed)
  • Integration tests after npm run bundle — not needed: test-only change to a UI unit-test file; no behavior is exercised only through the bundled CLI or integration harness
中文说明

轮次总结 — PR #9739(对被拒提交的同轮验证修复)

背景

  • 同轮验证修复:上一个提交被确定性验证以 tests failed in packages/cli 拒绝。提供的工作流日志显示全部 20771 个测试通过,但存在 40 个 Unhandled Rejection(未处理的 Promise 拒绝)错误,导致 vitest 以退出码 1 结束(Linux 上 dangerouslyIgnoreUnhandledErrors 为 false)。按照修复指令,被拒提交(dddbd698a9,经合并顶端 56574e112f 保留)保持不变;本轮在其上追加一个已验证的后续提交。
  • 已在轮前树上复现:使用工作流的原始命令(cd packages/cli && npx vitest run --changed origin/main --passWithNoTests):40 个未处理拒绝,退出码 1。
  • 根因packages/cli/src/ui/AppContainer.test.tsx 安装的 Partial<SubagentManager> mock 缺少 getAvailableModelGradesAgentTool 构造函数调用未 await 的 refreshSubagents();其第一次 updateDescriptionAndSchema() 因缺少该方法抛出,catch 块再次调用它并再次抛出——于是这个发射后不管的 Promise 以 TypeError: this.subagentManager.getAvailableModelGrades is not a function(agent.ts:1034,经 :887)拒绝。该调用由 main 上的 d44030a4c0 引入时未更新此 mock;该缺陷在 main 上是潜伏的,只因为本 PR 改动的 core 文件把 AppContainer.test.tsx 拉进了 vitest 的 --changed origin/main 集合才在此暴露。
  • 同一危险类别的第二个实例:当 TypeError 路径不再先行拒绝后,还剩一个拒绝——rewind 测试 surfaces unexpected outer errors through history 在测试中途安装了会抛错的 getGeminiClient spy,仍在进行中的未 await initialize() IIFE 从 AgentTool.refreshSubagentsfinally(agent.ts:890)命中它 → Error: client exploded 未处理拒绝。
  • 增长审计(任何编辑前必须完成):结论 sound,两个维度均通过(工作目录中的 growth-audit.json)。修复仅涉及测试:在一个本就令工作流失败的文件中新增 6 行;零源码行、零新机制。

反馈处置

评审主体

  • [rv:5021281993] CHANGES_REQUESTED——"部分审查,缺口已披露":不要求代码改动。它披露了未审查区域(被跳过的 CI 套件、因时间预算被截断的反向审计轮次),并在收敛姿态下列出 23 条"已记录、本轮不要求修改"的条目。其收敛观察(发现反复出现在同一批文件上)已在实质上被保留提交回应:该提交整合了反复出现簇背后的两个共同根因(两个存储各自维护分叉的逐出策略;在应以仓库身份为准的地方仅按编号判等)。披露的未审查区域留给 CI 与后续轮次。

行内 Critical 发现 — 7 条全部已由保留提交在代码中解决,本轮重新验证

这些修复由被拒提交 dddbd698a9 实现;本轮重新验证了每一条在 HEAD 上仍然成立——通过直接内容检查(全部标记均在)与重跑其见证套件为绿(见"验证"一节)。

发现 保留提交中的修复 本轮重新验证
[rc:3854989813] bridge 活目录按位置截断而 sidecar 按来源权威截断 setSessionPrs 用权威的持久化列表覆写活条目;两个 REST 元数据路由与 ACP 分发均调用 标记位于 bridge.ts:10027、session.ts:5508/5679、dispatch.ts:2979;bridge 套件 787 通过;server.test.ts 1083 + transport.test.ts 345 通过
[rc:3854989832] fork 父仓库确认忽略 resolved pageMapTrusted 现还要求 resolved === workspaceRepoKey 标记位于 session-pr-backfill.ts:325;session-pr-backfill.test.ts 38 通过
[rc:3854989864] formUrlByNumber 由未过门的 transcript URL 形式构建 形式 URL 映射仅由通过 allowedRepoKeys 的形式构建,经 sources 传入 标记位于 session-pr-backfill.ts:336-353/421/441;backfill 套件绿
[rc:3854989875] 列表合并仅按编号覆盖持久化 state 状态覆盖以 p.url === liveEntry.url 为前提 标记位于 session-list.ts:526;server.test.ts 绿
[rc:3854989892] upsertSessionPrs 仅按编号去重 复数 upsert 按 URL 判等,与单数版写明的规则一致 标记位于 session-pr-service.ts:438-465;session-pr-service.test.ts 绿
[rc:3854989897] moveSessionPrSidecar 实体化两个端点 源缺失时在锁链触碰端点前直接返回;实体化但未写入的文件被移除 标记位于 session-pr-service.ts:549/563;sessionService.test.ts 绿
[rc:3854989907] normalizeRemoteToWebUrl 误判盘符 origin scp 分支拒绝单字符"主机" 标记位于 github-prs.ts:422-424;github-prs.test.ts 绿

延后的非 Critical 反馈

仅 Critical 模式已启用;Deferred non-Critical feedback(延后的非 Critical 反馈)一节按工作流审计记录处理——不改代码、不解决线程、不回复。

失败检查

Signal the reviewed fork PR: CANCELLEDreview-address: CANCELLED 是同一自动化被取代的工作流运行,不是代码检查失败;无需代码改动。

本轮改动

单个文件 packages/cli/src/ui/AppContainer.test.tsx,+6 行,仅测试:

  1. 在现有 Partial<SubagentManager> mock 中补上 getAvailableModelGrades: vi.fn().mockReturnValue(new Map())——与 packages/core/src/tools/agent/agent.test.ts 对同一管理器使用的 mock 形态完全一致。消除 TypeError 类未处理拒绝(在完整 --changed 运行上实测:40 个错误 → 1 个)。
  2. surfaces unexpected outer errors through history 中加入作用域内 initialize 桩(vi.spyOn(mockConfig, 'initialize').mockResolvedValue(undefined))并附注释——与本文件已记录并在别处使用的同类危险场景处置约定完全一致。消除剩余的 client exploded 未处理拒绝(1 → 0 个错误)。

变异探针:每个守卫的"缺失态"都被实际运行过且为红——轮前树(无 hunk 1)产生 40 个错误、退出码 1;仅有 hunk 1 的树(无 hunk 2)产生 1 个 client exploded 错误。两个守卫都恢复后 → 最终运行 0 个未处理错误。

提交394c039469fix(cli): stop unhandled test rejections from incomplete SubagentManager mock (#9739) — 纯增量叠加在保留的 56574e112f 之上;不改写历史。

环境特定的本地失败(披露,超出范围)

在本自托管运行器上,完整 --changed 套件还出现 33 个位于本 PR 从未触碰的文件中的测试失败(settings/config 环境变量加载、live-host daemon 启动、sandbox 镜像解析、Footer 快照、AuthDialog/cd/directory/docs/extensions/ide 命令测试)——本轮改动前后完全一致(已对比失败列表验证)。它们是本机器特有的环境问题:工作流自己的运行在同一提交上全部通过(Test Files 679 passed; Tests 20771 passed | 90 skipped)。它们不妨碍对所提供的确定性拒绝的修复——该拒绝的特征(未处理错误)已完全消除:最终运行的汇总中没有 Errors 行。

验证

本轮实际执行的命令:

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check packages/cli/src/ui/AppContainer.test.tsx — 通过
  • cd packages/cli && npx vitest run --changed origin/main --passWithNoTests(工作流原始命令):
    • 修复前 — 40 个未处理拒绝,退出码 1(复现所提供的拒绝)
    • 仅 hunk 1 后 — 1 个未处理拒绝(client exploded 第二类)
    • hunk 1+2 后(最终)— 0 个未处理错误;AppContainer.test.tsx 157 通过;运行内的 PR 见证文件为绿(server.test.ts 1083、transport.test.ts 345、session-pr-backfill.test.ts 38、session-pr-refresh.test.ts 19);仅剩的失败是上文列出的 33 个环境特定失败,前后一致
  • cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx — 157 通过(被修改的 rewind 测试仍断言 Rewind failed: client exploded
  • cd packages/core && npx vitest run src/services/session-pr-service.test.ts src/utils/github-prs.test.ts src/services/sessionService.test.ts src/tools/shell.test.ts — 4 个文件,632 通过(对保留提交的重新验证)
  • cd packages/acp-bridge && npx vitest run src/bridge.test.ts — 787 通过(更早一次尝试因并发的 npm run build 正在重写 core 的 dist/ 而收集失败;干净重跑为绿)
  • 保留提交的内容标记经 git grep 在 HEAD 上重新验证(七个修复全部在位;见上表)
  • npm run generate:settings-schema — 无需(未改动 settings 源)
  • npm run bundle 后的集成测试 — 无需:仅改动 UI 单测文件;没有任何行为只能经打包 CLI 或集成测试框架验证

Deferred non-Critical feedback

Critical-only mode is active: the PR's diff grew src 261 / test 453 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). (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 261 / 测试 453 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 @qwen-code /retry 可开启新的计数窗口。)

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

Reserve pr-<number> worktree slugs (R1-21), mention state in the pr validation errors (R1-22), and tighten tests (alreadyBound read-back, gh query args, invalid-state PATCH, slug reservation). The bot rounds already landed the lock/fence/attribution fixes; residual text-detector limits are documented.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: test-efficacy — probe harness could not self-validate (no probe file was green in the unmutated baseline; vitest dist prerequisite in the per-file probe runner).

Not explored to full depth (tool budget reached): "agent invariant-b (packages/core/src/utils/github-prs.ts)": none — no check was cut short.; chunk 3: none — wait, per the brief, since nothing was cut short, no budget line is warranted. Nothing was cut short..

Deferred under the convergence posture (round 13, not a blocker) — recorded, not requested in this round:

  • docs/design/2026-08-20-webshell-session-pr-binding.md:26 — [review] This PR amends this exact doc bullet list (adds the…
  • docs/design/2026-08-20-webshell-session-pr-binding.md:69 — [review] The new backfill section of the design doc documents an…
  • packages/acp-bridge/src/bridge.test.ts:25406 — [review] The new pr-binding suite never exercises the handler's…
  • packages/acp-bridge/src/bridge.ts:9870 — [review] The bridge's new pr.state enum check throws…
  • packages/acp-bridge/src/bridge.ts:9960 — [review] The "no change, no event" de-dup in updateSessionMetadata …
  • packages/cli/src/acp-integration/session/Session.ts:8246 — [review] The wire connecting the shell-hook binding to the daemon…
  • packages/cli/src/serve/fast-path-open.test.ts:311 — [review] The flake fix (10s vi.waitFor timeout) is applied to 3…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:199 — [review] The backfill suite never asserts the gh page fetch…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:427 — [review] The PR description still advertises the removed transcript…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:1206 — [review] The fork-layout test's bare-number case (SESSION_B)…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:1274 — [review] The route's per-workspace error isolation (try/catch at…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:97 — [review] REVIEW_COMMAND_PATTERN 's URL alternative is…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:98 — [review] The #? token — the /review #N invocation form…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:98 — [review] The URL alternative terminates the PR number with only…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:143 — [review] The user-record fallback regex-scans the first text part…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:386 — [review] The full-sidecar skip equates "number present" with…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:396 — [review] Free-slot offer sizing keeps the strongest freeSlots …
  • packages/cli/src/serve/routes/session-pr-backfill.ts:424 — [review] Backfill's per-candidate failure isolation (the failed …
  • packages/cli/src/serve/routes/session.ts:2262 — [review] The new state write-boundary validation has no test at…
  • packages/cli/src/serve/routes/session.ts:2263 — [review] The new pr.state validation rejects requests through the…
  • …and 36 more (see the run report)

Convergence: round 13 posted 3 inline comment(s), 3 of them reported for the first time; the previous round posted 7 (7 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/session-pr-backfill.ts (findings in round 12; 1 more now); packages/cli/src/serve/server/session-list.ts (findings in round 12; 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.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查: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。

未审查:test-efficacy — probe harness could not self-validate (no probe file was green in the unmutated baseline; vitest dist prerequisite in the per-file probe runner)。

未探索到全部深度(达到工具调用预算):"agent invariant-b (packages/core/src/utils/github-prs.ts)"none — no check was cut short.;chunk 3:none — wait, per the brief, since nothing was cut short, no budget line is warranted. Nothing was cut short.

收敛姿态下延后(第 13 轮,非阻断)——已记录,本轮不要求修改:共 56 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 13 轮发布了 3 条行内评论,其中 3 条是首次提出;上一轮发布了 7 条(其中 7 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/routes/session-pr-backfill.ts(第 12 轮已出过发现,本轮又有 1 条);packages/cli/src/serve/server/session-list.ts(第 12 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +540 to +544
for (const liveEntry of livePrs) {
if (
!persistedNumbers.has(liveEntry.number) &&
ordered.length < SESSION_PR_LIST_LIMIT
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R13-1: The live-only append loop gates on the running merged length (ordered.length < SESSION_PR_LIST_LIMIT) instead of the persisted list's length, so below the cap it drops the session's NEWEST live-only bindings once the running total reaches the cap — contradicting the comment directly above it ("below it a live-only entry is genuinely the newest"). The bind routes are bridge-first: they append to the live entry, then await upsertSessionPr, then reconcile; with the 2s-TTL persisted snapshot still short, two overlapping binds (or one bind in flight) make live [P2..P9, B1, B2] against persisted [P1..P9] — the merge appends B1, ordered.length hits 10, and B2, the newest binding, fails the gate and is dropped, so the badge shows B1 as latest until the next fresh listing. The single-bind variant at persisted=10 hides a brand-new binding for the whole in-flight window.

Witness (probe driving the real listWorkspaceSessionsForResponse in an isolated tree):

persisted=[1..9], live=[2..9,11,12]:
  PR code  -> merged numbers [1,2,3,4,5,6,7,8,9,11]  <- newest binding 12 dropped, stale 1 kept
  with fix -> merged numbers [2,3,4,5,6,7,8,9,11,12]  <- flips, both arms pass

Gate on the persisted size and let the existing final slice remain the only cap:

Suggested change
for (const liveEntry of livePrs) {
if (
!persistedNumbers.has(liveEntry.number) &&
ordered.length < SESSION_PR_LIST_LIMIT
) {
for (const liveEntry of livePrs) {
if (
!persistedNumbers.has(liveEntry.number) &&
persistedPrs.length < SESSION_PR_LIST_LIMIT
) {
中文说明

活列表追加循环用的是合并后列表的当前长度(ordered.length < SESSION_PR_LIST_LIMIT)而非持久化列表的长度,因此在未达上限时,一旦合并长度达到 10 就会丢弃会话最新的活绑定——与上方注释(“未达上限时活条目确实是最新的”)直接矛盾。绑定路由是先写 bridge 再 await upsertSessionPr 再对账,配合 2 秒 TTL 的持久化快照,两个并发绑定(或一个在途绑定)会形成活列表 [P2..P9, B1, B2] 对持久 [P1..P9]:合并追加 B1 后 ordered.length 到 10,最新绑定 B2 被丢弃,徽章会把 B1 显示为最新,直到下次列表刷新。探针(驱动真实 listWorkspaceSessionsForResponse):PR 代码下合并结果 [1,2,...,9,11](最新绑定 12 被丢、过期的 1 保留);按建议修复后为 [2,...,9,11,12],两侧均验证通过。建议改为以持久列表长度作为门槛,最终的 slice 继续作为唯一上限。

— qwen3.8-max via Qwen Code /review (v0.22.0)

expect(sessionService.emitSessionPrBound).not.toHaveBeenCalled();
});

it('binds when a promote is refused after the command settled', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] D13-44: A SUCCESSFUL promote and any is_background: true gh pr create never reach bindGhPrCreate — its sole call site (shell.ts:2759) sits AFTER the if (result.promoted) return handlePromotedForeground(...) early return, and executeBackground never calls it — yet this same PR removed the transcript gh pr create backfill source whose committed justification (session-pr-backfill.ts:176) asserts "live creates bind through the shell tool post-hook", and the refresh sweep only re-states existing bindings. The new suite pins only the promote-REFUSED arm that still binds. So a Ctrl+B mid-gh pr create --fill, or an agent-run background create, completes exit 0 printing the PR URL and the session badge never shows the PR — permanently, with no recovery path.

Witness (probe in the PR's own binding harness, isolated tree):

background arm: is_background:true, exit 0, PR URL in output
  -> registry.complete called; upsertSessionPrsMock NEVER called
promote arm: promoted:true resolution
  -> upsertSessionPrsMock never called
BASE 20/20 green; candidate fix (run the gh-verified gate on the settle path) flips the probe

Re-run the same gh-verified gate (pre/post snapshot, open-state, repo-key, branch-identity, URL-in-output) when a background or promoted shell settles — the settle hook already exists. If background/promoted creates are deliberately out of scope instead, say so in bindGhPrCreate's doc comment, correct the backfill comment that justifies the transcript-source removal, and pin the exclusion with a negative test next to this one.

中文说明

成功 promote 以及任何 is_background: truegh pr create 都到不了 bindGhPrCreate——其唯一调用点(shell.ts:2759)位于 if (result.promoted) return handlePromotedForeground(...) 提前返回之后,executeBackground 也从不触发它;而本 PR 又删除了 transcript gh pr create 回填源,其提交时的理由(session-pr-backfill.ts:176)声称“实时创建由 shell 工具后置钩子绑定”,刷新定时器也只刷新已有绑定的状态。新增测试只钉住了仍会绑定的 promote-被拒绝分支。于是:gh pr create --fill 进行中被 Ctrl+B 转后台、或 agent 以后台方式执行创建,命令以 exit 0 结束并打印 PR URL,但会话徽章永远不会显示这个 PR——且没有任何恢复途径。探针(使用 PR 自带的绑定测试框架):后台分支下 registry.complete 被调用而 upsertSessionPrsMock 从未被调用;promote 分支同样从未调用;基线 20/20 全绿,候选修复(在 settle 路径上执行同一套 gh 验证门)可使探针翻转。建议:后台/promoted 命令落定时重跑同一套 gh 验证门(settle 钩子已存在);若刻意不覆盖这些形态,请在 bindGhPrCreate 文档注释中说明、修正背书删除回填源的注释,并在此测试旁补一个负例钉住该排除。

— qwen3.8-max via Qwen Code /review (v0.22.0)

url = `${sources.remote}/pull/${number}`;
}
}
const state = sources.pageStateByNumber.get(number);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] D13-50: bindCandidateNumbers stamps state from the gh page by PR number alone, regardless of which repo the resolved url belongs to. In the fork layout — fork-trust branch holding, gh page listing the PARENT's PRs — a /review <fork-url> form resolves the fork URL correctly but pairs it with the parent repo's same-numbered PR's state (PR numbers are per-repo, so that is a DIFFERENT PR): fork PR #7 open + parent PR #7 merged persists {number: 7, url: .../me/fork/pull/7, state: 'merged'} and the badge/tooltip renders the fork's OPEN PR as merged. The refresh sweep cannot correct it — it keys stamps by the page's repo key, looks each binding up under its own URL's key, and its own test pins fork-URL bindings as skipped — and 'merged' is terminal, so the wrong state persists. Same-repo layouts are immune (numberToUrl pairs URL and state from one entry).

Witness (probe, isolated tree):

fork layout (origin me/fork, fetchAttributionRepoKeys -> {resolved: fork, parent: parent}),
/review https://github.com/me/fork/pull/7, parent page listing PR 7 merged:
  PR code -> persisted {number:7, url:"https://github.com/me/fork/pull/7", state:"merged"}
  with fix (stamp only when the bound URL's repo matches the page's repo) -> state dropped;
  all 38 shipped backfill tests still pass

Thread pageRepoKey into sources and stamp state only when the bound URL's repo matches the page's repo, e.g. set sources.pageRepoKey = pageRepoKey in the gate construction and use it here:

const state =
  url !== undefined && repoKeyFromWebUrl(url) === sources.pageRepoKey
    ? sources.pageStateByNumber.get(number)
    : undefined;
中文说明

bindCandidateNumbers 仅按 PR 编号从 gh 页面取 state,不检查解析出的 url 属于哪个仓库。在 fork 布局下(fork 信任分支成立、gh 页面列出的是父仓库的 PR),/review <fork-url> 形式能正确解析出 fork 的 URL,却配上了父仓库同号 PR 的状态(PR 编号按仓库计,这是另一个 PR):fork 的 PR #7 是 open、父仓库的 PR #7 是 merged 时,会持久化 {number: 7, url: .../me/fork/pull/7, state: 'merged'},徽章/工具提示会把 fork 的 OPEN PR 显示为已合并。刷新扫描也纠正不了:它按页面仓库 key 建状态索引、按绑定自身 URL 的 key 查找(其自身测试已钉住 fork-URL 绑定会被跳过),且 'merged' 是终态,错误状态会一直存在。同仓库布局不受影响(numberToUrl 的 URL 与状态来自同一条目)。探针:按建议修复(仅当绑定 URL 的仓库与页面仓库一致时打标)后状态不再写入,现有 38 个 backfill 测试全部仍绿。建议把 pageRepoKey 传入 sources,并在此处加上仓库一致性条件。

— qwen3.8-max via Qwen Code /review (v0.22.0)

wenshao and others added 5 commits August 26, 2026 08:56
R1-10/R3-10: accept verified wrapper shapes (sudo/env/nohup/command with flags or KEY=val, path-qualified gh binaries, the pr new alias) in the execution gate; quote-awareness stays approximate by design (the shell-aware tokenizer cannot enter the serve closure). R1-22: pr validation errors now name the state constraint at the route and bridge.
…oncile test

The reconcile test built its sidecar path from the bridge session id,
which is `sess:<cwd>` — on Windows that expands to `sess:D:\work\a`, so
`path.join` turned the filename into extra path segments and
`writeSessionPrs`' `mkdir(dirname)` failed with ENOENT on the illegal
drive colon:

  ENOENT: no such file or directory, mkdir
  'C:\...\Temp\bridge-pr-reconcile-XXXX\sess:D:\work'

The id was never meaningful here — the test only ever refers to the file
through `sidecarPath`. Use a fixed name inside the temp directory.
Production paths are unaffected: they come from
`sessionService.getPrSessionPathForArchiveState()`, which keys off the
persisted session UUID like the existing `.jsonl` and `.worktree.json`
sidecars.
The gh pr create gate advertises inline-token shapes (GH_TOKEN=x gh pr
create --fill), but fetchCurrentBranchPullRequest ran its verification
legs bare: with no ambient gh auth the legs errored and the binding
silently missed. Extract the leading GH_*/GITHUB_* assignments from the
create segment and thread them into both the pre-run snapshot and the
post-run attribution leg, so they authenticate the way the create
itself did.
The inline-credential threading added for the gh pr create verification
legs needs the resolver to authenticate with the same env; restore the
optional env parameter (gitEnv(env)) that the round-6 rewrite dropped.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: test-efficacy — probe harness could not self-validate (no probe file was green in the unmutated baseline; vitest dist prerequisite in the probe tree).

Not explored to full depth (tool budget reached): chunk 17: none — no checks were cut short..

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Deferred under the convergence posture (round 14, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/serve/acp-http/dispatch.ts:2979 — [review] reconcile runs after session_metadata_updated publishes — the event payload carries the diverged positional-cap list
  • packages/acp-bridge/src/bridgeClient.ts:2243 — [review] shell-detected bindings only mark the catalog clock; pr-less metadata echoes miss the shell-bound PR
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:1216 — [review] fork-layout SESSION_B asserts only the number — a trust-branch regression ships green with the 404 fork URL
  • packages/cli/src/serve/routes/session-pr-backfill.ts:336 — [review] allowedRepoKeys admits pageRepoKey unconditionally, including divergent untrusted pages
  • packages/cli/src/serve/routes/session-pr-backfill.ts:396 — [review] free-slot sizing counts already-bound candidates — fresh weaker candidates are permanently spliced away
  • packages/cli/src/serve/run-qwen-serve.ts:6100 — [review] dynamic import of session-pr-refresh.js has no .catch — an unhandled rejection crashes the daemon on fast-path boot
  • packages/cli/src/serve/server/session-pr-refresh.ts:51 — [review] no lower interval floor — 0.001 minutes installs a 60ms gh-spawning busy loop
  • packages/core/src/services/session-pr-service.ts:160 — [review] gate regex enumerates corner shapes — six verified-real create forms miss (stacked wrappers, timeout/time, ./ and $HOME paths, quoted assignments)
  • packages/core/src/services/session-pr-service.ts:402 — [review] upsertSessionPr drops an explicit source:'review' on NEW entries (undefined ranks above review)
  • packages/core/src/utils/github-prs.test.ts:554 — [review] no test passes env to the three fetchers — dropping the env override ships green
  • packages/cli/src/acp-integration/session/Session.ts:8246 — [review] the pr-bound callback registration/emission (middle link of the propagation chain) is untested
  • packages/sdk-typescript/src/daemon/session-pr.ts:40 — [review] SDK shape gate's new state acceptance has no test pairing
  • packages/acp-bridge/src/bridge.ts:9870 — [review] bridge-level invalid-state rejection absent from bridge.test.ts's table — sole gate on the ACP path
  • packages/web-shell/client/components/sidebar/SessionDetailsTooltip.tsx:171 — [review] tooltip merged/closed suffix and two new i18n keys untested — label-swap and suffix-deletion mutants ship green (probe)
  • packages/cli/src/serve/acp-http/transport.test.ts:461 — [review] FakeBridge setSessionPrs drops state — the reconcile's state propagation is pinned by nothing
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:111 — [review] the sweep's archived loop half has zero coverage in the file that adds it
  • packages/core/src/services/session-pr-service.test.ts:451 — [review] no-write tests use byte-equality that cannot detect an identical rewrite — guard removal ships green (mutant+inode probe)
  • packages/core/src/services/session-pr-service.ts:616 — [review] moveSessionPrSidecar's rename fast path is unreachable — both locks materialize the destination
  • packages/core/src/services/sessionService.ts:2023 — [review] enumerateSessionIdsForArchiveState silently truncates at 10000 despite the FULL-set doc (probe: 10001 seeded → 1 dropped deterministically)
  • packages/core/src/tools/shell.test.ts:431 — [review] output.includes(url) substring gate — a superstring URL (pull/771 ⊃ pull/77) passes and falsifies creator attribution (probe e2e)
  • …and 12 more (see the run report)

Convergence: round 14 posted 10 inline comment(s), 7 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/session-pr-backfill.ts (findings in round 13; 1 more now); packages/cli/src/serve/server/session-list.ts (findings in round 13; 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.)

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.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:test-efficacy — probe harness could not self-validate (no probe file was green in the unmutated baseline; vitest dist prerequisite in the probe tree)。

未探索到全部深度(达到工具调用预算):chunk 17:none — no checks were cut short.

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

收敛姿态下延后(第 14 轮,非阻断)——已记录,本轮不要求修改:共 32 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 14 轮发布了 10 条行内评论,其中 7 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/routes/session-pr-backfill.ts(第 13 轮已出过发现,本轮又有 1 条);packages/cli/src/serve/server/session-list.ts(第 13 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +542 to +543
!persistedNumbers.has(liveEntry.number) &&
ordered.length < SESSION_PR_LIST_LIMIT

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R13-1 (round 13 — re-checked at this commit, still stands; no commit touched this file since the comment): the live-only append loop gates on the running merged length (ordered.length < SESSION_PR_LIST_LIMIT) instead of the persisted list's length, so below the cap it drops the session's NEWEST live-only bindings once the running total reaches the cap — contradicting the comment directly above it ("below it a live-only entry is genuinely the newest"). The bind routes are bridge-first (append live, await upsertSessionPr, then reconcile); with the 2s-TTL persisted snapshot still short, two overlapping binds make live [P2..P9, B1, B2] against persisted [P1..P9] — the merge appends B1, ordered.length hits 10, and B2, the newest binding, fails the gate and is dropped, so the badge shows B1 as latest until the next fresh listing.

Witness (round-13 probe driving the real listWorkspaceSessionsForResponse):

persisted=[1..9], live=[2..9,11,12]:
  PR code  -> merged [1,2,3,4,5,6,7,8,9,11]  <- newest binding 12 dropped, stale 1 kept
  with fix -> merged [2,3,4,5,6,7,8,9,11,12]

Gate on the persisted size and let the existing final slice remain the only cap:

Suggested change
!persistedNumbers.has(liveEntry.number) &&
ordered.length < SESSION_PR_LIST_LIMIT
!persistedNumbers.has(liveEntry.number) &&
persistedPrs.length < SESSION_PR_LIST_LIMIT
中文说明

[Critical] R13-1(第 13 轮发现,本提交复核后仍然成立——该文件自评论后无任何提交):活列表追加循环用合并后列表的当前长度(ordered.length < SESSION_PR_LIST_LIMIT)而非持久化列表的长度作门槛,因此在未达上限时,一旦合并长度达到 10 就会丢弃会话最新的活绑定——与上方注释("未达上限时活条目确实是最新的")直接矛盾。绑定路由是先写 bridge 再 await upsertSessionPr 再对账;配合 2 秒 TTL 的持久化快照,两个并发绑定会形成活列表 [P2..P9, B1, B2] 对持久 [P1..P9]:合并追加 B1 后 ordered.length 到 10,最新绑定 B2 被门槛丢弃,徽章会把 B1 显示为最新,直到下次全新列表刷新。探针(驱动真实 listWorkspaceSessionsForResponse):PR 代码下合并结果 [1..9,11](最新绑定 12 被丢、过期的 1 保留);按建议修复后为 [2..9,11,12]。建议以持久列表长度作门槛,最终 slice 继续作为唯一上限。

— qwen3.8-max via Qwen Code /review (v0.22.0)

expect(sessionService.emitSessionPrBound).not.toHaveBeenCalled();
});

it('binds when a promote is refused after the command settled', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R13-2 (round 13 — re-checked at this commit, still stands): a SUCCESSFUL promote and any is_background: true gh pr create never reach bindGhPrCreate — its sole call site (shell.ts:2765) sits AFTER the if (result.promoted) early return, the promote settle handler only transitions the registry entry, and executeBackground never calls it — yet this PR removed the transcript gh pr create backfill source whose committed justification (session-pr-backfill.ts) still asserts "live creates bind through the shell tool post-hook". So a Ctrl+B mid-gh pr create --fill (promote succeeds), or an agent-run background create, completes exit 0 printing the PR URL and the session badge never shows the PR — permanently, with no recovery path (backfill sources are only /review commands and the worktree convention; the refresh sweep only re-states existing bindings). The suite pins only the promote-REFUSED arm (this test); there is no exclusion note in bindGhPrCreate's doc comment and no negative test.

Re-run the same gh-verified gate (pre/post snapshot, open-state, repo-key, branch-identity, URL-in-output) when a background or promoted shell settles — the settle hook already exists. If background/promoted creates are deliberately out of scope instead, say so in bindGhPrCreate's doc comment, correct the backfill comment that justifies the transcript-source removal, and pin the exclusion with a negative test next to this one.

中文说明

[Critical] R13-2(第 13 轮发现,本提交复核后仍然成立):成功 promote 以及任何 is_background: truegh pr create 都到不了 bindGhPrCreate——其唯一调用点(shell.ts:2765)位于 if (result.promoted) 提前返回之后,promote 落定处理器只转移 registry 状态,executeBackground 也从不触发它;而本 PR 又删除了 transcript gh pr create 回填源,其提交时的理由(session-pr-backfill.ts)仍声称"实时创建由 shell 工具后置钩子绑定"。于是:gh pr create --fill 进行中被 Ctrl+B 转后台(promote 成功)、或 agent 以后台方式执行创建,命令以 exit 0 结束并打印 PR URL,但会话徽章永远不会显示这个 PR——且没有任何恢复途径(回填源只有 /review 命令与 worktree 约定;刷新定时器只刷新已有绑定的状态)。测试只钉住了仍会绑定的 promote-被拒绝分支(本测试);bindGhPrCreate 文档注释无排除说明,也没有负例测试。建议:后台/promoted 命令落定时重跑同一套 gh 验证门(settle 钩子已存在);若刻意不覆盖这些形态,请在文档注释中说明、修正背书删除回填源的注释,并在此测试旁补一个负例钉住该排除。

— qwen3.8-max via Qwen Code /review (v0.22.0)

url = `${sources.remote}/pull/${number}`;
}
}
const state = sources.pageStateByNumber.get(number);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R13-3 (round 13 — re-checked at this commit, still stands; no commit touched this file since the comment): bindCandidateNumbers stamps state from the gh page by PR number alone, regardless of which repo the resolved url belongs to. In the fork layout — fork-trust branch holding, gh page listing the PARENT's PRs — a /review <fork-url> form resolves the fork URL correctly but pairs it with the parent repo's same-numbered PR's state (PR numbers are per-repo, so that is a DIFFERENT PR): fork PR #7 open + parent PR #7 merged persists {number: 7, url: .../me/fork/pull/7, state: 'merged'} and the badge/tooltip renders the fork's OPEN PR as merged. The refresh sweep cannot correct it — it keys stamps by the page's repo key, looks each binding up under its own URL's key, and its own test pins fork-URL bindings as skipped — and 'merged' is terminal, so the wrong state persists.

Witness (round-13 probe, isolated tree):

fork layout, /review https://github.com/me/fork/pull/7, parent page listing PR 7 merged:
  PR code -> persisted {number:7, url:"https://github.com/me/fork/pull/7", state:"merged"}
  with fix (stamp only when the bound URL's repo matches the page's repo) -> state dropped;
  all 38 shipped backfill tests still pass

Thread pageRepoKey into sources and stamp state only when the bound URL's repo matches the page's repo:

const state =
  url !== undefined && repoKeyFromWebUrl(url) === sources.pageRepoKey
    ? sources.pageStateByNumber.get(number)
    : undefined;
中文说明

[Critical] R13-3(第 13 轮发现,本提交复核后仍然成立——该文件自评论后无任何提交):bindCandidateNumbers 仅按 PR 编号从 gh 页面取 state,不检查解析出的 url 属于哪个仓库。在 fork 布局下(fork 信任分支成立、gh 页面列出的是父仓库的 PR),/review <fork-url> 形式能正确解析出 fork 的 URL,却配上了父仓库同号 PR 的状态(PR 编号按仓库计,这是另一个 PR):fork 的 PR #7 是 open、父仓库的 PR #7 是 merged 时,会持久化 {number: 7, url: .../me/fork/pull/7, state: 'merged'},徽章/工具提示会把 fork 的 OPEN PR 显示为已合并。刷新定时器也纠正不了:它按页面仓库 key 建状态索引、按绑定自身 URL 的 key 查找(其自身测试已钉住 fork-URL 绑定会被跳过),且 'merged' 是终态,错误状态会一直存在。建议把 pageRepoKey 传入 sources,仅当绑定 URL 的仓库与页面仓库一致时打标(见上方代码)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

if (slug.includes('..') || slug.startsWith('.') || slug.startsWith('-')) {
return 'Worktree name must not start with "." or "-" or contain "..".';
}
if (/^pr-[1-9]\d{0,8}$/.test(slug)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-1: the new pr-<number> reservation sits in the shared validateUserWorktreeSlug, which createUserWorktree (gitWorktreeService.ts:1648) also runs — so it rejects the exact system-generated pr-<N> slug that the --worktree=#<N> launch produces, breaking PR-worktree creation; the same validator gates exit_worktree and --worktree <slug> re-attach, so existing pr-<N> worktrees can no longer be exited or re-entered by name.

qwen --worktree=#42 when .qwen/worktrees/pr-42 does not exist: worktreeStartup resolves slug pr-42 (bypassing user-slug validation by design), fetches the PR ref successfully, then calls service.createUserWorktree('pr-42', sha) — this reservation returns Worktree name must not look like "pr-<number>"… and startup aborts. exit_worktree({name:'pr-42'}) fails validateToolParams for the same reason, so users inside a legitimate PR worktree cannot keep/remove it through the tool — while this PR's own backfill treats the pr-<N> sidecar convention as its highest-authority binding source, and the new unit test cements the rejection.

Witness: the pre-existing packages/cli/src/startup/worktreeStartup.test.ts "creates a pr- worktree from FETCH_HEAD when fetch succeeds (local fake remote)" FAILS deterministically at this commit (expect(res!.ok).toBe(true) → false, 3 consecutive runs) and PASSES 18/18 at the merge base; validateUserWorktreeSlug('pr-42') returning that exact rejection string was reproduced against the built dist.

Keep the reservation for user-typed slugs only: bypass it for internal PR worktree creation (e.g. an internal option on createUserWorktree for the --worktree=#<N> launch path), and let exit_worktree / the startup re-attach path accept existing pr-<N> slugs.

中文说明

[Critical] R14-1:新增的 pr-<number> 保留名校验放在共享的 validateUserWorktreeSlug 中,而 createUserWorktree(gitWorktreeService.ts:1648)也会执行该校验——因此它会拒绝 --worktree=#<N> 启动路径自身生成的系统 slug pr-<N>,导致 PR worktree 创建失败;同一校验也把守着 exit_worktree--worktree <slug> 重挂载,已有的 pr-<N> worktree 从此无法按名退出或重新进入。失败场景:.qwen/worktrees/pr-42 不存在时运行 qwen --worktree=#42:worktreeStartup 按设计绕过用户 slug 校验解析出 pr-42、成功 fetch PR ref,随后调用 createUserWorktree('pr-42', sha)——本保留名校验返回错误、启动中止;exit_worktree({name:'pr-42'}) 也因同样原因过不了参数校验——而本 PR 自己的回填却把 pr-<N> sidecar 约定当作最高权威绑定源,新加的单测还把该拒绝固化了下来。证据:既有测试 worktreeStartup.test.ts "creates a pr- worktree from FETCH_HEAD when fetch succeeds" 在本提交确定性失败(3 次连续运行),在合并基上 18/18 全绿;validateUserWorktreeSlug('pr-42') 的拒绝串已在构建产物上复现。建议:保留名只约束用户手输的 slug——为内部 PR worktree 创建路径(如 createUserWorktree 增加内部选项)绕过该校验,并让 exit_worktree / 启动重挂载路径接受已有的 pr-<N> slug。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +523 to +525
return liveEntry
? {
...liveEntry,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-2: the merge spreads ...liveEntry unconditionally for numbers present in both stores, so when a sidecar-only writer (the shell hook / backfill) re-binds a PR number to a DIFFERENT repository, every list response keeps the stale live entry's url while the persisted order correctly puts the re-bind last — the badge links to the wrong repository's PR until daemon restart or another metadata bind on that session. Only the state overlay is guarded by p.url === liveEntry.url; the URL direction is unargued. The shell hook's own comment names this re-bind shape ("A same-number entry pointing at ANOTHER repo … is re-bound here"), and emitSessionPrBound is notification-only — nothing reconciles the live entry afterwards (the three merge call sites are read-only response builders).

Session binds repo-a's PR #5 via the metadata route (both stores hold {number:5, url:repo-a}); a gh pr create in the same session creates repo-b's PR #5 and the shell post-hook rewrites the sidecar entry to repo-b — every subsequent list request hits liveByNumber.get(5), {...liveEntry} wins, and the newest binding renders repo-a's URL.

Witness (verifier probe against the unmodified built dist): sidecar upserted repo-a #5 then re-bound to repo-b #5 (source:'create'), live left at repo-a → merged prs: [{"number":5,"url":"https://github.com/repo-a/r/pull/5"}] (stale live URL wins); the same probe with the one-line fix below → url repo-b (correct), both arms verified.

Resolve URL conflicts in favor of the persisted entry — the merge's own comment names the sidecar "the append-only binding-time record" (the opposite race is bounded to one request because the bind route invalidates the session-list cache in its finally):

return liveEntry
  ? {
      ...liveEntry,
      url: p.url,
      ...(p.state !== undefined && p.url === liveEntry.url
        ? { state: p.state }
        : {}),
中文说明

[Critical] R14-2:合并逻辑对两个存储中都存在的编号无条件展开 ...liveEntry,因此当仅写 sidecar 的写入方(shell 钩子/回填)把某个 PR 编号重新绑定到另一个仓库时,每次列表响应都会保留过期活条目的 url,而持久化顺序已正确把重绑定排在最后——徽章会链接到错误仓库的 PR,直到 daemon 重启或该会话再次发生元数据绑定。只有 state 覆盖有 p.url === liveEntry.url 守卫,URL 方向毫无防护。shell 钩子自己的注释就描述了这种重绑定形态("指向其他仓库的同编号条目……在此被重新绑定"),而 emitSessionPrBound 只是通知——之后没有任何东西对账活条目(三处合并调用点都是只读的响应构建器)。失败场景:会话先经元数据路由绑定 repo-a 的 PR #5(两存储均为 repo-a);同一会话里 gh pr create 创建了 repo-b 的 PR #5,shell 后置钩子把 sidecar 改写为 repo-b——此后每次列表请求命中 liveByNumber.get(5){...liveEntry} 胜出,最新绑定渲染的是 repo-a 的 URL。证据(验证探针驱动未改动的构建产物):先写 repo-a #5 再重绑定为 repo-b #5 后,合并结果为 repo-a 的 URL(过期活 URL 胜出);应用下方单行修复后为 repo-b(正确),两侧均验证。建议:URL 冲突时以持久化条目为准——合并逻辑自己的注释就说 sidecar 是"按绑定时间追加的权威记录"(反向竞态只影响一次请求,因为绑定路由在 finally 里会使会话列表缓存失效)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

[preRunPrSnapshot, preRunBranch, preRunRepoKeys] = await Promise.all([
fetchCurrentBranchPullRequest(cwd, preRunGhEnv),
fetchCurrentBranchName(cwd),
fetchAttributionRepoKeys(cwd),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-3: the pre-run repo-identity verification leg is the only gh leg called WITHOUT the command's inline GH_*/GITHUB_* credentials (preRunGhEnv), although fetchAttributionRepoKeys declares an env? parameter and the comment two lines above requires every verification leg to authenticate the way the create itself does.

Agent runs GH_TOKEN=x gh pr create --fill on a host with no ambient gh auth — the exact token shape this PR added ghPrCreateInlineEnv to support. The create succeeds (the spawned command carries the token), but gh repo view --json url,parent runs bare, fails authentication, resolves {}, and bindGhPrCreate's repo gate (createdRepoKey !== preRunRepoKeys.resolved && !== .parent, both undefined) declines — the session's PR is created but never bound, silently, every time. Backfill cannot recover it (transcript traces were deliberately removed as a source).

Witness (verifier probe): RAW gh repo view (no token): exit 4 | gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable; fetchAttributionRepoKeys(repo) [no env arg] => {}; bindGhPrCreate repo gate declines? true; the fix shape resolves {resolved: 'github.com/cli/cli'}. Note: threading the PARTIAL record alone is not sufficient — see R14-4; pass a full-environment overlay.

fetchAttributionRepoKeys(cwd, ghEnv), // ghEnv = { ...process.env, ...preRunGhEnv }
中文说明

[Critical] R14-3:预运行仓库身份验证腿是唯一没有传入命令内联 GH_*/GITHUB_* 凭据(preRunGhEnv)的 gh 腿,尽管 fetchAttributionRepoKeys 声明了 env? 参数、且上方两行的注释要求每条验证腿都以与创建命令相同的方式认证。失败场景:在没有环境 gh 认证的主机上运行 GH_TOKEN=x gh pr create --fill——正是本 PR 新增 ghPrCreateInlineEnv 要支持的形态。创建本身成功(生成的命令自带 token),但 gh repo view --json url,parent 裸跑、认证失败、解析为 {}bindGhPrCreate 的仓库门(两个值均为 undefined)拒绝——会话的 PR 创建成功却永远不绑定,且静默发生。回填也恢复不了(transcript 痕迹已被有意移除)。证据(验证探针):裸跑 gh repo view exit 4;fetchAttributionRepoKeys 无 env 参数返回 {};仓库门拒绝;修复形态可解析出仓库键。注意:只传部分记录({GH_TOKEN})并不够——见 R14-4,请传完整环境叠加。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// advertised token shape binds nothing.
preRunGhEnv = ghPrCreateInlineEnv(commandToExecute);
[preRunPrSnapshot, preRunBranch, preRunRepoKeys] = await Promise.all([
fetchCurrentBranchPullRequest(cwd, preRunGhEnv),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-4: the inline gh env is passed as a PARTIAL record ({GH_TOKEN: …} extracted by ghPrCreateInlineEnv), but gitEnv(base) in git-branches.ts spreads base ?? process.env as the child's ENTIRE environment — so the gh verification legs spawn with no PATH, HOME, or any other process variable. execFile('gh') then fails to resolve the binary outside libc's /bin:/usr/bin fallback (ENOENT), and even a gh in /usr/bin dies with "unable to find git executable in PATH". The same defect hits the post-run leg (~3145).

Every inline-token gh pr create — including the clean GH_TOKEN=t0ken gh pr create --fill shape the new unit test advertises — spawns its verification legs with env keys [GH_TOKEN, LANG, LC_ALL] only; the pre-run leg resolves {status:'error'} and bindGhPrCreate declines at the errored-snapshot arm (~3140), so the advertised token shape binds nothing. The test pins the broken shape (toHaveBeenCalledWith('/test/dir', { GH_TOKEN: 't0ken' })) with execFile mocked throughout, hiding the PATH loss.

Witness (verifier probe, fake gh on a PATH-only location): ARM A (env=undefined, PATH inherited): {status:'pr',number:77} / ARM B (env={GH_TOKEN:x}, the reviewed shape): {status:'error'} / ARM C (env=process.env+GH_TOKEN): {status:'pr',number:77}; also env -i GH_TOKEN=… /usr/bin/gh repo view → "unable to find git executable in PATH".

Merge the inline assignments onto the full process env before passing (gitEnv still strips the repo-shifting GIT_*/GH_REPO vars):

const ghEnv = preRunGhEnv ? { ...process.env, ...preRunGhEnv } : undefined;
// pass ghEnv to both fetchCurrentBranchPullRequest legs and fetchAttributionRepoKeys
中文说明

[Critical] R14-4:内联 gh 环境是以部分记录(ghPrCreateInlineEnv 提取出的 {GH_TOKEN: …})传入的,但 git-branches.ts 的 gitEnv(base)base ?? process.env 整体作为子进程的全部环境——于是各 gh 验证腿生成时没有 PATHHOME 等任何进程变量。execFile('gh') 在 libc 的 /bin:/usr/bin 回退之外无法解析可执行文件(ENOENT),即便 gh 在 /usr/bin 也会报 "unable to find git executable in PATH"。同一缺陷也命中后置验证腿(约 3145 行)。失败场景:任何内联 token 的 gh pr create——包括新单测展示的标准形态 GH_TOKEN=t0ken gh pr create --fill——其验证腿的子进程环境只有 [GH_TOKEN, LANG, LC_ALL];预运行腿解析为 {status:'error'}bindGhPrCreate 在错误快照分支(约 3140 行)拒绝——宣传的 token 形态什么都绑定不了。单测因全程 mock execFile 而钉住了这个坏形态,掩盖了 PATH 丢失。证据(验证探针,gh 放在仅 PATH 可达的位置):ARM A(env=undefined,继承 PATH)→ {status:'pr'};ARM B(env={GH_TOKEN:x},即被审查形态)→ {status:'error'};ARM C(env=process.env+GH_TOKEN)→ {status:'pr'}。建议:传入前把内联赋值合并到完整进程环境上(见上方代码),并同时修复后置腿与 fetchAttributionRepoKeys

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +173 to +174
const GH_INLINE_ENV_ASSIGNMENT_PATTERN =
/^(GH_[A-Za-z0-9_]*|GITHUB_[A-Za-z0-9_]*)=(\S+)$/;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-5: ghPrCreateInlineEnv captures inline assignment values as raw shell text — this pattern keeps surrounding quotes and never expands $VAR/${VAR} — so the gh verification legs authenticate with a different credential than the real gh pr create used, and the binding silently misses.

A session with no ambient gh auth runs GH_TOKEN="$SECRET" gh pr create --fill or GH_TOKEN=$CI_TOKEN gh pr create --fill (both pass the segment gate). The capture returns GH_TOKEN with literal quotes / the unexpanded $CI_TOKEN; gitEnv spreads the record verbatim into execFile's env (Node performs no shell unquoting/expansion), so gh pr view/gh repo view receive the literal text, fail auth, the pre-run snapshot resolves {status:'error'}, and bindGhPrCreate declines — the successfully created PR is never bound. Quoted literals and variable references are the common shapes for inline tokens in CI.

Witness (verifier probe on the committed function): GH_TOKEN="$SECRET" gh pr create --fill → capture {"GH_TOKEN":"\"$SECRET\""}; GH_TOKEN=$CI_TOKEN …{"GH_TOKEN":"$CI_TOKEN"}; driving the committed fetchCurrentBranchPullRequest with those captures (ambient auth removed) → {status:'error'} for both arms; the candidate fix (strip matching quotes + expand $VAR + full-env overlay) flipped both arms to {status:'none'}.

Strip one layer of matching surrounding quotes from the captured value; for values containing $, backticks, or quotes that cannot be resolved, decline the extraction (return undefined) or expand $VAR/${VAR} at the call site against the child's effective environment.

中文说明

[Critical] R14-5:ghPrCreateInlineEnv 以原始 shell 文本捕获内联赋值——该模式保留引号、且从不展开 $VAR/${VAR}——导致 gh 验证腿使用的凭据与真实 gh pr create 所用的不同,绑定静默丢失。失败场景:无环境 gh 认证的会话运行 GH_TOKEN="$SECRET" gh pr create --fillGH_TOKEN=$CI_TOKEN gh pr create --fill(两者都通过段闸门)。捕获结果分别是带字面引号的 GH_TOKEN / 未展开的 $CI_TOKENgitEnv 原样把它铺进 execFile 的环境(Node 不做 shell 去引号/展开),于是 gh pr view/gh repo view 收到字面文本、认证失败,预运行快照解析为 {status:'error'}bindGhPrCreate 拒绝——成功创建的 PR 永不绑定。引号字面量与变量引用正是 CI 中内联 token 的常见形态。证据(验证探针,作用于已提交函数):两种形态分别捕获为 {"GH_TOKEN":"\"$SECRET\""}{"GH_TOKEN":"$CI_TOKEN"};用这些捕获值驱动真实 fetchCurrentBranchPullRequest(移除环境认证)→ 两臂均 {status:'error'};候选修复(去匹配引号 + 展开 $VAR + 完整环境叠加)使两臂翻转为 {status:'none'}。建议:对捕获值剥去一层匹配的成对引号;对含 $、反引号或无法解析引号的值,拒绝提取(返回 undefined),或在调用处按子进程的有效环境展开 $VAR/${VAR}

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +207 to +212
if (previousWasFlag) {
// A flag's value (`sudo -u runner`), not the binary.
previousWasFlag = false;
continue;
}
break;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-6: the ghPrCreateInlineEnv token scan breaks on any non-GH_*/GITHUB_* assignment token, but the gate grammar it claims to mirror accepts arbitrary leading assignments ((?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*) — so one non-GH assignment prefixing the GH ones makes the extraction return undefined while commandRunsGhPrCreate still fires.

A session with no ambient gh auth runs CI=true GH_TOKEN=ghp_x gh pr create --fill. The gate returns true (verified against the exact segment regex), but the scan breaks at CI=true (not a GH assignment, not the binary, not a wrapper/flag → the terminating break below) and never captures GH_TOKEN; the verification legs run on ambient auth only, gh exits with an auth error, the pre-run snapshot becomes {status:'error'}, and bindGhPrCreate declines — the created PR is silently unbound. The only negative test (FOO=bar gh pr create --fill, no GH assignment at all) does not cover the mixed-prefix shape, so this stays green.

Witness (verifier probe on committed code): {command:'CI=true GH_TOKEN=ghp_x gh pr create --fill', gate:true, env:null}; the bare-auth leg → {status:'error'}; candidate fix (continue past NAME=value tokens instead of breaking) → captured env keys ['GH_TOKEN'], leg → {status:'none'}.

In the scan, consume any token matching the gate's assignment class (/^[A-Za-z_][A-Za-z0-9_]*=\S+$/) with continue (capturing only GH_/GITHUB_ ones) instead of breaking, so the scan and the gate admit the same prefix grammar.

中文说明

[Critical] R14-6:ghPrCreateInlineEnv 的 token 扫描遇到任何非 GH_*/GITHUB_* 赋值 token 就 break,但它声称对齐的闸门文法接受任意前导赋值——于是 GH 赋值之前多一个非 GH 赋值,提取就返回 undefined,而 commandRunsGhPrCreate 仍然触发。失败场景:无环境 gh 认证的会话运行 CI=true GH_TOKEN=ghp_x gh pr create --fill:闸门返回 true(已按确切段正则验证),但扫描在 CI=true 处 break(下方这个终结 break),永远捕获不到 GH_TOKEN;验证腿只能用环境认证裸跑、gh 认证失败、预运行快照变为 {status:'error'}bindGhPrCreate 拒绝——创建出的 PR 静默不绑定。唯一的负例测试(FOO=bar gh pr create --fill,完全无 GH 赋值)不覆盖混合前缀形态,因此保持绿色。证据(验证探针,作用于已提交代码):{gate:true, env:null};裸认证腿 {status:'error'};候选修复(对 NAME=value token 用 continue 而非 break)→ 捕获到 ['GH_TOKEN'],腿 → {status:'none'}。建议:扫描对匹配闸门赋值类的任意 token 用 continue 消费(只捕获 GH_/GITHUB_),使扫描与闸门接受同一前缀文法。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +375 to +378
const prPath = sessionService.getPrSessionPathForArchiveState(
candidate.sessionId,
candidate.archiveState,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-7: backfill fixes each candidate's archive state at enumeration time (candidate.archiveState) and never re-checks it before writing, so an archive/restore transition landing during the scan+gh window strands the new bindings in the WRONG archive state's chats dir — an orphan sidecar no reader of the session's current state ever reads. The sibling shell binder explicitly re-resolves its location for this exact race ("an archive transition landing during the gh round-trip above must not strand the binding", shell.ts); this writer does not, and the grep finds no getSessionLocation call anywhere in this file.

POST /sessions/backfill-prs on a workspace with many transcripts: it collects all candidates first (reading every transcript fully), then runs the git/gh legs (~4s+), then writes per candidate. During that window the user archives session S — moveSessionPrSidecar moves the sidecar to the archived dir — but the write phase still holds archiveState: 'active' for S: readSessionPrs on the now-absent active path returns null (freeSlots = 10), and upsertSessionPrswithSidecarLock materializes and writes a brand-new ACTIVE sidecar for an archived session. The badge reads the archived path and never shows the bindings; moveSessionPrSidecar's merge-heal only fires on S's NEXT transition, which an archived session may never have.

Witness (verifier probe — the mocked fetch performs the daemon's real archiveSessions inside the window): the session genuinely archived, backfill reported {scanned:1, bound:1, failed:0}, transcript in archive dir: true — but ACTIVE sidecar holds [{number:7,…,source:'worktree'}] and ARCHIVED sidecar: null (orphaned).

Re-resolve the session's current archive state just before the upsert (mirroring the shell binder's getSessionLocation re-resolve) and skip or re-target the write when it no longer matches the enumerated state.

中文说明

[Critical] R14-7:回填在枚举时就固定了每个候选的归档状态(candidate.archiveState),写入前不再复核;因此在"扫描+gh"窗口内发生的归档/恢复迁移,会把新绑定写进错误归档状态的 chats 目录——成为一个任何当前状态读取方都读不到的孤儿 sidecar。兄弟的 shell 绑定器正是为这个竞态显式重解析位置(shell.ts:"gh 往返期间发生的归档迁移不得使绑定搁浅在复活的 active sidecar 上");本写入方没有,整个文件也搜不到任何 getSessionLocation 调用。失败场景:对含大量 transcript 的 workspace 触发 POST /sessions/backfill-prs:先收集全部候选(整读每个 transcript),再跑 git/gh 腿(约 4 秒以上),然后逐候选写入。窗口内用户归档会话 S——moveSessionPrSidecar 把 sidecar 移入归档目录——但写阶段仍持有 S 的 archiveState: 'active':对已不存在的 active 路径 readSessionPrs 返回 null(freeSlots = 10),upsertSessionPrswithSidecarLock 物化并为已归档会话写入一个全新的 ACTIVE sidecar。徽章读归档路径,永远看不到绑定;moveSessionPrSidecar 的合并修复只在 S 的下一次迁移时触发,而归档会话可能永远没有下一次。证据(验证探针——mock 的 fetch 在窗口内执行 daemon 真实的 archiveSessions):会话确实被归档,回填报告 {scanned:1, bound:1, failed:0},transcript 已在归档目录——但 ACTIVE sidecar 持有 [{number:7,…}],ARCHIVED sidecar 为 null(被孤儿化)。建议:在 upsert 前重解析会话当前归档状态(对齐 shell 绑定器的 getSessionLocation 重解析),与枚举状态不符时跳过或改写目标。

— qwen3.8-max via Qwen Code /review (v0.22.0)

wenshao and others added 3 commits August 26, 2026 13:51
The pr-<N> slug reservation (review R1-21) also rejected the legitimate
PR-backed path: setupStartupWorktree("QwenLM#42") routes through
createUserWorktree, whose internal slug validation reserved the shape
for PR-backed worktrees while refusing to create one. Add a prBacked
option that lifts only the reservation check; user-chosen slugs keep it.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: test-efficacy — probe harness could not self-validate (probe baseline collected nothing; vitest dist prerequisite in the probe tree).

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Deferred under the convergence posture (round 15, not a blocker) — recorded, not requested in this round:

  • docs/design/2026-08-20-webshell-session-pr-binding.md:69 — [review] design doc describes a re-offer-based eviction policy the…
  • packages/acp-bridge/src/bridge.ts:9960 — [review] no-op guard keyed on the LATEST entry only —…
  • packages/acp-bridge/src/bridge.ts:10027 — [review] setSessionPrs reconciles AFTER the event publish/catalog…
  • packages/cli/src/acp-integration/session/Session.ts:8254 — [review] Session-side pr-bound callback registration + dispose…
  • packages/cli/src/serve/acp-http/transport.test.ts:5561 — [review] setSessionPrs reconciliation assertion cannot distinguish…
  • packages/cli/src/serve/fast-path-open.test.ts:113 — [review] vi.waitFor 1s→10s flake fix applied to 3 of 7 polls; the…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:208 — [review] env argument pinned only on the fetchGitHubPullRequests…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:749 — [review] run-4 test pins starvation: with one free slot the offer…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:782 — [review] no positive /review #N test although the pattern carries a…
  • packages/cli/src/serve/routes/session-pr-backfill.test.ts:1255 — [review] backfill failed counter / unwritable-sidecar…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:184 — [review] dead DI surface: fetchPullRequests/fetchRepoKeys optional…
  • packages/cli/src/serve/routes/session-pr-backfill.ts:513 — [review] backfill route registers non-strict mutate() while the…
  • packages/cli/src/serve/routes/session.ts:2275 — [review] no positive state test through either metadata route…
  • packages/cli/src/serve/run-qwen-serve.ts:6093 — [review] daemon wiring of the state-refresh sweep untested…
  • packages/cli/src/serve/run-qwen-serve.ts:6100 — [review] dynamic import of session-pr-refresh.js has no .catch — an…
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:111 — [review] sweep suite never exercises the 'archived' archive-state…
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:186 — [review] sweep suite pins the credential-bearing env argument as…
  • packages/cli/src/serve/server/session-pr-refresh.test.ts:437 — [review] "one slim gh pr list per workspace per sweep" has no…
  • packages/cli/src/serve/server/session-pr-refresh.ts:48 — [review] resolveSessionPrRefreshIntervalMs clamps only the upper…
  • packages/cli/src/serve/server/session-pr-refresh.ts:161 — [review] one poisoned sidecar aborts the entire workspace sweep (no…
  • …and 22 more (see the run report)

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.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:test-efficacy — probe harness could not self-validate (probe baseline collected nothing; vitest dist prerequisite in the probe tree)。

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

收敛姿态下延后(第 15 轮,非阻断)——已记录,本轮不要求修改:共 42 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +541 to +546
if (
!persistedNumbers.has(liveEntry.number) &&
ordered.length < SESSION_PR_LIST_LIMIT
) {
ordered.push(liveEntry);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R13-1 (round 13 — re-checked at this commit, still stands): the live-only append loop gates on the running merged length (ordered.length < SESSION_PR_LIST_LIMIT) instead of the persisted list's length, so below the cap it drops the session's NEWEST live-only bindings once the running total reaches the cap — contradicting the comment directly above it ("below it a live-only entry is genuinely the newest").

Failure scenario: with the persisted list below the cap (e.g. 8 entries) and several live-only bindings present, the loop stops appending once the running total hits 10 — the newest bindings are dropped, while the final slice(-SESSION_PR_LIST_LIMIT) would have kept them and evicted the oldest persisted entries instead, per the sidecar's binding-time order the same comment appeals to.

Witness: not run — ledger re-check: the merged-length gate is present unchanged in the rewritten merge at the reviewed commit.

Suggested change
if (
!persistedNumbers.has(liveEntry.number) &&
ordered.length < SESSION_PR_LIST_LIMIT
) {
ordered.push(liveEntry);
}
if (!persistedNumbers.has(liveEntry.number)) {
ordered.push(liveEntry);
}
中文说明

[Critical] R13-1(第 13 轮提出——本轮在该提交上复查,仍然成立):仅存在于 live 条目的追加循环以“合并后的当前长度”(ordered.length < SESSION_PR_LIST_LIMIT)为闸门,而不是以持久化列表的长度为闸门——因此在未达上限时,一旦累计长度达到上限就会丢弃会话最新的 live-only 绑定,与正上方注释(“未达上限时,live-only 条目确实是最新的”)直接矛盾。

触发场景:持久化列表未达上限(例如 8 条)且存在多个 live-only 绑定时,循环在累计达到 10 时停止追加——最新的绑定被丢弃;而末尾的 slice(-SESSION_PR_LIST_LIMIT) 本可以保留它们、改为逐出最旧的持久化条目,这才符合该注释所援引的 sidecar 绑定时间顺序。

证据:未执行——账本复查:被重写后的合并逻辑中该“累计长度”闸门原样存在。

建议修复:改为不以累计长度为闸门追加(让末尾的 slice 负责逐出最旧条目)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +2764 to +2765
if ((!result.aborted || wasPromoteRefused) && result.exitCode === 0) {
this.bindGhPrCreate(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R13-2 (round 13 — re-checked at this commit, still stands): a SUCCESSFUL promote and any is_background: true gh pr create never reach bindGhPrCreate — this sole call site sits AFTER the if (result.promoted) early return above, and executeBackground never calls it — yet this PR removed the transcript gh pr create backfill source, so these creates are permanently unbound.

Failure scenario: the user presses Ctrl+B mid-run and the promote succeeds (or the agent passes is_background: true); the child exits 0 having created the PR; no sidecar write, no emitSessionPrBound, and backfill deliberately never attributes gh pr create traces — the session badge never shows the PR this session created. The design update accepts the pure-background shape as a limitation, but not the successful-promote shape.

Witness: not run — ledger re-check: sole call site verified after the promoted early return at the reviewed commit; corroborated this round by both convergence-pair auditors tracing the same call graph.

Suggested fix: capture the same pre-run snapshot in executeBackground / the promote settle hook and run the binding gate when the entry settles with exit code 0, or refuse gh pr create-shaped commands in background mode the way git commit is refused.

中文说明

[Critical] R13-2(第 13 轮提出——本轮在该提交上复查,仍然成立):成功的 promote 以及任何 is_background: truegh pr create 都永远到不了 bindGhPrCreate——这个唯一调用点位于上方 if (result.promoted) 提前返回之后,而 executeBackground 从不调用它——但本 PR 又移除了转录中的 gh pr create 回填来源,因此这类创建永久无法绑定。

触发场景:用户在运行中按 Ctrl+B 且 promote 成功(或 agent 传入 is_background: true);子进程以退出码 0 结束并已创建 PR;没有 sidecar 写入、没有 emitSessionPrBound,而 backfill 又刻意不归因 gh pr create 痕迹——该会话的徽章永远不会显示这个 PR。设计更新仅把“纯后台”形态列为已知限制,并未包含“promote 成功”形态。

证据:未执行——账本复查:在审查提交上确认唯一调用点位于 promoted 提前返回之后;本轮两个收敛对审计者追踪同一调用图再次印证。

建议修复:在 executeBackground / promote settle 钩子中捕获同样的运行前快照,并在条目以退出码 0 结束时执行绑定闸门;或像拒绝 git commit 一样,在后台模式拒绝 gh pr create 形态的命令。

— qwen3.8-max via Qwen Code /review (v0.22.0)

url = `${sources.remote}/pull/${number}`;
}
}
const state = sources.pageStateByNumber.get(number);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R13-3 (round 13 — re-checked at this commit, still stands): bindCandidateNumbers stamps state from the gh page by PR number alone, regardless of which repo the resolved url belongs to.

Failure scenario: in the fork layout (fork-trust branch holding, gh page listing the PARENT's PRs) a /review <fork-url> form resolves the fork URL correctly but pairs it with the parent repo's same-numbered PR's state — a DIFFERENT PR, since numbers are per-repo; the sidecar then carries a state that belongs to another repository's PR.

Witness: not run — ledger re-check: const state = sources.pageStateByNumber.get(number) at the reviewed commit carries no repo check against the resolved url; the URL form path can resolve a fork URL while the page map is the parent's.

Suggested fix: key the state lookup on (repoKey, number) — only consume pageStateByNumber when the resolved url's repo key matches the page's repo key.

中文说明

[Critical] R13-3(第 13 轮提出——本轮在该提交上复查,仍然成立):bindCandidateNumbers 仅凭 PR 编号从 gh 页面打上 state,不管解析出的 url 属于哪个仓库。

触发场景:fork 布局下(fork 信任分支成立、gh 页面列出的是父仓库的 PR),/review <fork-url> 形态能正确解析出 fork 的 URL,却配上父仓库同号 PR 的状态——编号按仓库独立计数,那是另一个 PR;sidecar 因此带上了属于另一个仓库 PR 的状态。

证据:未执行——账本复查:审查提交上的 const state = sources.pageStateByNumber.get(number) 对解析出的 url 没有任何仓库校验;URL 形态路径可能解析出 fork URL,而页面映射却是父仓库的。

建议修复:以 (仓库键, 编号) 作为状态查找的键——仅当解析出的 url 的仓库键与页面仓库键一致时才消费 pageStateByNumber

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +1607 to +1610
if (
/^pr-[1-9]\d{0,8}$/.test(slug) &&
options?.allowPrBackedShape !== true
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-1 (re-checked at this commit — the creation half is fixed by prBacked: isPullRequest; the exit_worktree half still stands): the new pr-<number> reservation sits in the shared validateUserWorktreeSlug, and the unmodified ExitWorktreeTool.validateToolParams (exit-worktree.ts:596) calls it without allowPrBackedShape — so exit_worktree now rejects the exact PR-backed worktrees this PR's own --worktree=#<N> flow creates.

Failure scenario: a session launched with qwen --worktree=#42 runs inside the pr-42 worktree; when the user asks to leave or clean up, the agent calls exit_worktree({name: 'pr-42'}) and validation returns "Worktree name must not look like "pr-"" — for a worktree that IS PR-backed. Headless sessions have no WorktreeExitDialog fallback; only manual git worktree remove remains. exit_worktree never creates slugs, so the squatting concern cannot apply.

Witness: probe — ExitWorktreeTool.validateToolParams({name:'pr-42', action:'keep'})"Worktree name must not look like \"pr-<number>\": that shape is reserved for PR-backed worktrees."; control my-featurenull; fix arm (allowPrBackedShape: true) → null. Source: [probe]

Suggested change
if (
/^pr-[1-9]\d{0,8}$/.test(slug) &&
options?.allowPrBackedShape !== true
) {
if (
/^pr-[1-9]\d{0,8}$/.test(slug) &&
options?.allowPrBackedShape !== true &&
!options?.allowExistingPrBackedShape
) {

(or simply pass { allowPrBackedShape: true } from ExitWorktreeTool.validateToolParams)

中文说明

[Critical] R14-1(本轮复查——创建路径的一半已由 prBacked: isPullRequest 修复;exit_worktree 的一半仍然成立):新的 pr-<number> 保留位位于共享的 validateUserWorktreeSlug 中,而未被本 PR 修改的 ExitWorktreeTool.validateToolParams(exit-worktree.ts:596)调用它时不带 allowPrBackedShape——于是 exit_worktree 现在会拒绝本 PR 自己的 --worktree=#<N> 流程所创建的 PR 工作树。

触发场景:以 qwen --worktree=#42 启动的会话运行在 pr-42 工作树中;用户要求退出或清理时,agent 调用 exit_worktree({name: 'pr-42'}),校验返回“工作树名不能形如 pr-”——而它正是 PR 工作树。无头会话没有 WorktreeExitDialog 兜底,只剩手工 git worktree removeexit_worktree 从不创建 slug,因此抢占(squatting)顾虑在这里不可能成立。

证据:探针——ExitWorktreeTool.validateToolParams({name:'pr-42', action:'keep'}) → 返回保留位错误;对照 my-featurenull;修复分支(allowPrBackedShape: true)→ null。Source: [probe]

建议修复:在 ExitWorktreeTool.validateToolParams 中传入 { allowPrBackedShape: true }(任何已存在且形如该模式的工作树必然就是 PR 工作树)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +523 to +530
return liveEntry
? {
...liveEntry,
...(p.state !== undefined && p.url === liveEntry.url
? { state: p.state }
: {}),
}
: p;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-2 (round 14 — re-checked at this commit, still stands): the merge spreads ...liveEntry unconditionally for numbers present in both stores, so when a sidecar-only writer (the shell hook / backfill) re-binds a PR number to a DIFFERENT repository, every list response keeps the stale live entry's url while the persisted order correctly puts the re-bind last — the badge links to the wrong repository's PR until daemon restart or another live re-bind.

Only state carries the p.url === liveEntry.url guard; the url itself always comes from the live entry, although the sidecar is the authoritative binding-time record.

Witness: not run — ledger re-check: the unconditional ...liveEntry spread for matching numbers is present in mergeLiveSessionSummary at the reviewed commit; the same-URL guard exists only on the state stamp.

Suggested fix: when the persisted url differs from the live url, prefer the persisted entry (or at least its url) — the sidecar is authoritative for re-binds.

中文说明

[Critical] R14-2(第 14 轮提出——本轮在该提交上复查,仍然成立):合并时对两个存储中都存在的编号无条件展开 ...liveEntry——当仅写 sidecar 的写入方(shell 钩子 / backfill)把某个 PR 编号重新绑定到另一个仓库时,所有列表响应仍保留过期 live 条目的 url,而持久化顺序却正确地把重绑放在最后——徽章会链接到错误仓库的 PR,直到守护进程重启或再次 live 重绑。

只有 state 带有 p.url === liveEntry.url 守卫;url 本身永远取自 live 条目——尽管 sidecar 才是权威的绑定时间记录。

证据:未执行——账本复查:审查提交上的 mergeLiveSessionSummary 中对匹配编号的无条件 ...liveEntry 展开原样存在;同 URL 守卫只作用于 state 的打印。

建议修复:当持久化 url 与 live url 不同时,优先使用持久化条目(至少其 url)——重绑应以 sidecar 为准。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +2363 to +2365
fetchCurrentBranchPullRequest(cwd, preRunGhEnv),
fetchCurrentBranchName(cwd),
fetchAttributionRepoKeys(cwd),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-3 (round 14 — re-checked at this commit, still stands): the pre-run repo-identity verification leg is the only gh leg called WITHOUT the command's inline GH_*/GITHUB_* credentials (preRunGhEnv), although fetchAttributionRepoKeys declares an env? parameter and the comment two lines above requires every verification leg to authenticate the way the create itself does.

Failure scenario: on a daemon/CI host with no ambient gh auth, GH_TOKEN=x gh pr create --fill runs; gh repo view --json url,parent errors unauthenticated, fetchAttributionRepoKeys resolves {}, and the repo-key gate in bindGhPrCreate declines — every legitimate inline-token create silently never binds. The backfill route's identical call correctly passes the workspace env.

Witness: probe (sanitized env) — fetchAttributionRepoKeys(no env){}; raw gh spawn exits 4 with "To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable." Source: [probe]

Suggested change
fetchCurrentBranchPullRequest(cwd, preRunGhEnv),
fetchCurrentBranchName(cwd),
fetchAttributionRepoKeys(cwd),
fetchCurrentBranchPullRequest(cwd, preRunGhEnv),
fetchCurrentBranchName(cwd),
fetchAttributionRepoKeys(cwd, preRunGhEnv),
中文说明

[Critical] R14-3(第 14 轮提出——本轮在该提交上复查,仍然成立):运行前的仓库身份校验分支是唯一没有携带命令内联 GH_*/GITHUB_* 凭证(preRunGhEnv)的 gh 分支——尽管 fetchAttributionRepoKeys 声明了 env? 参数,且上方两行的注释要求每个校验分支都以与 create 本身相同的方式鉴权。

触发场景:在没有环境级 gh 鉴权的守护进程/CI 主机上执行 GH_TOKEN=x gh pr create --fillgh repo view --json url,parent 因未鉴权而报错,fetchAttributionRepoKeys 解析为 {}bindGhPrCreate 中的仓库键闸门拒绝——每一个合法的内联令牌创建都静默地永远无法绑定。backfill 路由中同样的调用正确地传入了工作区 env。

证据:探针(净化环境)——fetchAttributionRepoKeys(无 env){};裸 gh 派生以退出码 4 报 “To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable.”。Source: [probe]

建议修复:把 preRunGhEnv 也传给 fetchAttributionRepoKeys

— qwen3.8-max via Qwen Code /review (v0.22.0)

// The verification legs must authenticate the way the create itself
// does (inline GH_TOKEN with no ambient gh auth), or the gate's
// advertised token shape binds nothing.
preRunGhEnv = ghPrCreateInlineEnv(commandToExecute);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-4 (round 14 — re-checked at this commit, still stands): the inline gh env is passed as a PARTIAL record ({GH_TOKEN: …} extracted by ghPrCreateInlineEnv), but gitEnv(base) spreads base ?? process.env as the child's ENTIRE environment — so the gh verification legs spawn with no PATH, HOME, or any other process variable.

Failure scenario: ghPrCreateInlineEnv('GH_TOKEN=x gh pr create --fill') yields {GH_TOKEN:'x'}; gitEnv({GH_TOKEN:'x'}) yields only {GH_TOKEN, LC_ALL, LANG}; real gh under that env exits non-zero with "unable to find git executable in PATH", the snapshot resolves {status:'error'}, and bindGhPrCreate declines — every inline-token create (the exact shape the adjacent comment advertises) silently never binds, on every host, even after R14-3's fix passes env to the repo-keys leg.

Witness: probe — ARM A (PR code shape, partial inline env): {"status":"error"} / ARM B (merged over process.env): {"status":"none"} / raw partial-env gh spawn stderr: unable to find git executable in PATH; please install git before retrying (exit 1). Source: [probe]

Suggested change
preRunGhEnv = ghPrCreateInlineEnv(commandToExecute);
const inlineGhEnv = ghPrCreateInlineEnv(commandToExecute);
preRunGhEnv = inlineGhEnv ? { ...process.env, ...inlineGhEnv } : undefined;
中文说明

[Critical] R14-4(第 14 轮提出——本轮在该提交上复查,仍然成立):内联 gh env 以部分记录传入(ghPrCreateInlineEnv 提取出的 {GH_TOKEN: …}),但 gitEnv(base) 会把 base ?? process.env 作为子进程的全部环境展开——于是 gh 校验分支派生时没有 PATHHOME 或任何其他进程变量。

触发场景:ghPrCreateInlineEnv('GH_TOKEN=x gh pr create --fill') 得到 {GH_TOKEN:'x'}gitEnv({GH_TOKEN:'x'}) 只得到 {GH_TOKEN, LC_ALL, LANG};真实 gh 在该环境下以非零退出并报 “unable to find git executable in PATH”,快照解析为 {status:'error'}bindGhPrCreate 拒绝——每一个内联令牌创建(恰是相邻注释宣称支持的形态)在任何主机上都静默地永远无法绑定;即便 R14-3 修复后把 env 传给了仓库键分支也无济于事。

证据:探针——ARM A(PR 代码形态,部分内联 env):{"status":"error"} / ARM B(并入 process.env):{"status":"none"} / 裸部分 env 的 gh 派生 stderr:unable to find git executable in PATH; please install git before retrying(退出码 1)。Source: [probe]

建议修复:在调用点把内联赋值并入进程环境({ ...process.env, ...inlineGhEnv })。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +173 to +174
const GH_INLINE_ENV_ASSIGNMENT_PATTERN =
/^(GH_[A-Za-z0-9_]*|GITHUB_[A-Za-z0-9_]*)=(\S+)$/;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-5 (round 14 — re-checked at this commit, still stands): ghPrCreateInlineEnv captures inline assignment values as raw shell text — this pattern keeps surrounding quotes and never expands $VAR/${VAR} — so the gh verification legs authenticate with a different credential than the real gh pr create used, and the binding silently misses.

Failure scenario: a session with no ambient gh auth runs GH_TOKEN="$SECRET" gh pr create --fill (or GH_TOKEN=${TOK}): the extraction yields the literal quoted/unexpanded string, the verification legs authenticate with it (or fail to), and the binding silently misses while the create itself succeeded.

Witness: probe — GH_TOKEN="$SECRET" gh pr create --fill → extracted {"GH_TOKEN":"\"$SECRET\""} (quotes kept, variable unexpanded). Source: [probe]

Suggested fix: expand the common shell forms (strip matching quotes, expand $VAR/${VAR} from the process environment) before handing the value to the verification legs.

中文说明

[Critical] R14-5(第 14 轮提出——本轮在该提交上复查,仍然成立):ghPrCreateInlineEnv 把内联赋值的值当作原始 shell 文本捕获——该模式保留外层引号、且不展开 $VAR/${VAR}——于是 gh 校验分支用来鉴权的凭证与真实 gh pr create 使用的不同,绑定被静默漏掉。

触发场景:没有环境级 gh 鉴权的会话执行 GH_TOKEN="$SECRET" gh pr create --fill(或 GH_TOKEN=${TOK}):提取得到的是带引号/未展开的字面量,校验分支用它鉴权(或鉴权失败),创建本身成功了,绑定却被静默漏掉。

证据:探针——GH_TOKEN="$SECRET" gh pr create --fill → 提取结果 {"GH_TOKEN":"\"$SECRET\""}(引号保留、变量未展开)。Source: [probe]

建议修复:在把值交给校验分支之前展开常见 shell 形态(剥离配对引号、从进程环境展开 $VAR/${VAR})。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +207 to +212
if (previousWasFlag) {
// A flag's value (`sudo -u runner`), not the binary.
previousWasFlag = false;
continue;
}
break;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-6 (round 14 — re-checked at this commit, still stands): the ghPrCreateInlineEnv token scan breaks on any non-GH_*/GITHUB_* assignment token, but the gate grammar it claims to mirror accepts arbitrary leading assignments ((?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*) — so one non-GH assignment prefixing the GH ones makes the extraction return undefined while commandRunsGhPrCreate still fires.

Failure scenario: a session with no ambient gh auth runs FOO=bar GH_TOKEN=x gh pr create --fill: the gate returns true, the scan breaks at FOO=bar, preRunGhEnv is undefined, the verification legs run unauthenticated and error, and the binding silently misses. Likewise gh pr create --web; GH_TOKEN=x gh pr create --fill — the first gate-matching segment shadows the later segment's token.

Witness: probe — FOO=bar GH_TOKEN=x gh pr create --fill → gate true, extraction null; gh pr create --web; GH_TOKEN=x gh pr create --fill → gate true, extraction null. Source: [probe]

Suggested fix: skip non-GH assignments instead of breaking, and scan every gate-matching segment (not only the first).

中文说明

[Critical] R14-6(第 14 轮提出——本轮在该提交上复查,仍然成立):ghPrCreateInlineEnv 的令牌扫描遇到任何非 GH_*/GITHUB_* 的赋值令牌就会中断,但它声称对齐的闸门语法却接受任意前置赋值((?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*)——于是 GH 赋值前面只要有一个非 GH 赋值,提取就返回 undefined,而 commandRunsGhPrCreate 仍然触发。

触发场景:没有环境级 gh 鉴权的会话执行 FOO=bar GH_TOKEN=x gh pr create --fill:闸门返回 true,扫描在 FOO=bar 处中断,preRunGhEnv 为 undefined,校验分支无鉴权运行并报错,绑定被静默漏掉。同理 gh pr create --web; GH_TOKEN=x gh pr create --fill——第一个过闸门的段落遮蔽了后面段落的令牌。

证据:探针——FOO=bar GH_TOKEN=x gh pr create --fill → 闸门 true、提取 null;gh pr create --web; GH_TOKEN=x gh pr create --fill → 闸门 true、提取 null。Source: [probe]

建议修复:遇到非 GH 赋值时跳过而不是中断,并扫描每一个过闸门的段落(而不只是第一个)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +444 to +448
): Promise<void> {
const prPath = sessionService.getPrSessionPathForArchiveState(
candidate.sessionId,
candidate.archiveState,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-7 (round 14 — re-checked at this commit, still stands): backfill fixes each candidate's archive state at enumeration time (candidate.archiveState) and never re-checks it before writing, so an archive/restore transition landing during the scan+gh window strands the new bindings in the WRONG archive state's chats dir — an orphan sidecar no reader of the session's current state ever reads.

Failure scenario: the daemon archives a session while backfill is scanning it (archive is a documented routine bulk operation); the candidate's archiveState still says active, the gh round-trip lands, and upsertSessionPrs writes the active path of a now-archived session; listing/badge/sweep all resolve the sidecar by the session's CURRENT archive state, so the bindings never surface. The sibling shell binder explicitly re-resolves the archive location immediately before its locked mutation.

Witness: not run — ledger re-check: both prPath computations (pre-read at :375, write at :445) use candidate.archiveState captured at enumeration; no re-resolution exists before the write at the reviewed commit.

Suggested fix: re-resolve the archive state inside (or immediately before) the locked write, the way the shell binder does.

中文说明

[Critical] R14-7(第 14 轮提出——本轮在该提交上复查,仍然成立):backfill 在枚举时固化每个候选的归档状态(candidate.archiveState),写入前不再复查——扫描+gh 往返窗口内若发生归档/恢复迁移,新绑定会被写到错误归档状态的 chats 目录,成为一个无人读取的孤儿 sidecar(所有读取方都按会话当前状态解析路径)。

触发场景:backfill 扫描期间守护进程归档了某会话(归档是被文档化的例行批量操作);候选的 archiveState 仍是 active,gh 往返落定后 upsertSessionPrs 写入一个已归档会话的 active 路径;列表/徽章/刷新扫描都按会话当前归档状态解析 sidecar,这些绑定永远不会显现。同族的 shell 绑定器会在其加锁写入前显式重新解析归档位置。

证据:未执行——账本复查:两处 prPath 计算(:375 预读、:445 写入)都使用枚举时捕获的 candidate.archiveState;审查提交上写入前不存在任何重新解析。

建议修复:像 shell 绑定器一样,在加锁写入内部(或紧接其前)重新解析归档状态。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:

Autofix review round — PR #9739 (same-run verification repair)

The previous commit of this round was REJECTED by deterministic verification:
npm run build failed on the agent-committed fix with three strict-null
errors, and the gate's baseline A/B confirmed the failure belongs to this
round (the baseline leg at the pushed head was green). Per the same-run
repair rule, the rejected commit is PRESERVED and this round adds exactly one
verified follow-up commit fixing that rejection.

Rejection repaired

  • Deterministic rejection: tsc --build failed in the CLI package with
    error TS18047: 'reattached' is possibly 'null' at
    src/startup/worktreeStartup.test.ts lines 350–352.
  • Root cause: the R14-1 witness test guards with
    if (!reattached!.ok) return; — that narrows the non-null-asserted
    expression but NOT the reattached variable itself under strict
    nullability, so the three following reattached.context.* accesses are
    type errors. The sibling tests in the same file (first!.context,
    second!.context) already use the non-null-asserted access form.
  • Fix: one follow-up commit
    (fix(cli): repair strict-null build break in pr-42 re-attach test (#9739))
    changes exactly those three accesses to reattached!.context.*, matching
    the file's established convention. Type-level only — runtime behavior is
    identical by construction, so this commit adds no guard, branch, or
    behavior and needs no mutation probe.
  • **Reproduced befor

Why it was not pushed:

Note: the base has since been auto-updated; the verdict below predates that update, and the next round's re-measurement may charge the round.

tests failed in packages/cli

c52�[2m > �[22mshould write OSC 52 sequence to stderr when stdout is not TTY but stderr is �[33m 358�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m clipboardUtils�[2m > �[22mwriteOsc52�[2m > �[22mshould return false and not write when neither stdout nor stderr is TTY �[33m 381�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m clipboardUtils�[2m > �[22mwriteOsc52�[2m > �[22mshould handle special characters in text �[33m 402�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m clipboardUtils�[2m > �[22mwriteOsc52�[2m > �[22mshould handle empty string �[33m 422�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m clipboardUtils�[2m > �[22mwriteOsc52�[2m > �[22mshould return false on write error �[33m 394�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m clipboardUtils�[2m > �[22mwriteOsc52�[2m > �[22mshould wrap in tmux DCS envelope when TMUX is set �[33m 412�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m clipboardUtils�[2m > �[22mwriteOsc52�[2m > �[22mshould wrap in screen DCS envelope when STY is set �[33m 388�[2mms�[22m�[39m

�[31m⎯⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Tests 1 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m

�[41m�[1m FAIL �[22m�[49m src/commands/review/test-efficacy.test.ts�[2m > �[22mrestoreProbeTreeTracked, through runOneMutant�[2m > �[22mrefuses to run when the index hides a tracked file from the restore
�[31m�[1mError�[22m: vitest not found searching up from /tmp/qwen-skipwt-k8mrkK�[39m
�[36m �[2m❯�[22m findVitestBin src/commands/review/test-efficacy.ts:�[2m1361:13�[22m�[39m
    �[90m1359| �[39m    // folding it into "not found" sends the reader hunting a missing …
    �[90m1360| �[39m    if ((error as { code?: string }).code === 'MODULE_NOT_FOUND') {
    �[90m1361| �[39m      throw new Error(`vitest not found searching up from ${worktree}`…
    �[90m   | �[39m            �[31m^�[39m
    �[90m1362| �[39m    }
    �[90m1363| �[39m    throw error;
�[90m �[2m❯�[22m runProbeSuite src/commands/review/test-efficacy.ts:�[2m1771:5�[22m�[39m
�[90m �[2m❯�[22m attempt src/commands/review/test-efficacy.ts:�[2m2360:27�[22m�[39m
�[90m �[2m❯�[22m runOneMutant src/commands/review/test-efficacy.ts:�[2m2390:18�[22m�[39m
�[90m �[2m❯�[22m src/commands/review/test-efficacy.test.ts:�[2m560:17�[22m�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m


�[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m678 passed�[39m�[22m�[90m (679)�[39m
�[2m      Tests �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m20803 passed�[39m�[22m�[2m | �[22m�[33m87 skipped�[39m�[90m (20891)�[39m
�[2m   Start at �[22m 22:27:11
�[2m   Duration �[22m 211.55s�[2m (transform 393.17s, setup 118.26s, collect 5895.74s, tests 789.66s, environment 309.10s, prepare 115.32s)�[22m

JUNIT report written to /home/github-runner/actions-runner-test-19/_work/qwen-code/qwen-code/packages/cli/junit.xml
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /home/github-runner/actions-runner-test-19/_work/qwen-code/qwen-code/packages/cli
npm error workspace @qwen-code/qwen-code@0.22.0
npm error location /home/github-runner/actions-runner-test-19/_work/qwen-code/qwen-code/packages/cli
npm error command failed
npm error command sh -c vitest run --changed origin/main --passWithNoTests
中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

验证门的拒绝原因与日志证据见上方英文部分(gate-rejection 不翻译)。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/32969394174


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants